From 11f05ec14cb03f2eba1fe19701c73b2640930568 Mon Sep 17 00:00:00 2001 From: GiggleLiu Date: Mon, 23 Mar 2026 09:52:25 +0800 Subject: [PATCH 01/18] refactor: add aggregate value primitives --- src/types.rs | 178 +++++++++++++++++++++++++++++++++++++++- src/unit_tests/types.rs | 72 ++++++++++++++++ 2 files changed, 249 insertions(+), 1 deletion(-) diff --git a/src/types.rs b/src/types.rs index 2e615d0e9..8b3772120 100644 --- a/src/types.rs +++ b/src/types.rs @@ -1,6 +1,6 @@ //! Common types used across the problemreductions library. -use serde::de::{self, Visitor}; +use serde::de::{self, DeserializeOwned, Visitor}; use serde::{Deserialize, Deserializer, Serialize, Serializer}; use std::fmt; @@ -164,6 +164,182 @@ impl From for One { /// Backward-compatible alias for `One`. pub type Unweighted = One; +/// Foldable aggregate values for enumerating a problem's configuration space. +pub trait Aggregate: Clone + fmt::Debug + Serialize + DeserializeOwned { + /// Neutral element for folding. + fn identity() -> Self; + + /// Associative combine operation. + fn combine(self, other: Self) -> Self; + + /// Whether this aggregate admits representative witness configurations. + fn supports_witnesses() -> bool { + false + } + + /// Whether a configuration-level value belongs to the witness set + /// for the final aggregate value. + fn contributes_to_witnesses(_config_value: &Self, _total: &Self) -> bool { + false + } +} + +/// Maximum aggregate over feasible values. +#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)] +pub struct Max(pub Option); + +impl Aggregate for Max { + fn identity() -> Self { + Max(None) + } + + fn combine(self, other: Self) -> Self { + use std::cmp::Ordering; + + match (self.0, other.0) { + (None, rhs) => Max(rhs), + (lhs, None) => Max(lhs), + (Some(lhs), Some(rhs)) => { + let ord = lhs.partial_cmp(&rhs).expect("cannot compare values (NaN?)"); + match ord { + Ordering::Less => Max(Some(rhs)), + Ordering::Equal | Ordering::Greater => Max(Some(lhs)), + } + } + } + } + + fn supports_witnesses() -> bool { + true + } + + fn contributes_to_witnesses(config_value: &Self, total: &Self) -> bool { + matches!((config_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 { + Some(value) => write!(f, "Max({value})"), + None => write!(f, "Max(None)"), + } + } +} + +/// Minimum aggregate over feasible values. +#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)] +pub struct Min(pub Option); + +impl Aggregate for Min { + fn identity() -> Self { + Min(None) + } + + fn combine(self, other: Self) -> Self { + use std::cmp::Ordering; + + match (self.0, other.0) { + (None, rhs) => Min(rhs), + (lhs, None) => Min(lhs), + (Some(lhs), Some(rhs)) => { + let ord = lhs.partial_cmp(&rhs).expect("cannot compare values (NaN?)"); + match ord { + Ordering::Greater => Min(Some(rhs)), + Ordering::Equal | Ordering::Less => Min(Some(lhs)), + } + } + } + } + + fn supports_witnesses() -> bool { + true + } + + fn contributes_to_witnesses(config_value: &Self, total: &Self) -> bool { + matches!((config_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 { + Some(value) => write!(f, "Min({value})"), + None => write!(f, "Min(None)"), + } + } +} + +/// Sum aggregate for value-only problems. +#[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) -> Self { + let mut total = self.0; + total += other.0; + Sum(total) + } +} + +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); + +impl Aggregate for Or { + fn identity() -> Self { + Or(false) + } + + fn combine(self, other: Self) -> Self { + Or(self.0 || other.0) + } + + fn supports_witnesses() -> bool { + true + } + + fn contributes_to_witnesses(config_value: &Self, total: &Self) -> bool { + config_value.0 && total.0 + } +} + +impl fmt::Display for Or { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "Or({})", self.0) + } +} + +/// 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) -> Self { + And(self.0 && other.0) + } +} + +impl fmt::Display for And { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "And({})", self.0) + } +} + /// Result of evaluating a constrained optimization problem. /// /// For optimization problems with constraints (like MaximumIndependentSet), diff --git a/src/unit_tests/types.rs b/src/unit_tests/types.rs index 663fee0db..a010cf886 100644 --- a/src/unit_tests/types.rs +++ b/src/unit_tests/types.rs @@ -1,5 +1,77 @@ use super::*; +#[test] +fn test_max_identity_and_combine() { + assert_eq!(Max::::identity(), Max(None)); + assert_eq!(Max(Some(7)).combine(Max(Some(3))), Max(Some(7))); + assert_eq!(Max(Some(3)).combine(Max(Some(7))), Max(Some(7))); + assert_eq!(Max::::identity().combine(Max(Some(5))), Max(Some(5))); +} + +#[test] +fn test_min_identity_and_combine() { + assert_eq!(Min::::identity(), Min(None)); + assert_eq!(Min(Some(3)).combine(Min(Some(7))), Min(Some(3))); + assert_eq!(Min(Some(7)).combine(Min(Some(3))), Min(Some(3))); + assert_eq!(Min::::identity().combine(Min(Some(5))), Min(Some(5))); +} + +#[test] +fn test_sum_identity_and_combine() { + assert_eq!(Sum::::identity(), Sum(0)); + assert_eq!(Sum(4_u64).combine(Sum(3_u64)), Sum(7)); +} + +#[test] +fn test_or_identity_and_combine() { + assert_eq!(Or::identity(), Or(false)); + assert_eq!(Or(false).combine(Or(true)), Or(true)); + assert_eq!(Or(false).combine(Or(false)), Or(false)); +} + +#[test] +fn test_and_identity_and_combine() { + assert_eq!(And::identity(), And(true)); + assert_eq!(And(true).combine(And(false)), And(false)); + assert_eq!(And(true).combine(And(true)), And(true)); +} + +#[test] +fn test_sum_witness_defaults() { + assert!(!Sum::::supports_witnesses()); + assert!(!Sum::::contributes_to_witnesses(&Sum(3), &Sum(7))); +} + +#[test] +fn test_and_witness_defaults() { + assert!(!And::supports_witnesses()); + assert!(!And::contributes_to_witnesses(&And(true), &And(true))); +} + +#[test] +fn test_max_witness_hooks() { + assert!(Max::::supports_witnesses()); + assert!(Max::contributes_to_witnesses(&Max(Some(7)), &Max(Some(7)))); + assert!(!Max::contributes_to_witnesses(&Max(Some(3)), &Max(Some(7)))); + assert!(!Max::contributes_to_witnesses(&Max(None), &Max(Some(7)))); +} + +#[test] +fn test_min_witness_hooks() { + assert!(Min::::supports_witnesses()); + assert!(Min::contributes_to_witnesses(&Min(Some(3)), &Min(Some(3)))); + assert!(!Min::contributes_to_witnesses(&Min(Some(7)), &Min(Some(3)))); + assert!(!Min::contributes_to_witnesses(&Min(None), &Min(Some(3)))); +} + +#[test] +fn test_or_witness_hooks() { + assert!(Or::supports_witnesses()); + assert!(Or::contributes_to_witnesses(&Or(true), &Or(true))); + assert!(!Or::contributes_to_witnesses(&Or(false), &Or(true))); + assert!(!Or::contributes_to_witnesses(&Or(true), &Or(false))); +} + #[test] fn test_solution_size_valid() { let size: SolutionSize = SolutionSize::Valid(42); From 87d3e1864e93116fb54ada84d7046bb5aa1eb384 Mon Sep 17 00:00:00 2001 From: GiggleLiu Date: Mon, 23 Mar 2026 10:03:47 +0800 Subject: [PATCH 02/18] refactor: unify core solving on aggregate values --- src/models/algebraic/bmf.rs | 4 +- .../algebraic/closest_vector_problem.rs | 4 +- .../consecutive_block_minimization.rs | 2 +- .../algebraic/consecutive_ones_submatrix.rs | 2 +- src/models/algebraic/ilp.rs | 4 +- src/models/algebraic/quadratic_assignment.rs | 4 +- src/models/algebraic/qubo.rs | 4 +- .../algebraic/sparse_matrix_compression.rs | 2 +- src/models/formula/circuit.rs | 2 +- src/models/formula/ksat.rs | 2 +- src/models/formula/nae_satisfiability.rs | 2 +- src/models/formula/qbf.rs | 2 +- src/models/formula/sat.rs | 2 +- src/models/graph/acyclic_partition.rs | 2 +- .../balanced_complete_bipartite_subgraph.rs | 2 +- src/models/graph/biclique_cover.rs | 4 +- .../graph/biconnectivity_augmentation.rs | 2 +- .../graph/bottleneck_traveling_salesman.rs | 4 +- .../bounded_component_spanning_forest.rs | 2 +- .../directed_two_commodity_integral_flow.rs | 2 +- src/models/graph/disjoint_connecting_paths.rs | 2 +- src/models/graph/generalized_hex.rs | 2 +- src/models/graph/graph_partitioning.rs | 4 +- src/models/graph/hamiltonian_circuit.rs | 2 +- src/models/graph/hamiltonian_path.rs | 2 +- src/models/graph/integral_flow_bundles.rs | 2 +- .../graph/integral_flow_homologous_arcs.rs | 2 +- .../graph/integral_flow_with_multipliers.rs | 2 +- src/models/graph/isomorphic_spanning_tree.rs | 2 +- src/models/graph/kclique.rs | 2 +- src/models/graph/kcoloring.rs | 2 +- src/models/graph/kth_best_spanning_tree.rs | 2 +- .../graph/length_bounded_disjoint_paths.rs | 2 +- src/models/graph/longest_circuit.rs | 2 +- src/models/graph/longest_path.rs | 4 +- src/models/graph/max_cut.rs | 4 +- src/models/graph/maximal_is.rs | 4 +- src/models/graph/maximum_clique.rs | 4 +- src/models/graph/maximum_independent_set.rs | 4 +- src/models/graph/maximum_matching.rs | 4 +- src/models/graph/min_max_multicenter.rs | 2 +- .../graph/minimum_cut_into_bounded_sets.rs | 2 +- src/models/graph/minimum_dominating_set.rs | 4 +- .../graph/minimum_dummy_activities_pert.rs | 4 +- src/models/graph/minimum_feedback_arc_set.rs | 4 +- .../graph/minimum_feedback_vertex_set.rs | 4 +- src/models/graph/minimum_multiway_cut.rs | 4 +- src/models/graph/minimum_sum_multicenter.rs | 4 +- src/models/graph/minimum_vertex_cover.rs | 4 +- src/models/graph/mixed_chinese_postman.rs | 2 +- src/models/graph/multiple_choice_branching.rs | 2 +- .../graph/multiple_copy_file_allocation.rs | 2 +- .../graph/optimal_linear_arrangement.rs | 2 +- .../graph/partition_into_paths_of_length_2.rs | 2 +- src/models/graph/partition_into_triangles.rs | 2 +- .../graph/path_constrained_network_flow.rs | 2 +- src/models/graph/rooted_tree_arrangement.rs | 2 +- src/models/graph/rural_postman.rs | 2 +- .../graph/shortest_weight_constrained_path.rs | 2 +- src/models/graph/spin_glass.rs | 4 +- src/models/graph/steiner_tree.rs | 4 +- src/models/graph/steiner_tree_in_graphs.rs | 4 +- .../graph/strong_connectivity_augmentation.rs | 2 +- src/models/graph/subgraph_isomorphism.rs | 2 +- src/models/graph/traveling_salesman.rs | 4 +- .../graph/undirected_flow_lower_bounds.rs | 2 +- .../undirected_two_commodity_integral_flow.rs | 2 +- src/models/misc/additional_key.rs | 2 +- src/models/misc/bin_packing.rs | 4 +- .../misc/boyce_codd_normal_form_violation.rs | 2 +- src/models/misc/capacity_assignment.rs | 2 +- src/models/misc/conjunctive_boolean_query.rs | 2 +- .../misc/conjunctive_query_foldability.rs | 2 +- ...onsistency_of_database_frequency_tables.rs | 2 +- src/models/misc/ensemble_computation.rs | 2 +- src/models/misc/expected_retrieval_cost.rs | 2 +- src/models/misc/factoring.rs | 4 +- src/models/misc/flow_shop_scheduling.rs | 2 +- src/models/misc/knapsack.rs | 4 +- src/models/misc/longest_common_subsequence.rs | 2 +- .../misc/minimum_tardiness_sequencing.rs | 4 +- src/models/misc/multiprocessor_scheduling.rs | 2 +- src/models/misc/paintshop.rs | 4 +- src/models/misc/partially_ordered_knapsack.rs | 4 +- src/models/misc/partition.rs | 2 +- .../misc/precedence_constrained_scheduling.rs | 2 +- .../misc/rectilinear_picture_compression.rs | 2 +- .../misc/resource_constrained_scheduling.rs | 2 +- .../scheduling_with_individual_deadlines.rs | 2 +- ...ing_to_minimize_maximum_cumulative_cost.rs | 2 +- ...ng_to_minimize_weighted_completion_time.rs | 4 +- ...quencing_to_minimize_weighted_tardiness.rs | 2 +- ...encing_with_release_times_and_deadlines.rs | 2 +- .../misc/sequencing_within_intervals.rs | 2 +- .../misc/shortest_common_supersequence.rs | 2 +- src/models/misc/stacker_crane.rs | 2 +- src/models/misc/staff_scheduling.rs | 2 +- .../misc/string_to_string_correction.rs | 2 +- src/models/misc/subset_sum.rs | 2 +- src/models/misc/sum_of_squares_partition.rs | 2 +- src/models/misc/timetable_design.rs | 2 +- src/models/set/comparative_containment.rs | 2 +- src/models/set/consecutive_sets.rs | 2 +- src/models/set/exact_cover_by_3_sets.rs | 2 +- src/models/set/maximum_set_packing.rs | 4 +- src/models/set/minimum_cardinality_key.rs | 2 +- src/models/set/minimum_hitting_set.rs | 4 +- src/models/set/minimum_set_covering.rs | 4 +- src/models/set/prime_attribute_name.rs | 2 +- .../set/rooted_tree_storage_assignment.rs | 2 +- src/models/set/set_basis.rs | 2 +- .../set/two_dimensional_consecutive_sets.rs | 2 +- src/registry/dyn_problem.rs | 2 +- src/rules/test_helpers.rs | 26 +-- src/solvers/brute_force.rs | 100 ++++++-- src/solvers/mod.rs | 17 +- src/traits.rs | 25 +- src/unit_tests/rules/traits.rs | 4 +- src/unit_tests/solvers/brute_force.rs | 215 +++++++----------- src/unit_tests/traits.rs | 134 +++++------ 120 files changed, 411 insertions(+), 406 deletions(-) diff --git a/src/models/algebraic/bmf.rs b/src/models/algebraic/bmf.rs index 74d011a06..8f3516307 100644 --- a/src/models/algebraic/bmf.rs +++ b/src/models/algebraic/bmf.rs @@ -205,7 +205,7 @@ pub(crate) fn matrix_hamming_distance(a: &[Vec], b: &[Vec]) -> usize impl Problem for BMF { const NAME: &'static str = "BMF"; - type Metric = SolutionSize; + type Value = SolutionSize; fn dims(&self) -> Vec { // B: m*k + C: k*n binary variables @@ -224,7 +224,7 @@ impl Problem for BMF { } impl OptimizationProblem for BMF { - type Value = i32; + type Objective = i32; fn direction(&self) -> Direction { Direction::Minimize diff --git a/src/models/algebraic/closest_vector_problem.rs b/src/models/algebraic/closest_vector_problem.rs index d306299f5..46387bc8e 100644 --- a/src/models/algebraic/closest_vector_problem.rs +++ b/src/models/algebraic/closest_vector_problem.rs @@ -240,7 +240,7 @@ where + 'static, { const NAME: &'static str = "ClosestVectorProblem"; - type Metric = SolutionSize; + type Value = SolutionSize; fn dims(&self) -> Vec { self.bounds @@ -284,7 +284,7 @@ where + std::fmt::Debug + 'static, { - type Value = f64; + type Objective = f64; fn direction(&self) -> Direction { Direction::Minimize diff --git a/src/models/algebraic/consecutive_block_minimization.rs b/src/models/algebraic/consecutive_block_minimization.rs index 42e299f05..b95daa740 100644 --- a/src/models/algebraic/consecutive_block_minimization.rs +++ b/src/models/algebraic/consecutive_block_minimization.rs @@ -156,7 +156,7 @@ impl ConsecutiveBlockMinimization { impl Problem for ConsecutiveBlockMinimization { const NAME: &'static str = "ConsecutiveBlockMinimization"; - type Metric = bool; + type Value = bool; fn dims(&self) -> Vec { vec![self.num_cols; self.num_cols] diff --git a/src/models/algebraic/consecutive_ones_submatrix.rs b/src/models/algebraic/consecutive_ones_submatrix.rs index 03b0feb91..615a17fc3 100644 --- a/src/models/algebraic/consecutive_ones_submatrix.rs +++ b/src/models/algebraic/consecutive_ones_submatrix.rs @@ -173,7 +173,7 @@ impl ConsecutiveOnesSubmatrix { impl Problem for ConsecutiveOnesSubmatrix { const NAME: &'static str = "ConsecutiveOnesSubmatrix"; - type Metric = bool; + type Value = bool; fn variant() -> Vec<(&'static str, &'static str)> { crate::variant_params![] diff --git a/src/models/algebraic/ilp.rs b/src/models/algebraic/ilp.rs index 5dab49674..876f44bb7 100644 --- a/src/models/algebraic/ilp.rs +++ b/src/models/algebraic/ilp.rs @@ -244,7 +244,7 @@ impl ILP { impl Problem for ILP { const NAME: &'static str = "ILP"; - type Metric = SolutionSize; + type Value = SolutionSize; fn dims(&self) -> Vec { vec![V::DIMS_PER_VAR; self.num_vars] @@ -264,7 +264,7 @@ impl Problem for ILP { } impl OptimizationProblem for ILP { - type Value = f64; + type Objective = f64; fn direction(&self) -> Direction { match self.sense { diff --git a/src/models/algebraic/quadratic_assignment.rs b/src/models/algebraic/quadratic_assignment.rs index 8b1b900de..645082fb8 100644 --- a/src/models/algebraic/quadratic_assignment.rs +++ b/src/models/algebraic/quadratic_assignment.rs @@ -115,7 +115,7 @@ impl QuadraticAssignment { impl Problem for QuadraticAssignment { const NAME: &'static str = "QuadraticAssignment"; - type Metric = SolutionSize; + type Value = SolutionSize; fn dims(&self) -> Vec { vec![self.num_locations(); self.num_facilities()] @@ -165,7 +165,7 @@ impl Problem for QuadraticAssignment { } impl OptimizationProblem for QuadraticAssignment { - type Value = i64; + type Objective = i64; fn direction(&self) -> Direction { Direction::Minimize diff --git a/src/models/algebraic/qubo.rs b/src/models/algebraic/qubo.rs index f1b76091a..97afac296 100644 --- a/src/models/algebraic/qubo.rs +++ b/src/models/algebraic/qubo.rs @@ -158,7 +158,7 @@ where + std::ops::Mul, { const NAME: &'static str = "QUBO"; - type Metric = SolutionSize; + type Value = SolutionSize; fn dims(&self) -> Vec { vec![2; self.num_vars] @@ -184,7 +184,7 @@ where + std::ops::AddAssign + std::ops::Mul, { - type Value = W::Sum; + type Objective = W::Sum; fn direction(&self) -> Direction { Direction::Minimize diff --git a/src/models/algebraic/sparse_matrix_compression.rs b/src/models/algebraic/sparse_matrix_compression.rs index a41dfcc10..b9473274e 100644 --- a/src/models/algebraic/sparse_matrix_compression.rs +++ b/src/models/algebraic/sparse_matrix_compression.rs @@ -119,7 +119,7 @@ impl SparseMatrixCompression { impl Problem for SparseMatrixCompression { const NAME: &'static str = "SparseMatrixCompression"; - type Metric = bool; + type Value = bool; fn dims(&self) -> Vec { vec![self.bound_k; self.num_rows()] diff --git a/src/models/formula/circuit.rs b/src/models/formula/circuit.rs index f252c18b0..a47e3ba68 100644 --- a/src/models/formula/circuit.rs +++ b/src/models/formula/circuit.rs @@ -289,7 +289,7 @@ pub(crate) fn is_circuit_satisfying( impl Problem for CircuitSAT { const NAME: &'static str = "CircuitSAT"; - type Metric = bool; + type Value = bool; fn dims(&self) -> Vec { vec![2; self.variables.len()] diff --git a/src/models/formula/ksat.rs b/src/models/formula/ksat.rs index 0b9681a7c..c90393b3f 100644 --- a/src/models/formula/ksat.rs +++ b/src/models/formula/ksat.rs @@ -168,7 +168,7 @@ impl KSatisfiability { impl Problem for KSatisfiability { const NAME: &'static str = "KSatisfiability"; - type Metric = bool; + type Value = bool; fn dims(&self) -> Vec { vec![2; self.num_vars] diff --git a/src/models/formula/nae_satisfiability.rs b/src/models/formula/nae_satisfiability.rs index 54e896a4a..a8a0d757c 100644 --- a/src/models/formula/nae_satisfiability.rs +++ b/src/models/formula/nae_satisfiability.rs @@ -135,7 +135,7 @@ impl NAESatisfiability { impl Problem for NAESatisfiability { const NAME: &'static str = "NAESatisfiability"; - type Metric = bool; + type Value = bool; fn dims(&self) -> Vec { vec![2; self.num_vars] diff --git a/src/models/formula/qbf.rs b/src/models/formula/qbf.rs index 830ddc167..8e35180fa 100644 --- a/src/models/formula/qbf.rs +++ b/src/models/formula/qbf.rs @@ -157,7 +157,7 @@ impl QuantifiedBooleanFormulas { impl Problem for QuantifiedBooleanFormulas { const NAME: &'static str = "QuantifiedBooleanFormulas"; - type Metric = bool; + type Value = bool; fn dims(&self) -> Vec { vec![] diff --git a/src/models/formula/sat.rs b/src/models/formula/sat.rs index 17272080c..c85319738 100644 --- a/src/models/formula/sat.rs +++ b/src/models/formula/sat.rs @@ -180,7 +180,7 @@ impl Satisfiability { impl Problem for Satisfiability { const NAME: &'static str = "Satisfiability"; - type Metric = bool; + type Value = bool; fn dims(&self) -> Vec { vec![2; self.num_vars] diff --git a/src/models/graph/acyclic_partition.rs b/src/models/graph/acyclic_partition.rs index c81725854..d20fa16ee 100644 --- a/src/models/graph/acyclic_partition.rs +++ b/src/models/graph/acyclic_partition.rs @@ -156,7 +156,7 @@ where W: WeightElement + crate::variant::VariantParam, { const NAME: &'static str = "AcyclicPartition"; - type Metric = bool; + type Value = bool; fn variant() -> Vec<(&'static str, &'static str)> { crate::variant_params![W] diff --git a/src/models/graph/balanced_complete_bipartite_subgraph.rs b/src/models/graph/balanced_complete_bipartite_subgraph.rs index dbb46d4e5..3d39f188b 100644 --- a/src/models/graph/balanced_complete_bipartite_subgraph.rs +++ b/src/models/graph/balanced_complete_bipartite_subgraph.rs @@ -102,7 +102,7 @@ impl BalancedCompleteBipartiteSubgraph { impl Problem for BalancedCompleteBipartiteSubgraph { const NAME: &'static str = "BalancedCompleteBipartiteSubgraph"; - type Metric = bool; + type Value = bool; fn dims(&self) -> Vec { vec![2; self.num_vertices()] diff --git a/src/models/graph/biclique_cover.rs b/src/models/graph/biclique_cover.rs index ca1bf2e4a..42c9a465c 100644 --- a/src/models/graph/biclique_cover.rs +++ b/src/models/graph/biclique_cover.rs @@ -219,7 +219,7 @@ pub(crate) fn is_biclique_cover( impl Problem for BicliqueCover { const NAME: &'static str = "BicliqueCover"; - type Metric = SolutionSize; + type Value = SolutionSize; fn dims(&self) -> Vec { // Each vertex has k binary variables (one per biclique) @@ -239,7 +239,7 @@ impl Problem for BicliqueCover { } impl OptimizationProblem for BicliqueCover { - type Value = i32; + type Objective = i32; fn direction(&self) -> Direction { Direction::Minimize diff --git a/src/models/graph/biconnectivity_augmentation.rs b/src/models/graph/biconnectivity_augmentation.rs index b4f2bbc59..a07b8302b 100644 --- a/src/models/graph/biconnectivity_augmentation.rs +++ b/src/models/graph/biconnectivity_augmentation.rs @@ -165,7 +165,7 @@ where W: WeightElement + crate::variant::VariantParam, { const NAME: &'static str = "BiconnectivityAugmentation"; - type Metric = bool; + type Value = bool; fn variant() -> Vec<(&'static str, &'static str)> { crate::variant_params![G, W] diff --git a/src/models/graph/bottleneck_traveling_salesman.rs b/src/models/graph/bottleneck_traveling_salesman.rs index e86b7c95e..203642450 100644 --- a/src/models/graph/bottleneck_traveling_salesman.rs +++ b/src/models/graph/bottleneck_traveling_salesman.rs @@ -98,7 +98,7 @@ impl BottleneckTravelingSalesman { impl Problem for BottleneckTravelingSalesman { const NAME: &'static str = "BottleneckTravelingSalesman"; - type Metric = SolutionSize; + type Value = SolutionSize; fn variant() -> Vec<(&'static str, &'static str)> { crate::variant_params![] @@ -130,7 +130,7 @@ impl Problem for BottleneckTravelingSalesman { } impl OptimizationProblem for BottleneckTravelingSalesman { - type Value = i32; + type Objective = i32; fn direction(&self) -> Direction { Direction::Minimize diff --git a/src/models/graph/bounded_component_spanning_forest.rs b/src/models/graph/bounded_component_spanning_forest.rs index 053edbf57..95b95d8db 100644 --- a/src/models/graph/bounded_component_spanning_forest.rs +++ b/src/models/graph/bounded_component_spanning_forest.rs @@ -185,7 +185,7 @@ where W: WeightElement + crate::variant::VariantParam, { const NAME: &'static str = "BoundedComponentSpanningForest"; - type Metric = bool; + type Value = bool; fn variant() -> Vec<(&'static str, &'static str)> { crate::variant_params![G, W] diff --git a/src/models/graph/directed_two_commodity_integral_flow.rs b/src/models/graph/directed_two_commodity_integral_flow.rs index 20970c62b..8d9bfc293 100644 --- a/src/models/graph/directed_two_commodity_integral_flow.rs +++ b/src/models/graph/directed_two_commodity_integral_flow.rs @@ -244,7 +244,7 @@ impl DirectedTwoCommodityIntegralFlow { impl Problem for DirectedTwoCommodityIntegralFlow { const NAME: &'static str = "DirectedTwoCommodityIntegralFlow"; - type Metric = bool; + type Value = bool; fn dims(&self) -> Vec { self.capacities diff --git a/src/models/graph/disjoint_connecting_paths.rs b/src/models/graph/disjoint_connecting_paths.rs index 2f6bcebd6..1ff212f2c 100644 --- a/src/models/graph/disjoint_connecting_paths.rs +++ b/src/models/graph/disjoint_connecting_paths.rs @@ -117,7 +117,7 @@ where G: Graph + VariantParam, { const NAME: &'static str = "DisjointConnectingPaths"; - type Metric = bool; + type Value = bool; fn variant() -> Vec<(&'static str, &'static str)> { crate::variant_params![G] diff --git a/src/models/graph/generalized_hex.rs b/src/models/graph/generalized_hex.rs index 29b6c870c..67f9389ac 100644 --- a/src/models/graph/generalized_hex.rs +++ b/src/models/graph/generalized_hex.rs @@ -239,7 +239,7 @@ where G: Graph + VariantParam, { const NAME: &'static str = "GeneralizedHex"; - type Metric = bool; + type Value = bool; fn variant() -> Vec<(&'static str, &'static str)> { crate::variant_params![G] diff --git a/src/models/graph/graph_partitioning.rs b/src/models/graph/graph_partitioning.rs index 1ace66b80..6930fc775 100644 --- a/src/models/graph/graph_partitioning.rs +++ b/src/models/graph/graph_partitioning.rs @@ -92,7 +92,7 @@ where G: Graph + crate::variant::VariantParam, { const NAME: &'static str = "GraphPartitioning"; - type Metric = SolutionSize; + type Value = SolutionSize; fn variant() -> Vec<(&'static str, &'static str)> { crate::variant_params![G] @@ -131,7 +131,7 @@ impl OptimizationProblem for GraphPartitioning where G: Graph + crate::variant::VariantParam, { - type Value = i32; + type Objective = i32; fn direction(&self) -> Direction { Direction::Minimize diff --git a/src/models/graph/hamiltonian_circuit.rs b/src/models/graph/hamiltonian_circuit.rs index 9eed25e78..5124e5fde 100644 --- a/src/models/graph/hamiltonian_circuit.rs +++ b/src/models/graph/hamiltonian_circuit.rs @@ -92,7 +92,7 @@ where G: Graph + VariantParam, { const NAME: &'static str = "HamiltonianCircuit"; - type Metric = bool; + type Value = bool; fn variant() -> Vec<(&'static str, &'static str)> { crate::variant_params![G] diff --git a/src/models/graph/hamiltonian_path.rs b/src/models/graph/hamiltonian_path.rs index 0ab9ba528..94ca1248e 100644 --- a/src/models/graph/hamiltonian_path.rs +++ b/src/models/graph/hamiltonian_path.rs @@ -99,7 +99,7 @@ where G: Graph + VariantParam, { const NAME: &'static str = "HamiltonianPath"; - type Metric = bool; + type Value = bool; fn variant() -> Vec<(&'static str, &'static str)> { crate::variant_params![G] diff --git a/src/models/graph/integral_flow_bundles.rs b/src/models/graph/integral_flow_bundles.rs index 383c2171d..493839e96 100644 --- a/src/models/graph/integral_flow_bundles.rs +++ b/src/models/graph/integral_flow_bundles.rs @@ -202,7 +202,7 @@ impl IntegralFlowBundles { impl Problem for IntegralFlowBundles { const NAME: &'static str = "IntegralFlowBundles"; - type Metric = bool; + type Value = bool; fn dims(&self) -> Vec { self.arc_upper_bounds() diff --git a/src/models/graph/integral_flow_homologous_arcs.rs b/src/models/graph/integral_flow_homologous_arcs.rs index 51ae2b0cd..bce5b8b81 100644 --- a/src/models/graph/integral_flow_homologous_arcs.rs +++ b/src/models/graph/integral_flow_homologous_arcs.rs @@ -152,7 +152,7 @@ impl IntegralFlowHomologousArcs { impl Problem for IntegralFlowHomologousArcs { const NAME: &'static str = "IntegralFlowHomologousArcs"; - type Metric = bool; + type Value = bool; fn variant() -> Vec<(&'static str, &'static str)> { crate::variant_params![] diff --git a/src/models/graph/integral_flow_with_multipliers.rs b/src/models/graph/integral_flow_with_multipliers.rs index 4f25d731e..3412e7ee2 100644 --- a/src/models/graph/integral_flow_with_multipliers.rs +++ b/src/models/graph/integral_flow_with_multipliers.rs @@ -195,7 +195,7 @@ impl IntegralFlowWithMultipliers { impl Problem for IntegralFlowWithMultipliers { const NAME: &'static str = "IntegralFlowWithMultipliers"; - type Metric = bool; + type Value = bool; fn dims(&self) -> Vec { self.capacities diff --git a/src/models/graph/isomorphic_spanning_tree.rs b/src/models/graph/isomorphic_spanning_tree.rs index 8e3a55075..f200edc83 100644 --- a/src/models/graph/isomorphic_spanning_tree.rs +++ b/src/models/graph/isomorphic_spanning_tree.rs @@ -125,7 +125,7 @@ impl IsomorphicSpanningTree { impl Problem for IsomorphicSpanningTree { const NAME: &'static str = "IsomorphicSpanningTree"; - type Metric = bool; + type Value = bool; fn dims(&self) -> Vec { let n = self.graph.num_vertices(); diff --git a/src/models/graph/kclique.rs b/src/models/graph/kclique.rs index d16e2911c..6cb19fac9 100644 --- a/src/models/graph/kclique.rs +++ b/src/models/graph/kclique.rs @@ -73,7 +73,7 @@ where G: Graph + crate::variant::VariantParam, { const NAME: &'static str = "KClique"; - type Metric = bool; + type Value = bool; fn variant() -> Vec<(&'static str, &'static str)> { crate::variant_params![G] diff --git a/src/models/graph/kcoloring.rs b/src/models/graph/kcoloring.rs index 195cbbff0..382d75c88 100644 --- a/src/models/graph/kcoloring.rs +++ b/src/models/graph/kcoloring.rs @@ -145,7 +145,7 @@ where G: Graph + VariantParam, { const NAME: &'static str = "KColoring"; - type Metric = bool; + type Value = bool; fn variant() -> Vec<(&'static str, &'static str)> { crate::variant_params![K, G] diff --git a/src/models/graph/kth_best_spanning_tree.rs b/src/models/graph/kth_best_spanning_tree.rs index d65e86441..db61c1713 100644 --- a/src/models/graph/kth_best_spanning_tree.rs +++ b/src/models/graph/kth_best_spanning_tree.rs @@ -207,7 +207,7 @@ where W: WeightElement + crate::variant::VariantParam, { const NAME: &'static str = "KthBestSpanningTree"; - type Metric = bool; + type Value = bool; fn variant() -> Vec<(&'static str, &'static str)> { crate::variant_params![W] diff --git a/src/models/graph/length_bounded_disjoint_paths.rs b/src/models/graph/length_bounded_disjoint_paths.rs index 6540f11c6..c82ac0093 100644 --- a/src/models/graph/length_bounded_disjoint_paths.rs +++ b/src/models/graph/length_bounded_disjoint_paths.rs @@ -135,7 +135,7 @@ where G: Graph + VariantParam, { const NAME: &'static str = "LengthBoundedDisjointPaths"; - type Metric = bool; + type Value = bool; fn variant() -> Vec<(&'static str, &'static str)> { crate::variant_params![G] diff --git a/src/models/graph/longest_circuit.rs b/src/models/graph/longest_circuit.rs index 189645745..69593fafc 100644 --- a/src/models/graph/longest_circuit.rs +++ b/src/models/graph/longest_circuit.rs @@ -161,7 +161,7 @@ where W: WeightElement + crate::variant::VariantParam, { const NAME: &'static str = "LongestCircuit"; - type Metric = bool; + type Value = bool; fn variant() -> Vec<(&'static str, &'static str)> { crate::variant_params![G, W] diff --git a/src/models/graph/longest_path.rs b/src/models/graph/longest_path.rs index 682de7ed8..524b43669 100644 --- a/src/models/graph/longest_path.rs +++ b/src/models/graph/longest_path.rs @@ -150,7 +150,7 @@ where W: WeightElement + crate::variant::VariantParam, { const NAME: &'static str = "LongestPath"; - type Metric = SolutionSize; + type Value = SolutionSize; fn variant() -> Vec<(&'static str, &'static str)> { crate::variant_params![G, W] @@ -180,7 +180,7 @@ where G: Graph + crate::variant::VariantParam, W: WeightElement + crate::variant::VariantParam, { - type Value = W::Sum; + type Objective = W::Sum; fn direction(&self) -> Direction { Direction::Maximize diff --git a/src/models/graph/max_cut.rs b/src/models/graph/max_cut.rs index e3e281583..0542d15fb 100644 --- a/src/models/graph/max_cut.rs +++ b/src/models/graph/max_cut.rs @@ -171,7 +171,7 @@ where W: WeightElement + crate::variant::VariantParam, { const NAME: &'static str = "MaxCut"; - type Metric = SolutionSize; + type Value = SolutionSize; fn variant() -> Vec<(&'static str, &'static str)> { crate::variant_params![G, W] @@ -193,7 +193,7 @@ where G: Graph + crate::variant::VariantParam, W: WeightElement + crate::variant::VariantParam, { - type Value = W::Sum; + type Objective = W::Sum; fn direction(&self) -> Direction { Direction::Maximize diff --git a/src/models/graph/maximal_is.rs b/src/models/graph/maximal_is.rs index ac9f83add..05f9df229 100644 --- a/src/models/graph/maximal_is.rs +++ b/src/models/graph/maximal_is.rs @@ -153,7 +153,7 @@ where W: WeightElement + crate::variant::VariantParam, { const NAME: &'static str = "MaximalIS"; - type Metric = SolutionSize; + type Value = SolutionSize; fn variant() -> Vec<(&'static str, &'static str)> { crate::variant_params![G, W] @@ -182,7 +182,7 @@ where G: Graph + crate::variant::VariantParam, W: WeightElement + crate::variant::VariantParam, { - type Value = W::Sum; + type Objective = W::Sum; fn direction(&self) -> Direction { Direction::Maximize diff --git a/src/models/graph/maximum_clique.rs b/src/models/graph/maximum_clique.rs index 8cf23de0c..5e2d35909 100644 --- a/src/models/graph/maximum_clique.rs +++ b/src/models/graph/maximum_clique.rs @@ -119,7 +119,7 @@ where W: WeightElement + crate::variant::VariantParam, { const NAME: &'static str = "MaximumClique"; - type Metric = SolutionSize; + type Value = SolutionSize; fn variant() -> Vec<(&'static str, &'static str)> { crate::variant_params![G, W] @@ -148,7 +148,7 @@ where G: Graph + crate::variant::VariantParam, W: WeightElement + crate::variant::VariantParam, { - type Value = W::Sum; + type Objective = W::Sum; fn direction(&self) -> Direction { Direction::Maximize diff --git a/src/models/graph/maximum_independent_set.rs b/src/models/graph/maximum_independent_set.rs index 5fec92078..90afc72d3 100644 --- a/src/models/graph/maximum_independent_set.rs +++ b/src/models/graph/maximum_independent_set.rs @@ -119,7 +119,7 @@ where W: WeightElement + crate::variant::VariantParam, { const NAME: &'static str = "MaximumIndependentSet"; - type Metric = SolutionSize; + type Value = SolutionSize; fn variant() -> Vec<(&'static str, &'static str)> { crate::variant_params![G, W] @@ -148,7 +148,7 @@ where G: Graph + crate::variant::VariantParam, W: WeightElement + crate::variant::VariantParam, { - type Value = W::Sum; + type Objective = W::Sum; fn direction(&self) -> Direction { Direction::Maximize diff --git a/src/models/graph/maximum_matching.rs b/src/models/graph/maximum_matching.rs index 4b74cc540..aafdad305 100644 --- a/src/models/graph/maximum_matching.rs +++ b/src/models/graph/maximum_matching.rs @@ -187,7 +187,7 @@ where W: WeightElement + crate::variant::VariantParam, { const NAME: &'static str = "MaximumMatching"; - type Metric = SolutionSize; + type Value = SolutionSize; fn variant() -> Vec<(&'static str, &'static str)> { crate::variant_params![G, W] @@ -218,7 +218,7 @@ where G: Graph + crate::variant::VariantParam, W: WeightElement + crate::variant::VariantParam, { - type Value = W::Sum; + type Objective = W::Sum; fn direction(&self) -> Direction { Direction::Maximize diff --git a/src/models/graph/min_max_multicenter.rs b/src/models/graph/min_max_multicenter.rs index 05f3c26d8..6d2806e1d 100644 --- a/src/models/graph/min_max_multicenter.rs +++ b/src/models/graph/min_max_multicenter.rs @@ -246,7 +246,7 @@ where W: WeightElement + crate::variant::VariantParam, { const NAME: &'static str = "MinMaxMulticenter"; - type Metric = bool; + type Value = bool; fn variant() -> Vec<(&'static str, &'static str)> { crate::variant_params![G, W] diff --git a/src/models/graph/minimum_cut_into_bounded_sets.rs b/src/models/graph/minimum_cut_into_bounded_sets.rs index 6523ed5aa..3327e3ba3 100644 --- a/src/models/graph/minimum_cut_into_bounded_sets.rs +++ b/src/models/graph/minimum_cut_into_bounded_sets.rs @@ -164,7 +164,7 @@ where W: WeightElement + crate::variant::VariantParam, { const NAME: &'static str = "MinimumCutIntoBoundedSets"; - type Metric = bool; + type Value = bool; fn variant() -> Vec<(&'static str, &'static str)> { crate::variant_params![G, W] diff --git a/src/models/graph/minimum_dominating_set.rs b/src/models/graph/minimum_dominating_set.rs index fd7c4aeb3..d09df3a20 100644 --- a/src/models/graph/minimum_dominating_set.rs +++ b/src/models/graph/minimum_dominating_set.rs @@ -139,7 +139,7 @@ where W: WeightElement + crate::variant::VariantParam, { const NAME: &'static str = "MinimumDominatingSet"; - type Metric = SolutionSize; + type Value = SolutionSize; fn variant() -> Vec<(&'static str, &'static str)> { crate::variant_params![G, W] @@ -168,7 +168,7 @@ where G: Graph + crate::variant::VariantParam, W: WeightElement + crate::variant::VariantParam, { - type Value = W::Sum; + type Objective = W::Sum; fn direction(&self) -> Direction { Direction::Minimize diff --git a/src/models/graph/minimum_dummy_activities_pert.rs b/src/models/graph/minimum_dummy_activities_pert.rs index 1dff45eac..0b350a0c3 100644 --- a/src/models/graph/minimum_dummy_activities_pert.rs +++ b/src/models/graph/minimum_dummy_activities_pert.rs @@ -164,7 +164,7 @@ impl MinimumDummyActivitiesPert { impl Problem for MinimumDummyActivitiesPert { const NAME: &'static str = "MinimumDummyActivitiesPert"; - type Metric = SolutionSize; + type Value = SolutionSize; fn variant() -> Vec<(&'static str, &'static str)> { crate::variant_params![] @@ -201,7 +201,7 @@ impl Problem for MinimumDummyActivitiesPert { } impl OptimizationProblem for MinimumDummyActivitiesPert { - type Value = i32; + type Objective = i32; fn direction(&self) -> Direction { Direction::Minimize diff --git a/src/models/graph/minimum_feedback_arc_set.rs b/src/models/graph/minimum_feedback_arc_set.rs index ff2263065..519731a4b 100644 --- a/src/models/graph/minimum_feedback_arc_set.rs +++ b/src/models/graph/minimum_feedback_arc_set.rs @@ -126,7 +126,7 @@ where W: WeightElement + crate::variant::VariantParam, { const NAME: &'static str = "MinimumFeedbackArcSet"; - type Metric = SolutionSize; + type Value = SolutionSize; fn variant() -> Vec<(&'static str, &'static str)> { crate::variant_params![W] @@ -154,7 +154,7 @@ impl OptimizationProblem for MinimumFeedbackArcSet where W: WeightElement + crate::variant::VariantParam, { - type Value = W::Sum; + type Objective = W::Sum; fn direction(&self) -> Direction { Direction::Minimize diff --git a/src/models/graph/minimum_feedback_vertex_set.rs b/src/models/graph/minimum_feedback_vertex_set.rs index 02251b45b..242746434 100644 --- a/src/models/graph/minimum_feedback_vertex_set.rs +++ b/src/models/graph/minimum_feedback_vertex_set.rs @@ -122,7 +122,7 @@ where W: WeightElement + crate::variant::VariantParam, { const NAME: &'static str = "MinimumFeedbackVertexSet"; - type Metric = SolutionSize; + type Value = SolutionSize; fn variant() -> Vec<(&'static str, &'static str)> { crate::variant_params![W] @@ -156,7 +156,7 @@ impl OptimizationProblem for MinimumFeedbackVertexSet where W: WeightElement + crate::variant::VariantParam, { - type Value = W::Sum; + type Objective = W::Sum; fn direction(&self) -> Direction { Direction::Minimize diff --git a/src/models/graph/minimum_multiway_cut.rs b/src/models/graph/minimum_multiway_cut.rs index 43160a340..6d1d3482d 100644 --- a/src/models/graph/minimum_multiway_cut.rs +++ b/src/models/graph/minimum_multiway_cut.rs @@ -161,7 +161,7 @@ where W: WeightElement + crate::variant::VariantParam, { const NAME: &'static str = "MinimumMultiwayCut"; - type Metric = SolutionSize; + type Value = SolutionSize; fn variant() -> Vec<(&'static str, &'static str)> { crate::variant_params![G, W] @@ -192,7 +192,7 @@ where G: Graph + crate::variant::VariantParam, W: WeightElement + crate::variant::VariantParam, { - type Value = W::Sum; + type Objective = W::Sum; fn direction(&self) -> Direction { Direction::Minimize diff --git a/src/models/graph/minimum_sum_multicenter.rs b/src/models/graph/minimum_sum_multicenter.rs index d8ece2554..a1fe78022 100644 --- a/src/models/graph/minimum_sum_multicenter.rs +++ b/src/models/graph/minimum_sum_multicenter.rs @@ -214,7 +214,7 @@ where W: WeightElement + crate::variant::VariantParam, { const NAME: &'static str = "MinimumSumMulticenter"; - type Metric = SolutionSize; + type Value = SolutionSize; fn variant() -> Vec<(&'static str, &'static str)> { crate::variant_params![G, W] @@ -252,7 +252,7 @@ where G: Graph + crate::variant::VariantParam, W: WeightElement + crate::variant::VariantParam, { - type Value = W::Sum; + type Objective = W::Sum; fn direction(&self) -> Direction { Direction::Minimize diff --git a/src/models/graph/minimum_vertex_cover.rs b/src/models/graph/minimum_vertex_cover.rs index 4f59f7804..bcb2e1883 100644 --- a/src/models/graph/minimum_vertex_cover.rs +++ b/src/models/graph/minimum_vertex_cover.rs @@ -114,7 +114,7 @@ where W: WeightElement + crate::variant::VariantParam, { const NAME: &'static str = "MinimumVertexCover"; - type Metric = SolutionSize; + type Value = SolutionSize; fn variant() -> Vec<(&'static str, &'static str)> { crate::variant_params![G, W] @@ -143,7 +143,7 @@ where G: Graph + crate::variant::VariantParam, W: WeightElement + crate::variant::VariantParam, { - type Value = W::Sum; + type Objective = W::Sum; fn direction(&self) -> Direction { Direction::Minimize diff --git a/src/models/graph/mixed_chinese_postman.rs b/src/models/graph/mixed_chinese_postman.rs index d79188ba4..ce2709718 100644 --- a/src/models/graph/mixed_chinese_postman.rs +++ b/src/models/graph/mixed_chinese_postman.rs @@ -218,7 +218,7 @@ where W: WeightElement + crate::variant::VariantParam, { const NAME: &'static str = "MixedChinesePostman"; - type Metric = bool; + type Value = bool; fn variant() -> Vec<(&'static str, &'static str)> { crate::variant_params![W] diff --git a/src/models/graph/multiple_choice_branching.rs b/src/models/graph/multiple_choice_branching.rs index f7252ee3d..d8748ac5d 100644 --- a/src/models/graph/multiple_choice_branching.rs +++ b/src/models/graph/multiple_choice_branching.rs @@ -176,7 +176,7 @@ where W: WeightElement + crate::variant::VariantParam, { const NAME: &'static str = "MultipleChoiceBranching"; - type Metric = bool; + type Value = bool; fn variant() -> Vec<(&'static str, &'static str)> { crate::variant_params![W] diff --git a/src/models/graph/multiple_copy_file_allocation.rs b/src/models/graph/multiple_copy_file_allocation.rs index 702ff1572..5e9abce25 100644 --- a/src/models/graph/multiple_copy_file_allocation.rs +++ b/src/models/graph/multiple_copy_file_allocation.rs @@ -179,7 +179,7 @@ impl MultipleCopyFileAllocation { impl Problem for MultipleCopyFileAllocation { const NAME: &'static str = "MultipleCopyFileAllocation"; - type Metric = bool; + type Value = bool; fn variant() -> Vec<(&'static str, &'static str)> { crate::variant_params![] diff --git a/src/models/graph/optimal_linear_arrangement.rs b/src/models/graph/optimal_linear_arrangement.rs index 830d42efe..df85dbfff 100644 --- a/src/models/graph/optimal_linear_arrangement.rs +++ b/src/models/graph/optimal_linear_arrangement.rs @@ -145,7 +145,7 @@ where G: Graph + crate::variant::VariantParam, { const NAME: &'static str = "OptimalLinearArrangement"; - type Metric = bool; + type Value = bool; fn variant() -> Vec<(&'static str, &'static str)> { crate::variant_params![G] 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 b5ae20b51..a7bc31e7e 100644 --- a/src/models/graph/partition_into_paths_of_length_2.rs +++ b/src/models/graph/partition_into_paths_of_length_2.rs @@ -148,7 +148,7 @@ where G: Graph + VariantParam, { const NAME: &'static str = "PartitionIntoPathsOfLength2"; - type Metric = bool; + type Value = bool; fn variant() -> Vec<(&'static str, &'static str)> { crate::variant_params![G] diff --git a/src/models/graph/partition_into_triangles.rs b/src/models/graph/partition_into_triangles.rs index 717757212..89972a750 100644 --- a/src/models/graph/partition_into_triangles.rs +++ b/src/models/graph/partition_into_triangles.rs @@ -91,7 +91,7 @@ where G: Graph + VariantParam, { const NAME: &'static str = "PartitionIntoTriangles"; - type Metric = bool; + type Value = bool; fn variant() -> Vec<(&'static str, &'static str)> { crate::variant_params![G] diff --git a/src/models/graph/path_constrained_network_flow.rs b/src/models/graph/path_constrained_network_flow.rs index d034eae3e..d193a6320 100644 --- a/src/models/graph/path_constrained_network_flow.rs +++ b/src/models/graph/path_constrained_network_flow.rs @@ -216,7 +216,7 @@ impl PathConstrainedNetworkFlow { impl Problem for PathConstrainedNetworkFlow { const NAME: &'static str = "PathConstrainedNetworkFlow"; - type Metric = bool; + type Value = bool; fn dims(&self) -> Vec { self.paths diff --git a/src/models/graph/rooted_tree_arrangement.rs b/src/models/graph/rooted_tree_arrangement.rs index db6a721da..b68cbb0d9 100644 --- a/src/models/graph/rooted_tree_arrangement.rs +++ b/src/models/graph/rooted_tree_arrangement.rs @@ -100,7 +100,7 @@ where G: Graph + VariantParam, { const NAME: &'static str = "RootedTreeArrangement"; - type Metric = bool; + type Value = bool; fn variant() -> Vec<(&'static str, &'static str)> { crate::variant_params![G] diff --git a/src/models/graph/rural_postman.rs b/src/models/graph/rural_postman.rs index e77842b45..0cdf26fe6 100644 --- a/src/models/graph/rural_postman.rs +++ b/src/models/graph/rural_postman.rs @@ -252,7 +252,7 @@ where W: WeightElement + crate::variant::VariantParam, { const NAME: &'static str = "RuralPostman"; - type Metric = bool; + type Value = bool; fn variant() -> Vec<(&'static str, &'static str)> { crate::variant_params![G, W] diff --git a/src/models/graph/shortest_weight_constrained_path.rs b/src/models/graph/shortest_weight_constrained_path.rs index 2f79a9e41..f528a788b 100644 --- a/src/models/graph/shortest_weight_constrained_path.rs +++ b/src/models/graph/shortest_weight_constrained_path.rs @@ -308,7 +308,7 @@ where N: WeightElement + crate::variant::VariantParam, { const NAME: &'static str = "ShortestWeightConstrainedPath"; - type Metric = bool; + type Value = bool; fn variant() -> Vec<(&'static str, &'static str)> { crate::variant_params![G, N] diff --git a/src/models/graph/spin_glass.rs b/src/models/graph/spin_glass.rs index 92ba26c67..1949f8c7a 100644 --- a/src/models/graph/spin_glass.rs +++ b/src/models/graph/spin_glass.rs @@ -220,7 +220,7 @@ where + From, { const NAME: &'static str = "SpinGlass"; - type Metric = SolutionSize; + type Value = SolutionSize; fn dims(&self) -> Vec { vec![2; self.graph.num_vertices()] @@ -249,7 +249,7 @@ where + std::ops::Mul + From, { - type Value = W::Sum; + type Objective = W::Sum; fn direction(&self) -> Direction { Direction::Minimize diff --git a/src/models/graph/steiner_tree.rs b/src/models/graph/steiner_tree.rs index cb2236b78..1dce2f4d4 100644 --- a/src/models/graph/steiner_tree.rs +++ b/src/models/graph/steiner_tree.rs @@ -221,7 +221,7 @@ where W: WeightElement + crate::variant::VariantParam, { const NAME: &'static str = "SteinerTree"; - type Metric = SolutionSize; + type Value = SolutionSize; fn variant() -> Vec<(&'static str, &'static str)> { crate::variant_params![G, W] @@ -252,7 +252,7 @@ where G: Graph + crate::variant::VariantParam, W: WeightElement + crate::variant::VariantParam, { - type Value = W::Sum; + type Objective = W::Sum; fn direction(&self) -> Direction { Direction::Minimize diff --git a/src/models/graph/steiner_tree_in_graphs.rs b/src/models/graph/steiner_tree_in_graphs.rs index 7ab147afe..28bb062fb 100644 --- a/src/models/graph/steiner_tree_in_graphs.rs +++ b/src/models/graph/steiner_tree_in_graphs.rs @@ -175,7 +175,7 @@ where W: WeightElement + crate::variant::VariantParam, { const NAME: &'static str = "SteinerTreeInGraphs"; - type Metric = SolutionSize; + type Value = SolutionSize; fn variant() -> Vec<(&'static str, &'static str)> { crate::variant_params![G, W] @@ -210,7 +210,7 @@ where G: Graph + crate::variant::VariantParam, W: WeightElement + crate::variant::VariantParam, { - type Value = W::Sum; + type Objective = W::Sum; fn direction(&self) -> Direction { Direction::Minimize diff --git a/src/models/graph/strong_connectivity_augmentation.rs b/src/models/graph/strong_connectivity_augmentation.rs index b48d23936..383c9ed79 100644 --- a/src/models/graph/strong_connectivity_augmentation.rs +++ b/src/models/graph/strong_connectivity_augmentation.rs @@ -179,7 +179,7 @@ where W: WeightElement + crate::variant::VariantParam, { const NAME: &'static str = "StrongConnectivityAugmentation"; - type Metric = bool; + type Value = bool; fn variant() -> Vec<(&'static str, &'static str)> { crate::variant_params![W] diff --git a/src/models/graph/subgraph_isomorphism.rs b/src/models/graph/subgraph_isomorphism.rs index 50f9654ec..4e3ba2731 100644 --- a/src/models/graph/subgraph_isomorphism.rs +++ b/src/models/graph/subgraph_isomorphism.rs @@ -120,7 +120,7 @@ impl SubgraphIsomorphism { impl Problem for SubgraphIsomorphism { const NAME: &'static str = "SubgraphIsomorphism"; - type Metric = bool; + type Value = bool; fn dims(&self) -> Vec { let n_host = self.host_graph.num_vertices(); diff --git a/src/models/graph/traveling_salesman.rs b/src/models/graph/traveling_salesman.rs index 76cffc00b..21bea1c0a 100644 --- a/src/models/graph/traveling_salesman.rs +++ b/src/models/graph/traveling_salesman.rs @@ -150,7 +150,7 @@ where W: WeightElement + crate::variant::VariantParam, { const NAME: &'static str = "TravelingSalesman"; - type Metric = SolutionSize; + type Value = SolutionSize; fn variant() -> Vec<(&'static str, &'static str)> { crate::variant_params![G, W] @@ -181,7 +181,7 @@ where G: Graph + crate::variant::VariantParam, W: WeightElement + crate::variant::VariantParam, { - type Value = W::Sum; + type Objective = W::Sum; fn direction(&self) -> Direction { Direction::Minimize diff --git a/src/models/graph/undirected_flow_lower_bounds.rs b/src/models/graph/undirected_flow_lower_bounds.rs index 0db1709d9..dff4db4a7 100644 --- a/src/models/graph/undirected_flow_lower_bounds.rs +++ b/src/models/graph/undirected_flow_lower_bounds.rs @@ -216,7 +216,7 @@ impl UndirectedFlowLowerBounds { impl Problem for UndirectedFlowLowerBounds { const NAME: &'static str = "UndirectedFlowLowerBounds"; - type Metric = bool; + type Value = bool; fn variant() -> Vec<(&'static str, &'static str)> { crate::variant_params![] diff --git a/src/models/graph/undirected_two_commodity_integral_flow.rs b/src/models/graph/undirected_two_commodity_integral_flow.rs index 80019a5d1..7aa631089 100644 --- a/src/models/graph/undirected_two_commodity_integral_flow.rs +++ b/src/models/graph/undirected_two_commodity_integral_flow.rs @@ -218,7 +218,7 @@ impl UndirectedTwoCommodityIntegralFlow { impl Problem for UndirectedTwoCommodityIntegralFlow { const NAME: &'static str = "UndirectedTwoCommodityIntegralFlow"; - type Metric = bool; + type Value = bool; fn variant() -> Vec<(&'static str, &'static str)> { crate::variant_params![] diff --git a/src/models/misc/additional_key.rs b/src/models/misc/additional_key.rs index 8f0a60ea3..2437cadb1 100644 --- a/src/models/misc/additional_key.rs +++ b/src/models/misc/additional_key.rs @@ -192,7 +192,7 @@ impl AdditionalKey { impl Problem for AdditionalKey { const NAME: &'static str = "AdditionalKey"; - type Metric = bool; + type Value = bool; fn variant() -> Vec<(&'static str, &'static str)> { crate::variant_params![] diff --git a/src/models/misc/bin_packing.rs b/src/models/misc/bin_packing.rs index 9843518da..419575b50 100644 --- a/src/models/misc/bin_packing.rs +++ b/src/models/misc/bin_packing.rs @@ -87,7 +87,7 @@ where W::Sum: PartialOrd, { const NAME: &'static str = "BinPacking"; - type Metric = SolutionSize; + type Value = SolutionSize; fn variant() -> Vec<(&'static str, &'static str)> { crate::variant_params![W] @@ -112,7 +112,7 @@ where W: WeightElement + crate::variant::VariantParam, W::Sum: PartialOrd, { - type Value = i32; + type Objective = i32; fn direction(&self) -> Direction { Direction::Minimize diff --git a/src/models/misc/boyce_codd_normal_form_violation.rs b/src/models/misc/boyce_codd_normal_form_violation.rs index 2348e816a..f3d7a21b1 100644 --- a/src/models/misc/boyce_codd_normal_form_violation.rs +++ b/src/models/misc/boyce_codd_normal_form_violation.rs @@ -176,7 +176,7 @@ impl BoyceCoddNormalFormViolation { impl Problem for BoyceCoddNormalFormViolation { const NAME: &'static str = "BoyceCoddNormalFormViolation"; - type Metric = bool; + type Value = bool; fn dims(&self) -> Vec { vec![2; self.target_subset.len()] diff --git a/src/models/misc/capacity_assignment.rs b/src/models/misc/capacity_assignment.rs index 46d889b4c..58a34a558 100644 --- a/src/models/misc/capacity_assignment.rs +++ b/src/models/misc/capacity_assignment.rs @@ -155,7 +155,7 @@ impl CapacityAssignment { impl Problem for CapacityAssignment { const NAME: &'static str = "CapacityAssignment"; - type Metric = bool; + type Value = bool; fn dims(&self) -> Vec { vec![self.num_capacities(); self.num_links()] diff --git a/src/models/misc/conjunctive_boolean_query.rs b/src/models/misc/conjunctive_boolean_query.rs index 9dd0631f0..8154c92d5 100644 --- a/src/models/misc/conjunctive_boolean_query.rs +++ b/src/models/misc/conjunctive_boolean_query.rs @@ -191,7 +191,7 @@ impl ConjunctiveBooleanQuery { impl Problem for ConjunctiveBooleanQuery { const NAME: &'static str = "ConjunctiveBooleanQuery"; - type Metric = bool; + type Value = bool; fn variant() -> Vec<(&'static str, &'static str)> { crate::variant_params![] diff --git a/src/models/misc/conjunctive_query_foldability.rs b/src/models/misc/conjunctive_query_foldability.rs index 87b08299e..493f41188 100644 --- a/src/models/misc/conjunctive_query_foldability.rs +++ b/src/models/misc/conjunctive_query_foldability.rs @@ -262,7 +262,7 @@ impl ConjunctiveQueryFoldability { impl Problem for ConjunctiveQueryFoldability { const NAME: &'static str = "ConjunctiveQueryFoldability"; - type Metric = bool; + type Value = bool; fn variant() -> Vec<(&'static str, &'static str)> { crate::variant_params![] diff --git a/src/models/misc/consistency_of_database_frequency_tables.rs b/src/models/misc/consistency_of_database_frequency_tables.rs index f052d42e3..98aba0fd0 100644 --- a/src/models/misc/consistency_of_database_frequency_tables.rs +++ b/src/models/misc/consistency_of_database_frequency_tables.rs @@ -278,7 +278,7 @@ impl ConsistencyOfDatabaseFrequencyTables { impl Problem for ConsistencyOfDatabaseFrequencyTables { const NAME: &'static str = "ConsistencyOfDatabaseFrequencyTables"; - type Metric = bool; + type Value = bool; fn variant() -> Vec<(&'static str, &'static str)> { crate::variant_params![] diff --git a/src/models/misc/ensemble_computation.rs b/src/models/misc/ensemble_computation.rs index 0f710a072..fa937a514 100644 --- a/src/models/misc/ensemble_computation.rs +++ b/src/models/misc/ensemble_computation.rs @@ -146,7 +146,7 @@ impl EnsembleComputation { impl Problem for EnsembleComputation { const NAME: &'static str = "EnsembleComputation"; - type Metric = bool; + type Value = bool; fn dims(&self) -> Vec { vec![self.universe_size + self.budget; 2 * self.budget] diff --git a/src/models/misc/expected_retrieval_cost.rs b/src/models/misc/expected_retrieval_cost.rs index 640885691..b08e3b461 100644 --- a/src/models/misc/expected_retrieval_cost.rs +++ b/src/models/misc/expected_retrieval_cost.rs @@ -126,7 +126,7 @@ impl ExpectedRetrievalCost { impl Problem for ExpectedRetrievalCost { const NAME: &'static str = "ExpectedRetrievalCost"; - type Metric = bool; + type Value = bool; fn variant() -> Vec<(&'static str, &'static str)> { crate::variant_params![] diff --git a/src/models/misc/factoring.rs b/src/models/misc/factoring.rs index a0ebc4cb7..526b87892 100644 --- a/src/models/misc/factoring.rs +++ b/src/models/misc/factoring.rs @@ -134,7 +134,7 @@ pub(crate) fn is_factoring(target: u64, a: u64, b: u64) -> bool { impl Problem for Factoring { const NAME: &'static str = "Factoring"; - type Metric = SolutionSize; + type Value = SolutionSize; fn dims(&self) -> Vec { vec![2; self.m + self.n] @@ -158,7 +158,7 @@ impl Problem for Factoring { } impl OptimizationProblem for Factoring { - type Value = i32; + type Objective = i32; fn direction(&self) -> Direction { Direction::Minimize diff --git a/src/models/misc/flow_shop_scheduling.rs b/src/models/misc/flow_shop_scheduling.rs index bbdd9a1f8..73576735c 100644 --- a/src/models/misc/flow_shop_scheduling.rs +++ b/src/models/misc/flow_shop_scheduling.rs @@ -160,7 +160,7 @@ impl FlowShopScheduling { impl Problem for FlowShopScheduling { const NAME: &'static str = "FlowShopScheduling"; - type Metric = bool; + type Value = bool; fn variant() -> Vec<(&'static str, &'static str)> { crate::variant_params![] diff --git a/src/models/misc/knapsack.rs b/src/models/misc/knapsack.rs index 65973fe8e..114df707a 100644 --- a/src/models/misc/knapsack.rs +++ b/src/models/misc/knapsack.rs @@ -119,7 +119,7 @@ impl Knapsack { impl Problem for Knapsack { const NAME: &'static str = "Knapsack"; - type Metric = SolutionSize; + type Value = SolutionSize; fn variant() -> Vec<(&'static str, &'static str)> { crate::variant_params![] @@ -156,7 +156,7 @@ impl Problem for Knapsack { } impl OptimizationProblem for Knapsack { - type Value = i64; + type Objective = i64; fn direction(&self) -> Direction { Direction::Maximize diff --git a/src/models/misc/longest_common_subsequence.rs b/src/models/misc/longest_common_subsequence.rs index c84368d42..bb939dab8 100644 --- a/src/models/misc/longest_common_subsequence.rs +++ b/src/models/misc/longest_common_subsequence.rs @@ -134,7 +134,7 @@ fn is_subsequence(candidate: &[usize], target: &[usize]) -> bool { impl Problem for LongestCommonSubsequence { const NAME: &'static str = "LongestCommonSubsequence"; - type Metric = bool; + type Value = bool; fn variant() -> Vec<(&'static str, &'static str)> { crate::variant_params![] diff --git a/src/models/misc/minimum_tardiness_sequencing.rs b/src/models/misc/minimum_tardiness_sequencing.rs index ad49f5eb0..7cc5e3086 100644 --- a/src/models/misc/minimum_tardiness_sequencing.rs +++ b/src/models/misc/minimum_tardiness_sequencing.rs @@ -125,7 +125,7 @@ impl MinimumTardinessSequencing { impl Problem for MinimumTardinessSequencing { const NAME: &'static str = "MinimumTardinessSequencing"; - type Metric = SolutionSize; + type Value = SolutionSize; fn variant() -> Vec<(&'static str, &'static str)> { crate::variant_params![] @@ -179,7 +179,7 @@ impl Problem for MinimumTardinessSequencing { } impl OptimizationProblem for MinimumTardinessSequencing { - type Value = usize; + type Objective = usize; fn direction(&self) -> Direction { Direction::Minimize diff --git a/src/models/misc/multiprocessor_scheduling.rs b/src/models/misc/multiprocessor_scheduling.rs index cd5d85485..7e836b563 100644 --- a/src/models/misc/multiprocessor_scheduling.rs +++ b/src/models/misc/multiprocessor_scheduling.rs @@ -105,7 +105,7 @@ impl MultiprocessorScheduling { impl Problem for MultiprocessorScheduling { const NAME: &'static str = "MultiprocessorScheduling"; - type Metric = bool; + type Value = bool; fn variant() -> Vec<(&'static str, &'static str)> { crate::variant_params![] diff --git a/src/models/misc/paintshop.rs b/src/models/misc/paintshop.rs index acce73f7e..2c06fc79a 100644 --- a/src/models/misc/paintshop.rs +++ b/src/models/misc/paintshop.rs @@ -168,7 +168,7 @@ pub(crate) fn count_paint_switches(coloring: &[usize]) -> usize { impl Problem for PaintShop { const NAME: &'static str = "PaintShop"; - type Metric = SolutionSize; + type Value = SolutionSize; fn dims(&self) -> Vec { vec![2; self.num_cars] @@ -185,7 +185,7 @@ impl Problem for PaintShop { } impl OptimizationProblem for PaintShop { - type Value = i32; + type Objective = i32; fn direction(&self) -> Direction { Direction::Minimize diff --git a/src/models/misc/partially_ordered_knapsack.rs b/src/models/misc/partially_ordered_knapsack.rs index b4bc7c239..d616ca785 100644 --- a/src/models/misc/partially_ordered_knapsack.rs +++ b/src/models/misc/partially_ordered_knapsack.rs @@ -223,7 +223,7 @@ impl PartiallyOrderedKnapsack { impl Problem for PartiallyOrderedKnapsack { const NAME: &'static str = "PartiallyOrderedKnapsack"; - type Metric = SolutionSize; + type Value = SolutionSize; fn variant() -> Vec<(&'static str, &'static str)> { crate::variant_params![] @@ -266,7 +266,7 @@ impl Problem for PartiallyOrderedKnapsack { } impl OptimizationProblem for PartiallyOrderedKnapsack { - type Value = i64; + type Objective = i64; fn direction(&self) -> Direction { Direction::Maximize diff --git a/src/models/misc/partition.rs b/src/models/misc/partition.rs index 87703d18d..ba9ef1474 100644 --- a/src/models/misc/partition.rs +++ b/src/models/misc/partition.rs @@ -82,7 +82,7 @@ impl Partition { impl Problem for Partition { const NAME: &'static str = "Partition"; - type Metric = bool; + type Value = bool; fn variant() -> Vec<(&'static str, &'static str)> { crate::variant_params![] diff --git a/src/models/misc/precedence_constrained_scheduling.rs b/src/models/misc/precedence_constrained_scheduling.rs index 250458ce4..ce9f9180a 100644 --- a/src/models/misc/precedence_constrained_scheduling.rs +++ b/src/models/misc/precedence_constrained_scheduling.rs @@ -118,7 +118,7 @@ impl PrecedenceConstrainedScheduling { impl Problem for PrecedenceConstrainedScheduling { const NAME: &'static str = "PrecedenceConstrainedScheduling"; - type Metric = bool; + type Value = bool; fn variant() -> Vec<(&'static str, &'static str)> { crate::variant_params![] diff --git a/src/models/misc/rectilinear_picture_compression.rs b/src/models/misc/rectilinear_picture_compression.rs index 01cb2c67c..c023a9bbf 100644 --- a/src/models/misc/rectilinear_picture_compression.rs +++ b/src/models/misc/rectilinear_picture_compression.rs @@ -234,7 +234,7 @@ impl RectilinearPictureCompression { impl Problem for RectilinearPictureCompression { const NAME: &'static str = "RectilinearPictureCompression"; - type Metric = bool; + type Value = bool; fn variant() -> Vec<(&'static str, &'static str)> { crate::variant_params![] diff --git a/src/models/misc/resource_constrained_scheduling.rs b/src/models/misc/resource_constrained_scheduling.rs index 10de92907..26e42ec71 100644 --- a/src/models/misc/resource_constrained_scheduling.rs +++ b/src/models/misc/resource_constrained_scheduling.rs @@ -133,7 +133,7 @@ impl ResourceConstrainedScheduling { impl Problem for ResourceConstrainedScheduling { const NAME: &'static str = "ResourceConstrainedScheduling"; - type Metric = bool; + type Value = bool; fn variant() -> Vec<(&'static str, &'static str)> { crate::variant_params![] diff --git a/src/models/misc/scheduling_with_individual_deadlines.rs b/src/models/misc/scheduling_with_individual_deadlines.rs index 7a6629f17..8e283ecb1 100644 --- a/src/models/misc/scheduling_with_individual_deadlines.rs +++ b/src/models/misc/scheduling_with_individual_deadlines.rs @@ -102,7 +102,7 @@ impl SchedulingWithIndividualDeadlines { impl Problem for SchedulingWithIndividualDeadlines { const NAME: &'static str = "SchedulingWithIndividualDeadlines"; - type Metric = bool; + type Value = bool; fn variant() -> Vec<(&'static str, &'static str)> { crate::variant_params![] diff --git a/src/models/misc/sequencing_to_minimize_maximum_cumulative_cost.rs b/src/models/misc/sequencing_to_minimize_maximum_cumulative_cost.rs index 4e8f94dd1..e4c61335c 100644 --- a/src/models/misc/sequencing_to_minimize_maximum_cumulative_cost.rs +++ b/src/models/misc/sequencing_to_minimize_maximum_cumulative_cost.rs @@ -153,7 +153,7 @@ fn precedence_validation_error(precedences: &[(usize, usize)], num_tasks: usize) impl Problem for SequencingToMinimizeMaximumCumulativeCost { const NAME: &'static str = "SequencingToMinimizeMaximumCumulativeCost"; - type Metric = bool; + type Value = bool; fn variant() -> Vec<(&'static str, &'static str)> { crate::variant_params![] diff --git a/src/models/misc/sequencing_to_minimize_weighted_completion_time.rs b/src/models/misc/sequencing_to_minimize_weighted_completion_time.rs index 2f63c44b3..6e72740bf 100644 --- a/src/models/misc/sequencing_to_minimize_weighted_completion_time.rs +++ b/src/models/misc/sequencing_to_minimize_weighted_completion_time.rs @@ -207,7 +207,7 @@ impl<'de> Deserialize<'de> for SequencingToMinimizeWeightedCompletionTime { impl Problem for SequencingToMinimizeWeightedCompletionTime { const NAME: &'static str = "SequencingToMinimizeWeightedCompletionTime"; - type Metric = SolutionSize; + type Value = SolutionSize; fn variant() -> Vec<(&'static str, &'static str)> { crate::variant_params![] @@ -227,7 +227,7 @@ impl Problem for SequencingToMinimizeWeightedCompletionTime { } impl OptimizationProblem for SequencingToMinimizeWeightedCompletionTime { - type Value = u64; + type Objective = u64; fn direction(&self) -> Direction { Direction::Minimize diff --git a/src/models/misc/sequencing_to_minimize_weighted_tardiness.rs b/src/models/misc/sequencing_to_minimize_weighted_tardiness.rs index 58789cdfe..c69dcc7f1 100644 --- a/src/models/misc/sequencing_to_minimize_weighted_tardiness.rs +++ b/src/models/misc/sequencing_to_minimize_weighted_tardiness.rs @@ -153,7 +153,7 @@ impl SequencingToMinimizeWeightedTardiness { impl Problem for SequencingToMinimizeWeightedTardiness { const NAME: &'static str = "SequencingToMinimizeWeightedTardiness"; - type Metric = bool; + type Value = bool; fn variant() -> Vec<(&'static str, &'static str)> { crate::variant_params![] 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 8a706a34f..90d020c05 100644 --- a/src/models/misc/sequencing_with_release_times_and_deadlines.rs +++ b/src/models/misc/sequencing_with_release_times_and_deadlines.rs @@ -106,7 +106,7 @@ impl SequencingWithReleaseTimesAndDeadlines { impl Problem for SequencingWithReleaseTimesAndDeadlines { const NAME: &'static str = "SequencingWithReleaseTimesAndDeadlines"; - type Metric = bool; + type Value = bool; fn variant() -> Vec<(&'static str, &'static str)> { crate::variant_params![] diff --git a/src/models/misc/sequencing_within_intervals.rs b/src/models/misc/sequencing_within_intervals.rs index ed42ef47c..d4ae4df18 100644 --- a/src/models/misc/sequencing_within_intervals.rs +++ b/src/models/misc/sequencing_within_intervals.rs @@ -122,7 +122,7 @@ impl SequencingWithinIntervals { impl Problem for SequencingWithinIntervals { const NAME: &'static str = "SequencingWithinIntervals"; - type Metric = bool; + type Value = bool; fn variant() -> Vec<(&'static str, &'static str)> { crate::variant_params![] diff --git a/src/models/misc/shortest_common_supersequence.rs b/src/models/misc/shortest_common_supersequence.rs index b0a58641b..4a67b33a0 100644 --- a/src/models/misc/shortest_common_supersequence.rs +++ b/src/models/misc/shortest_common_supersequence.rs @@ -125,7 +125,7 @@ fn is_subsequence(needle: &[usize], haystack: &[usize]) -> bool { impl Problem for ShortestCommonSupersequence { const NAME: &'static str = "ShortestCommonSupersequence"; - type Metric = bool; + type Value = bool; fn variant() -> Vec<(&'static str, &'static str)> { crate::variant_params![] diff --git a/src/models/misc/stacker_crane.rs b/src/models/misc/stacker_crane.rs index b8786a4f1..297a465aa 100644 --- a/src/models/misc/stacker_crane.rs +++ b/src/models/misc/stacker_crane.rs @@ -259,7 +259,7 @@ impl StackerCrane { impl Problem for StackerCrane { const NAME: &'static str = "StackerCrane"; - type Metric = bool; + type Value = bool; fn variant() -> Vec<(&'static str, &'static str)> { crate::variant_params![] diff --git a/src/models/misc/staff_scheduling.rs b/src/models/misc/staff_scheduling.rs index e2f896493..5f392d6dd 100644 --- a/src/models/misc/staff_scheduling.rs +++ b/src/models/misc/staff_scheduling.rs @@ -150,7 +150,7 @@ impl StaffScheduling { impl Problem for StaffScheduling { const NAME: &'static str = "StaffScheduling"; - type Metric = bool; + type Value = bool; fn dims(&self) -> Vec { vec![self.worker_limit() + 1; self.num_schedules()] diff --git a/src/models/misc/string_to_string_correction.rs b/src/models/misc/string_to_string_correction.rs index 161de341c..d4f9de1a9 100644 --- a/src/models/misc/string_to_string_correction.rs +++ b/src/models/misc/string_to_string_correction.rs @@ -139,7 +139,7 @@ impl StringToStringCorrection { impl Problem for StringToStringCorrection { const NAME: &'static str = "StringToStringCorrection"; - type Metric = bool; + type Value = bool; fn variant() -> Vec<(&'static str, &'static str)> { crate::variant_params![] diff --git a/src/models/misc/subset_sum.rs b/src/models/misc/subset_sum.rs index 7685cec22..26e38c3df 100644 --- a/src/models/misc/subset_sum.rs +++ b/src/models/misc/subset_sum.rs @@ -108,7 +108,7 @@ impl SubsetSum { impl Problem for SubsetSum { const NAME: &'static str = "SubsetSum"; - type Metric = bool; + type Value = bool; fn variant() -> Vec<(&'static str, &'static str)> { crate::variant_params![] diff --git a/src/models/misc/sum_of_squares_partition.rs b/src/models/misc/sum_of_squares_partition.rs index 0445d040e..697db9fd3 100644 --- a/src/models/misc/sum_of_squares_partition.rs +++ b/src/models/misc/sum_of_squares_partition.rs @@ -164,7 +164,7 @@ impl<'de> Deserialize<'de> for SumOfSquaresPartition { impl Problem for SumOfSquaresPartition { const NAME: &'static str = "SumOfSquaresPartition"; - type Metric = bool; + type Value = bool; fn variant() -> Vec<(&'static str, &'static str)> { crate::variant_params![] diff --git a/src/models/misc/timetable_design.rs b/src/models/misc/timetable_design.rs index f7678d9fa..b1be88713 100644 --- a/src/models/misc/timetable_design.rs +++ b/src/models/misc/timetable_design.rs @@ -303,7 +303,7 @@ impl TimetableDesign { impl Problem for TimetableDesign { const NAME: &'static str = "TimetableDesign"; - type Metric = bool; + type Value = bool; fn dims(&self) -> Vec { vec![2; self.config_len()] diff --git a/src/models/set/comparative_containment.rs b/src/models/set/comparative_containment.rs index 50a80beb9..865f183f9 100644 --- a/src/models/set/comparative_containment.rs +++ b/src/models/set/comparative_containment.rs @@ -177,7 +177,7 @@ where W: WeightElement + crate::variant::VariantParam, { const NAME: &'static str = "ComparativeContainment"; - type Metric = bool; + type Value = bool; fn dims(&self) -> Vec { vec![2; self.universe_size] diff --git a/src/models/set/consecutive_sets.rs b/src/models/set/consecutive_sets.rs index c787d4d56..b978551e1 100644 --- a/src/models/set/consecutive_sets.rs +++ b/src/models/set/consecutive_sets.rs @@ -135,7 +135,7 @@ impl ConsecutiveSets { impl Problem for ConsecutiveSets { const NAME: &'static str = "ConsecutiveSets"; - type Metric = bool; + type Value = bool; fn dims(&self) -> Vec { // Each position can be any symbol (0..alphabet_size-1) or "unused" (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 0bf0fdf36..0d543cbcd 100644 --- a/src/models/set/exact_cover_by_3_sets.rs +++ b/src/models/set/exact_cover_by_3_sets.rs @@ -143,7 +143,7 @@ impl ExactCoverBy3Sets { impl Problem for ExactCoverBy3Sets { const NAME: &'static str = "ExactCoverBy3Sets"; - type Metric = bool; + type Value = bool; fn dims(&self) -> Vec { vec![2; self.subsets.len()] diff --git a/src/models/set/maximum_set_packing.rs b/src/models/set/maximum_set_packing.rs index 45aff086a..80f0db1db 100644 --- a/src/models/set/maximum_set_packing.rs +++ b/src/models/set/maximum_set_packing.rs @@ -141,7 +141,7 @@ where W: WeightElement + crate::variant::VariantParam, { const NAME: &'static str = "MaximumSetPacking"; - type Metric = SolutionSize; + type Value = SolutionSize; fn dims(&self) -> Vec { vec![2; self.sets.len()] @@ -169,7 +169,7 @@ impl OptimizationProblem for MaximumSetPacking where W: WeightElement + crate::variant::VariantParam, { - type Value = W::Sum; + type Objective = W::Sum; fn direction(&self) -> Direction { Direction::Maximize diff --git a/src/models/set/minimum_cardinality_key.rs b/src/models/set/minimum_cardinality_key.rs index c43bc2ff2..24ff59c97 100644 --- a/src/models/set/minimum_cardinality_key.rs +++ b/src/models/set/minimum_cardinality_key.rs @@ -148,7 +148,7 @@ impl MinimumCardinalityKey { impl Problem for MinimumCardinalityKey { const NAME: &'static str = "MinimumCardinalityKey"; - type Metric = bool; + type Value = bool; fn dims(&self) -> Vec { vec![2; self.num_attributes] diff --git a/src/models/set/minimum_hitting_set.rs b/src/models/set/minimum_hitting_set.rs index 06c362382..293bb6acc 100644 --- a/src/models/set/minimum_hitting_set.rs +++ b/src/models/set/minimum_hitting_set.rs @@ -117,7 +117,7 @@ impl MinimumHittingSet { impl Problem for MinimumHittingSet { const NAME: &'static str = "MinimumHittingSet"; - type Metric = SolutionSize; + type Value = SolutionSize; fn dims(&self) -> Vec { vec![2; self.universe_size] @@ -144,7 +144,7 @@ impl Problem for MinimumHittingSet { } impl OptimizationProblem for MinimumHittingSet { - type Value = usize; + type Objective = usize; fn direction(&self) -> Direction { Direction::Minimize diff --git a/src/models/set/minimum_set_covering.rs b/src/models/set/minimum_set_covering.rs index c17eb271d..7653ebab0 100644 --- a/src/models/set/minimum_set_covering.rs +++ b/src/models/set/minimum_set_covering.rs @@ -143,7 +143,7 @@ where W: WeightElement + crate::variant::VariantParam, { const NAME: &'static str = "MinimumSetCovering"; - type Metric = SolutionSize; + type Value = SolutionSize; fn dims(&self) -> Vec { vec![2; self.sets.len()] @@ -174,7 +174,7 @@ impl OptimizationProblem for MinimumSetCovering where W: WeightElement + crate::variant::VariantParam, { - type Value = W::Sum; + type Objective = W::Sum; fn direction(&self) -> Direction { Direction::Minimize diff --git a/src/models/set/prime_attribute_name.rs b/src/models/set/prime_attribute_name.rs index c2c1c7314..337d881c0 100644 --- a/src/models/set/prime_attribute_name.rs +++ b/src/models/set/prime_attribute_name.rs @@ -155,7 +155,7 @@ impl PrimeAttributeName { impl Problem for PrimeAttributeName { const NAME: &'static str = "PrimeAttributeName"; - type Metric = bool; + type Value = bool; fn dims(&self) -> Vec { vec![2; self.num_attributes] diff --git a/src/models/set/rooted_tree_storage_assignment.rs b/src/models/set/rooted_tree_storage_assignment.rs index 9692fcc84..26a0028c4 100644 --- a/src/models/set/rooted_tree_storage_assignment.rs +++ b/src/models/set/rooted_tree_storage_assignment.rs @@ -174,7 +174,7 @@ impl RootedTreeStorageAssignment { impl Problem for RootedTreeStorageAssignment { const NAME: &'static str = "RootedTreeStorageAssignment"; - type Metric = bool; + type Value = bool; fn dims(&self) -> Vec { vec![self.universe_size; self.universe_size] diff --git a/src/models/set/set_basis.rs b/src/models/set/set_basis.rs index 0d0cd8dbb..9746893e6 100644 --- a/src/models/set/set_basis.rs +++ b/src/models/set/set_basis.rs @@ -147,7 +147,7 @@ impl SetBasis { impl Problem for SetBasis { const NAME: &'static str = "SetBasis"; - type Metric = bool; + type Value = bool; fn dims(&self) -> Vec { vec![2; self.k * self.universe_size] diff --git a/src/models/set/two_dimensional_consecutive_sets.rs b/src/models/set/two_dimensional_consecutive_sets.rs index a3e6b20ac..be3dc056b 100644 --- a/src/models/set/two_dimensional_consecutive_sets.rs +++ b/src/models/set/two_dimensional_consecutive_sets.rs @@ -145,7 +145,7 @@ impl TwoDimensionalConsecutiveSets { impl Problem for TwoDimensionalConsecutiveSets { const NAME: &'static str = "TwoDimensionalConsecutiveSets"; - type Metric = bool; + type Value = bool; fn dims(&self) -> Vec { vec![self.alphabet_size; self.alphabet_size] diff --git a/src/registry/dyn_problem.rs b/src/registry/dyn_problem.rs index 6b1a9b0e0..92f0f0005 100644 --- a/src/registry/dyn_problem.rs +++ b/src/registry/dyn_problem.rs @@ -31,7 +31,7 @@ pub trait DynProblem: Any { impl DynProblem for T where T: Problem + Serialize + 'static, - T::Metric: fmt::Debug + Serialize, + T::Value: fmt::Debug + Serialize, { fn evaluate_dyn(&self, config: &[usize]) -> String { format!("{:?}", self.evaluate(config)) diff --git a/src/rules/test_helpers.rs b/src/rules/test_helpers.rs index a03655075..9136f735f 100644 --- a/src/rules/test_helpers.rs +++ b/src/rules/test_helpers.rs @@ -11,8 +11,8 @@ fn verify_optimization_round_trip( context: &str, ) where Source: OptimizationProblem + 'static, - ::Value: std::fmt::Debug + PartialEq, - ::Metric: std::fmt::Debug + PartialEq, + ::Objective: std::fmt::Debug + PartialEq, + ::Value: std::fmt::Debug + PartialEq, Extract: Fn(&[usize]) -> Vec, { assert!( @@ -99,8 +99,8 @@ pub(crate) fn assert_optimization_round_trip_from_optimization_target( R: ReductionResult, R::Source: OptimizationProblem + 'static, R::Target: OptimizationProblem + 'static, - ::Value: std::fmt::Debug + PartialEq, - ::Metric: std::fmt::Debug + PartialEq, + ::Objective: std::fmt::Debug + PartialEq, + ::Value: std::fmt::Debug + PartialEq, { let target_solutions = BruteForce::new().find_all_best(reduction.target_problem()); verify_optimization_round_trip( @@ -120,8 +120,8 @@ pub(crate) fn assert_optimization_round_trip_from_satisfaction_target( R: ReductionResult, R::Source: OptimizationProblem + 'static, R::Target: SatisfactionProblem + 'static, - ::Value: std::fmt::Debug + PartialEq, - ::Metric: std::fmt::Debug + PartialEq, + ::Objective: std::fmt::Debug + PartialEq, + ::Value: std::fmt::Debug + PartialEq, { let target_solutions = BruteForce::new().find_all_satisfying(reduction.target_problem()); verify_optimization_round_trip( @@ -140,8 +140,8 @@ pub(crate) fn assert_optimization_round_trip_chain( ) where Source: OptimizationProblem + 'static, Target: OptimizationProblem + 'static, - ::Value: std::fmt::Debug + PartialEq, - ::Metric: std::fmt::Debug + PartialEq, + ::Objective: std::fmt::Debug + PartialEq, + ::Value: std::fmt::Debug + PartialEq, { let target_solutions = BruteForce::new().find_all_best(chain.target_problem::()); verify_optimization_round_trip( @@ -222,13 +222,13 @@ mod tests { impl Problem for ToyOptimizationProblem { const NAME: &'static str = "ToyOptimizationProblem"; - type Metric = SolutionSize; + type Value = SolutionSize; fn dims(&self) -> Vec { vec![2, 2] } - fn evaluate(&self, config: &[usize]) -> Self::Metric { + fn evaluate(&self, config: &[usize]) -> Self::Value { match config { [1, 0] | [0, 1] => SolutionSize::Valid(1), _ => SolutionSize::Invalid, @@ -241,7 +241,7 @@ mod tests { } impl OptimizationProblem for ToyOptimizationProblem { - type Value = i32; + type Objective = i32; fn direction(&self) -> Direction { Direction::Maximize @@ -253,13 +253,13 @@ mod tests { impl Problem for ToySatisfactionProblem { const NAME: &'static str = "ToySatisfactionProblem"; - type Metric = bool; + type Value = bool; fn dims(&self) -> Vec { vec![2, 2] } - fn evaluate(&self, config: &[usize]) -> Self::Metric { + fn evaluate(&self, config: &[usize]) -> Self::Value { matches!(config, [1, 0] | [0, 1]) } diff --git a/src/solvers/brute_force.rs b/src/solvers/brute_force.rs index e84b7190e..47c7d578b 100644 --- a/src/solvers/brute_force.rs +++ b/src/solvers/brute_force.rs @@ -2,12 +2,14 @@ use crate::config::DimsIterator; use crate::solvers::Solver; -use crate::traits::{OptimizationProblem, Problem}; +use crate::traits::{OptimizationProblem, Problem, SatisfactionProblem}; +use crate::types::Aggregate; /// A brute force solver that enumerates all possible configurations. /// /// This solver is exponential in the number of variables but guarantees -/// finding all optimal solutions. +/// finding the full aggregate value and all witness configurations when the +/// aggregate type supports witnesses. #[derive(Debug, Clone, Default)] pub struct BruteForce; @@ -17,20 +19,18 @@ impl BruteForce { Self } - /// Find all optimal solutions for an optimization problem. - /// - /// Returns all configurations that achieve the optimal metric value. - /// Returns empty vec if all configurations are invalid. + /// Temporary compatibility helper for optimization problems. pub fn find_all_best(&self, problem: &P) -> Vec> { let iter = DimsIterator::new(problem.dims()); let direction = problem.direction(); let mut best_solutions: Vec> = vec![]; - let mut best_metric: Option> = None; + let mut best_metric: Option< + crate::types::SolutionSize<

::Objective>, + > = None; for config in iter { let metric = problem.evaluate(&config); - // Skip infeasible solutions if !metric.is_valid() { continue; } @@ -54,7 +54,6 @@ impl BruteForce { best_solutions.clear(); best_solutions.push(config); } else if best_metric.is_some() { - // Equal quality - add to solutions best_solutions.push(config); } } @@ -62,23 +61,92 @@ impl BruteForce { best_solutions } - /// Find all satisfying solutions for constraint satisfaction problems. - /// - /// Returns all configurations where `problem.evaluate(config)` returns `true`. - pub fn find_all_satisfying>(&self, problem: &P) -> Vec> { + /// Temporary compatibility helper for optimization problems. + pub fn find_best(&self, problem: &P) -> Option> { + self.find_all_best(problem).into_iter().next() + } + + /// Temporary compatibility helper for satisfaction problems. + pub fn find_all_satisfying(&self, problem: &P) -> Vec> { DimsIterator::new(problem.dims()) .filter(|config| problem.evaluate(config)) .collect() } + + /// Temporary compatibility helper for satisfaction problems. + pub fn find_satisfying(&self, problem: &P) -> Option> { + DimsIterator::new(problem.dims()).find(|config| problem.evaluate(config)) + } + + /// Find one witness configuration when the aggregate value admits them. + pub fn find_witness

(&self, problem: &P) -> Option> + where + P: Problem, + P::Value: Aggregate, + { + self.find_all_witnesses(problem).into_iter().next() + } + + /// Find all witness configurations for witness-supporting aggregates. + pub fn find_all_witnesses

(&self, problem: &P) -> Vec> + where + P: Problem, + P::Value: Aggregate, + { + let total = self.solve(problem); + + if !P::Value::supports_witnesses() { + return vec![]; + } + + DimsIterator::new(problem.dims()) + .filter(|config| { + let value = problem.evaluate(config); + P::Value::contributes_to_witnesses(&value, &total) + }) + .collect() + } + + /// Solve a problem and collect all witness configurations in one passable API. + pub fn solve_with_witnesses

(&self, problem: &P) -> (P::Value, Vec>) + where + P: Problem, + P::Value: Aggregate, + { + let total = self.solve(problem); + + if !P::Value::supports_witnesses() { + return (total, vec![]); + } + + let witnesses = DimsIterator::new(problem.dims()) + .filter(|config| { + let value = problem.evaluate(config); + P::Value::contributes_to_witnesses(&value, &total) + }) + .collect(); + + (total, witnesses) + } } impl Solver for BruteForce { + fn solve

(&self, problem: &P) -> P::Value + where + P: Problem, + P::Value: Aggregate, + { + DimsIterator::new(problem.dims()) + .map(|config| problem.evaluate(&config)) + .fold(P::Value::identity(), P::Value::combine) + } + fn find_best(&self, problem: &P) -> Option> { - self.find_all_best(problem).into_iter().next() + BruteForce::find_best(self, problem) } - fn find_satisfying>(&self, problem: &P) -> Option> { - DimsIterator::new(problem.dims()).find(|config| problem.evaluate(config)) + fn find_satisfying(&self, problem: &P) -> Option> { + BruteForce::find_satisfying(self, problem) } } diff --git a/src/solvers/mod.rs b/src/solvers/mod.rs index 894bc8207..4fea37889 100644 --- a/src/solvers/mod.rs +++ b/src/solvers/mod.rs @@ -10,16 +10,19 @@ pub use brute_force::BruteForce; #[cfg(feature = "ilp-solver")] pub use ilp::ILPSolver; -use crate::traits::{OptimizationProblem, Problem}; +use crate::traits::{OptimizationProblem, Problem, SatisfactionProblem}; /// Trait for problem solvers. pub trait Solver { - /// Find one optimal solution for an optimization problem. - /// - /// Returns a single configuration that achieves the optimal metric value, - /// or `None` if no feasible configuration exists. + /// Solve a problem to its aggregate value. + fn solve

(&self, problem: &P) -> P::Value + where + P: Problem, + P::Value: crate::types::Aggregate; + + /// Temporary compatibility helper for optimization problems. fn find_best(&self, problem: &P) -> Option>; - /// Find any satisfying solution for a satisfaction problem (Metric = bool). - fn find_satisfying>(&self, problem: &P) -> Option>; + /// Temporary compatibility helper for satisfaction problems. + fn find_satisfying(&self, problem: &P) -> Option>; } diff --git a/src/traits.rs b/src/traits.rs index 3ae298392..e8a39a096 100644 --- a/src/traits.rs +++ b/src/traits.rs @@ -1,18 +1,18 @@ //! Core traits for problem definitions. -/// Minimal problem trait — a problem is a function from configuration to metric. +/// Minimal problem trait — a problem is a function from configuration to value. /// /// This trait defines the interface for computational problems that can be /// solved by enumeration or reduction to other problems. pub trait Problem: Clone { /// Base name of this problem type (e.g., "MaximumIndependentSet"). const NAME: &'static str; - /// The evaluation metric type. - type Metric: Clone; + /// The evaluation value type. + type Value: Clone; /// Configuration space dimensions. Each entry is the cardinality of that variable. fn dims(&self) -> Vec; /// Evaluate the problem on a configuration. - fn evaluate(&self, config: &[usize]) -> Self::Metric; + fn evaluate(&self, config: &[usize]) -> Self::Value; /// Number of variables (derived from dims). fn num_variables(&self) -> usize { self.dims().len() @@ -33,23 +33,16 @@ pub trait Problem: Clone { } } -/// Extension for problems with a numeric objective to optimize. -/// -/// The supertrait bound guarantees `Metric = SolutionSize`, -/// so the solver can call `metric.is_valid()` and `metric.is_better()` -/// directly — no per-problem customization needed. -pub trait OptimizationProblem: Problem> { +/// Temporary compatibility trait for optimization problems during the aggregate migration. +pub trait OptimizationProblem: Problem> { /// The inner objective value type (e.g., `i32`, `f64`). - type Value: PartialOrd + Clone; + type Objective: PartialOrd + Clone; /// Whether to maximize or minimize the metric. fn direction(&self) -> crate::types::Direction; } -/// Marker trait for satisfaction (decision) problems. -/// -/// Satisfaction problems evaluate configurations to `bool`: -/// `true` if the configuration satisfies all constraints, `false` otherwise. -pub trait SatisfactionProblem: Problem {} +/// Temporary compatibility trait for satisfaction problems during the aggregate migration. +pub trait SatisfactionProblem: Problem {} /// Marker trait for explicitly declared problem variants. /// diff --git a/src/unit_tests/rules/traits.rs b/src/unit_tests/rules/traits.rs index ab3bff0c8..7a273633d 100644 --- a/src/unit_tests/rules/traits.rs +++ b/src/unit_tests/rules/traits.rs @@ -13,7 +13,7 @@ struct TargetProblem; impl Problem for SourceProblem { const NAME: &'static str = "Source"; - type Metric = i32; + type Value = i32; fn dims(&self) -> Vec { vec![2, 2] } @@ -27,7 +27,7 @@ impl Problem for SourceProblem { impl Problem for TargetProblem { const NAME: &'static str = "Target"; - type Metric = i32; + type Value = i32; fn dims(&self) -> Vec { vec![2, 2] } diff --git a/src/unit_tests/solvers/brute_force.rs b/src/unit_tests/solvers/brute_force.rs index 2e80628ef..1ec05a591 100644 --- a/src/unit_tests/solvers/brute_force.rs +++ b/src/unit_tests/solvers/brute_force.rs @@ -1,75 +1,64 @@ use super::*; use crate::solvers::Solver; -use crate::traits::{OptimizationProblem, Problem}; -use crate::types::{Direction, SolutionSize}; +use crate::traits::Problem; +use crate::types::{Max, Min, Or, Sum}; -// Simple maximization problem #[derive(Clone)] -struct MaxSumOpt { +struct MaxSumProblem { weights: Vec, } -impl Problem for MaxSumOpt { - const NAME: &'static str = "MaxSumOpt"; - type Metric = SolutionSize; +impl Problem for MaxSumProblem { + const NAME: &'static str = "MaxSumProblem"; + type Value = Max; + fn dims(&self) -> Vec { vec![2; self.weights.len()] } - fn evaluate(&self, config: &[usize]) -> SolutionSize { - SolutionSize::Valid( + + fn evaluate(&self, config: &[usize]) -> Self::Value { + Max(Some( config .iter() .zip(&self.weights) .map(|(&c, &w)| if c == 1 { w } else { 0 }) .sum(), - ) + )) } + fn variant() -> Vec<(&'static str, &'static str)> { vec![("graph", "SimpleGraph"), ("weight", "i32")] } } -impl OptimizationProblem for MaxSumOpt { - type Value = i32; - fn direction(&self) -> Direction { - Direction::Maximize - } -} - -// Simple minimization problem #[derive(Clone)] -struct MinSumOpt { +struct MinSumProblem { weights: Vec, } -impl Problem for MinSumOpt { - const NAME: &'static str = "MinSumOpt"; - type Metric = SolutionSize; +impl Problem for MinSumProblem { + const NAME: &'static str = "MinSumProblem"; + type Value = Min; + fn dims(&self) -> Vec { vec![2; self.weights.len()] } - fn evaluate(&self, config: &[usize]) -> SolutionSize { - SolutionSize::Valid( + + fn evaluate(&self, config: &[usize]) -> Self::Value { + Min(Some( config .iter() .zip(&self.weights) .map(|(&c, &w)| if c == 1 { w } else { 0 }) .sum(), - ) + )) } + fn variant() -> Vec<(&'static str, &'static str)> { vec![("graph", "SimpleGraph"), ("weight", "i32")] } } -impl OptimizationProblem for MinSumOpt { - type Value = i32; - fn direction(&self) -> Direction { - Direction::Minimize - } -} - -// Satisfaction problem (Metric = bool) #[derive(Clone)] struct SatProblem { num_vars: usize, @@ -78,174 +67,143 @@ struct SatProblem { impl Problem for SatProblem { const NAME: &'static str = "SatProblem"; - type Metric = bool; + type Value = Or; + fn dims(&self) -> Vec { vec![2; self.num_vars] } - fn evaluate(&self, config: &[usize]) -> bool { - self.satisfying.iter().any(|s| s == config) + + fn evaluate(&self, config: &[usize]) -> Self::Value { + Or(self.satisfying.iter().any(|s| s == config)) } + fn variant() -> Vec<(&'static str, &'static str)> { vec![("graph", "SimpleGraph"), ("weight", "bool")] } } -#[test] -fn test_solver_maximization() { - let problem = MaxSumOpt { - weights: vec![1, 2, 3], - }; - let solver = BruteForce::new(); - - let best = solver.find_all_best(&problem); - assert_eq!(best.len(), 1); - assert_eq!(best[0], vec![1, 1, 1]); // Select all for max sum = 6 -} - -#[test] -fn test_solver_minimization() { - let problem = MinSumOpt { - weights: vec![1, 2, 3], - }; - let solver = BruteForce::new(); - - let best = solver.find_all_best(&problem); - assert_eq!(best.len(), 1); - assert_eq!(best[0], vec![0, 0, 0]); // Select none for min sum = 0 +#[derive(Clone)] +struct SumProblem { + weights: Vec, } -#[test] -fn test_solver_multiple_optimal() { - // Two variables with equal weights -> multiple optima - let problem = MaxSumOpt { - weights: vec![5, 5], - }; - let solver = BruteForce::new(); +impl Problem for SumProblem { + const NAME: &'static str = "SumProblem"; + type Value = Sum; - let best = solver.find_all_best(&problem); - assert_eq!(best.len(), 1); - assert_eq!(best[0], vec![1, 1]); // Only one optimal: select both = 10 -} + fn dims(&self) -> Vec { + vec![2; self.weights.len()] + } -#[test] -fn test_solver_empty() { - let problem = MaxSumOpt { weights: vec![] }; - let solver = BruteForce::new(); + fn evaluate(&self, config: &[usize]) -> Self::Value { + Sum( + config + .iter() + .zip(&self.weights) + .map(|(&c, &w)| if c == 1 { w } else { 0 }) + .sum(), + ) + } - let best = solver.find_all_best(&problem); - assert_eq!(best, vec![Vec::::new()]); + fn variant() -> Vec<(&'static str, &'static str)> { + vec![("graph", "SimpleGraph"), ("weight", "u64")] + } } #[test] -fn test_solver_find_satisfying() { - let problem = SatProblem { - num_vars: 2, - satisfying: vec![vec![1, 0], vec![0, 1]], +fn test_solver_solves_max_value() { + let problem = MaxSumProblem { + weights: vec![1, 2, 3], }; let solver = BruteForce::new(); - let solution = solver.find_satisfying(&problem); - assert!(solution.is_some()); - let sol = solution.unwrap(); - assert!(problem.evaluate(&sol)); + assert_eq!(solver.solve(&problem), Max(Some(6))); } #[test] -fn test_solver_find_satisfying_unsat() { - let problem = SatProblem { - num_vars: 2, - satisfying: vec![], // No satisfying assignment +fn test_solver_solves_min_value() { + let problem = MinSumProblem { + weights: vec![1, 2, 3], }; let solver = BruteForce::new(); - let solution = solver.find_satisfying(&problem); - assert!(solution.is_none()); + assert_eq!(solver.solve(&problem), Min(Some(0))); } #[test] -fn test_solver_find_all_satisfying() { +fn test_solver_solves_satisfaction_value() { let problem = SatProblem { num_vars: 2, satisfying: vec![vec![1, 0], vec![0, 1]], }; let solver = BruteForce::new(); - let solutions = solver.find_all_satisfying(&problem); - assert_eq!(solutions.len(), 2); - assert!(solutions.contains(&vec![1, 0])); - assert!(solutions.contains(&vec![0, 1])); + assert_eq!(solver.solve(&problem), Or(true)); } #[test] -fn test_solver_find_satisfying_empty_dims_satisfiable() { - let problem = SatProblem { - num_vars: 0, - satisfying: vec![vec![]], +fn test_solver_find_witness() { + let problem = MaxSumProblem { + weights: vec![1, 2, 3], }; let solver = BruteForce::new(); - assert_eq!(solver.find_satisfying(&problem), Some(vec![])); - assert_eq!( - solver.find_all_satisfying(&problem), - vec![Vec::::new()] - ); + assert_eq!(solver.find_witness(&problem), Some(vec![1, 1, 1])); } #[test] -fn test_solver_find_satisfying_empty_dims_unsat() { +fn test_solver_find_witness_for_satisfaction_problem() { let problem = SatProblem { - num_vars: 0, - satisfying: vec![], + num_vars: 2, + satisfying: vec![vec![1, 0], vec![0, 1]], }; let solver = BruteForce::new(); - assert_eq!(solver.find_satisfying(&problem), None); - assert_eq!( - solver.find_all_satisfying(&problem), - Vec::>::new() - ); + let witness = solver.find_witness(&problem); + assert!(witness.is_some()); + assert_eq!(problem.evaluate(&witness.unwrap()), Or(true)); } #[test] -fn test_find_best_returns_one_solution() { - let problem = MaxSumOpt { +fn test_solver_find_witness_returns_none_for_sum_problem() { + let problem = SumProblem { weights: vec![1, 2, 3], }; let solver = BruteForce::new(); - let best = solver.find_best(&problem); - assert!(best.is_some()); - assert_eq!(best.unwrap(), vec![1, 1, 1]); + assert_eq!(solver.find_witness(&problem), None); } #[test] -fn test_find_best_empty_problem() { - let problem = MaxSumOpt { weights: vec![] }; +fn test_solver_find_all_witnesses() { + let problem = SatProblem { + num_vars: 2, + satisfying: vec![vec![1, 0], vec![0, 1]], + }; let solver = BruteForce::new(); - let best = solver.find_best(&problem); - assert_eq!(best, Some(vec![])); + let witnesses = solver.find_all_witnesses(&problem); + assert_eq!(witnesses.len(), 2); + assert!(witnesses.contains(&vec![1, 0])); + assert!(witnesses.contains(&vec![0, 1])); } #[test] -fn test_find_best_minimization() { - let problem = MinSumOpt { +fn test_solver_find_all_witnesses_returns_empty_for_sum_problem() { + let problem = SumProblem { weights: vec![1, 2, 3], }; let solver = BruteForce::new(); - let best = solver.find_best(&problem); - assert!(best.is_some()); - assert_eq!(best.unwrap(), vec![0, 0, 0]); + assert!(solver.find_all_witnesses(&problem).is_empty()); } #[test] fn test_solver_with_real_mis() { use crate::models::graph::MaximumIndependentSet; - use crate::topology::SimpleGraph; use crate::traits::Problem; + use crate::topology::SimpleGraph; - // Triangle graph: MIS = 1 let problem = MaximumIndependentSet::new( SimpleGraph::new(3, vec![(0, 1), (1, 2), (0, 2)]), vec![1i32; 3], @@ -253,7 +211,7 @@ fn test_solver_with_real_mis() { let solver = BruteForce::new(); let best = solver.find_all_best(&problem); - assert_eq!(best.len(), 3); // Three single-vertex solutions + assert_eq!(best.len(), 3); for sol in &best { assert_eq!(sol.iter().sum::(), 1); assert!(problem.evaluate(sol).is_valid()); @@ -265,7 +223,6 @@ fn test_solver_with_real_sat() { use crate::models::formula::{CNFClause, Satisfiability}; use crate::traits::Problem; - // (x1 OR x2) AND (NOT x1 OR NOT x2) let problem = Satisfiability::new( 2, vec![CNFClause::new(vec![1, 2]), CNFClause::new(vec![-1, -2])], diff --git a/src/unit_tests/traits.rs b/src/unit_tests/traits.rs index 6fadbaa2c..cedb4325b 100644 --- a/src/unit_tests/traits.rs +++ b/src/unit_tests/traits.rs @@ -1,7 +1,5 @@ -use crate::traits::{OptimizationProblem, Problem}; -use crate::types::{Direction, SolutionSize}; - -// === Problem trait tests === +use crate::traits::Problem; +use crate::types::{Max, Min, Or, Sum}; #[derive(Clone)] struct TestSatProblem { @@ -11,13 +9,16 @@ struct TestSatProblem { impl Problem for TestSatProblem { const NAME: &'static str = "TestSat"; - type Metric = bool; + type Value = Or; + fn dims(&self) -> Vec { vec![2; self.num_vars] } - fn evaluate(&self, config: &[usize]) -> bool { - self.satisfying.iter().any(|s| s == config) + + fn evaluate(&self, config: &[usize]) -> Self::Value { + Or(self.satisfying.iter().any(|s| s == config)) } + fn variant() -> Vec<(&'static str, &'static str)> { vec![("graph", "SimpleGraph"), ("weight", "bool")] } @@ -29,9 +30,10 @@ fn test_problem_sat() { num_vars: 2, satisfying: vec![vec![1, 0], vec![0, 1]], }; + assert_eq!(p.dims(), vec![2, 2]); - assert!(p.evaluate(&[1, 0])); - assert!(!p.evaluate(&[0, 0])); + assert_eq!(p.evaluate(&[1, 0]), Or(true)); + assert_eq!(p.evaluate(&[0, 0]), Or(false)); } #[test] @@ -40,6 +42,7 @@ fn test_problem_num_variables() { num_vars: 5, satisfying: vec![], }; + assert_eq!(p.num_variables(), 5); assert_eq!(p.dims().len(), 5); } @@ -50,12 +53,11 @@ fn test_problem_empty() { num_vars: 0, satisfying: vec![], }; + assert_eq!(p.num_variables(), 0); assert!(p.dims().is_empty()); } -// === OptimizationProblem trait tests === - #[derive(Clone)] struct TestMaxProblem { weights: Vec, @@ -63,31 +65,27 @@ struct TestMaxProblem { impl Problem for TestMaxProblem { const NAME: &'static str = "TestMax"; - type Metric = SolutionSize; + type Value = Max; + fn dims(&self) -> Vec { vec![2; self.weights.len()] } - fn evaluate(&self, config: &[usize]) -> SolutionSize { - SolutionSize::Valid( + + fn evaluate(&self, config: &[usize]) -> Self::Value { + Max(Some( config .iter() .enumerate() .map(|(i, &v)| if v == 1 { self.weights[i] } else { 0 }) .sum(), - ) + )) } + fn variant() -> Vec<(&'static str, &'static str)> { vec![("graph", "SimpleGraph"), ("weight", "i32")] } } -impl OptimizationProblem for TestMaxProblem { - type Value = i32; - fn direction(&self) -> Direction { - Direction::Maximize - } -} - #[derive(Clone)] struct TestMinProblem { costs: Vec, @@ -95,54 +93,48 @@ struct TestMinProblem { impl Problem for TestMinProblem { const NAME: &'static str = "TestMin"; - type Metric = SolutionSize; + type Value = Min; + fn dims(&self) -> Vec { vec![2; self.costs.len()] } - fn evaluate(&self, config: &[usize]) -> SolutionSize { - SolutionSize::Valid( + + fn evaluate(&self, config: &[usize]) -> Self::Value { + Min(Some( config .iter() .enumerate() .map(|(i, &v)| if v == 1 { self.costs[i] } else { 0 }) .sum(), - ) + )) } + fn variant() -> Vec<(&'static str, &'static str)> { vec![("graph", "SimpleGraph"), ("weight", "i32")] } } -impl OptimizationProblem for TestMinProblem { - type Value = i32; - fn direction(&self) -> Direction { - Direction::Minimize - } -} - #[test] -fn test_optimization_problem_maximize() { +fn test_problem_max_value() { let p = TestMaxProblem { weights: vec![3, 1, 4], }; - assert_eq!(p.evaluate(&[1, 0, 1]), SolutionSize::Valid(7)); - assert_eq!(p.evaluate(&[0, 0, 0]), SolutionSize::Valid(0)); - assert_eq!(p.evaluate(&[1, 1, 1]), SolutionSize::Valid(8)); - assert_eq!(p.direction(), Direction::Maximize); + + assert_eq!(p.evaluate(&[1, 0, 1]), Max(Some(7))); + assert_eq!(p.evaluate(&[0, 0, 0]), Max(Some(0))); + assert_eq!(p.evaluate(&[1, 1, 1]), Max(Some(8))); } #[test] -fn test_optimization_problem_minimize() { +fn test_problem_min_value() { let p = TestMinProblem { costs: vec![5, 2, 3], }; - assert_eq!(p.evaluate(&[1, 0, 0]), SolutionSize::Valid(5)); - assert_eq!(p.evaluate(&[0, 1, 1]), SolutionSize::Valid(5)); - assert_eq!(p.evaluate(&[0, 0, 0]), SolutionSize::Valid(0)); - assert_eq!(p.direction(), Direction::Minimize); -} -// === Multi-dimension (non-binary) problems === + assert_eq!(p.evaluate(&[1, 0, 0]), Min(Some(5))); + assert_eq!(p.evaluate(&[0, 1, 1]), Min(Some(5))); + assert_eq!(p.evaluate(&[0, 0, 0]), Min(Some(0))); +} #[derive(Clone)] struct MultiDimProblem { @@ -151,13 +143,16 @@ struct MultiDimProblem { impl Problem for MultiDimProblem { const NAME: &'static str = "MultiDim"; - type Metric = i32; + type Value = Sum; + fn dims(&self) -> Vec { self.dims.clone() } - fn evaluate(&self, config: &[usize]) -> i32 { - config.iter().map(|&c| c as i32).sum() + + fn evaluate(&self, config: &[usize]) -> Self::Value { + Sum(config.iter().map(|&c| c as i32).sum()) } + fn variant() -> Vec<(&'static str, &'static str)> { vec![("graph", "SimpleGraph"), ("weight", "i32")] } @@ -165,18 +160,16 @@ impl Problem for MultiDimProblem { #[test] fn test_multi_dim_problem() { - // 3 variables with cardinalities [2, 3, 4] let p = MultiDimProblem { dims: vec![2, 3, 4], }; + assert_eq!(p.dims(), vec![2, 3, 4]); assert_eq!(p.num_variables(), 3); - assert_eq!(p.evaluate(&[0, 0, 0]), 0); - assert_eq!(p.evaluate(&[1, 2, 3]), 6); + assert_eq!(p.evaluate(&[0, 0, 0]), Sum(0)); + assert_eq!(p.evaluate(&[1, 2, 3]), Sum(6)); } -// === Problem NAME constant === - #[test] fn test_problem_name() { assert_eq!(TestSatProblem::NAME, "TestSat"); @@ -185,8 +178,6 @@ fn test_problem_name() { assert_eq!(MultiDimProblem::NAME, "MultiDim"); } -// === Problem with f64 metric === - #[derive(Clone)] struct FloatProblem { weights: Vec, @@ -194,44 +185,38 @@ struct FloatProblem { impl Problem for FloatProblem { const NAME: &'static str = "FloatProblem"; - type Metric = SolutionSize; + type Value = Max; + fn dims(&self) -> Vec { vec![2; self.weights.len()] } - fn evaluate(&self, config: &[usize]) -> SolutionSize { - SolutionSize::Valid( + + fn evaluate(&self, config: &[usize]) -> Self::Value { + Max(Some( config .iter() .enumerate() .map(|(i, &v)| if v == 1 { self.weights[i] } else { 0.0 }) .sum(), - ) + )) } + fn variant() -> Vec<(&'static str, &'static str)> { vec![("graph", "SimpleGraph"), ("weight", "f64")] } } -impl OptimizationProblem for FloatProblem { - type Value = f64; - fn direction(&self) -> Direction { - Direction::Maximize - } -} - #[test] -fn test_float_metric_problem() { +fn test_float_value_problem() { let p = FloatProblem { weights: vec![1.5, 2.5, 3.0], }; + assert_eq!(p.dims(), vec![2, 2, 2]); - assert!((p.evaluate(&[1, 1, 0]).unwrap() - 4.0).abs() < 1e-10); - assert!((p.evaluate(&[1, 1, 1]).unwrap() - 7.0).abs() < 1e-10); - assert_eq!(p.direction(), Direction::Maximize); + assert!((p.evaluate(&[1, 1, 0]).0.unwrap() - 4.0).abs() < 1e-10); + assert!((p.evaluate(&[1, 1, 1]).0.unwrap() - 7.0).abs() < 1e-10); } -// === Catalog bridge === - #[test] fn problem_type_bridge_returns_catalog_entry_for_registered_type() { use crate::models::graph::MaximumIndependentSet; @@ -243,8 +228,6 @@ fn problem_type_bridge_returns_catalog_entry_for_registered_type() { assert!(!pt.dimensions.is_empty()); } -// === Clone constraint === - #[test] fn test_problem_is_clone() { let p1 = TestSatProblem { @@ -252,6 +235,7 @@ fn test_problem_is_clone() { satisfying: vec![vec![1, 0]], }; let p2 = p1.clone(); + assert_eq!(p2.dims(), vec![2, 2]); - assert!(p2.evaluate(&[1, 0])); + assert_eq!(p2.evaluate(&[1, 0]), Or(true)); } From 615da6a3a39da7dc98a1fedabba22e7000816ac9 Mon Sep 17 00:00:00 2001 From: GiggleLiu Date: Mon, 23 Mar 2026 10:11:11 +0800 Subject: [PATCH 03/18] refactor: split dynamic value solve from witness solve --- problemreductions-macros/src/lib.rs | 83 ++++++++++++++++++++++++----- src/registry/dyn_problem.rs | 38 ++++++++++--- src/registry/mod.rs | 8 ++- src/registry/variant.rs | 8 +-- src/unit_tests/example_db.rs | 2 +- src/unit_tests/registry/dispatch.rs | 79 ++++++++++++++++++++++++--- 6 files changed, 185 insertions(+), 33 deletions(-) diff --git a/problemreductions-macros/src/lib.rs b/problemreductions-macros/src/lib.rs index 0d790967f..6e895f3e8 100644 --- a/problemreductions-macros/src/lib.rs +++ b/problemreductions-macros/src/lib.rs @@ -557,10 +557,36 @@ fn generate_declare_variants(input: &DeclareVariantsInput) -> syn::Result quote! { - let config = ::find_best(&solver, p)?; + if let Some(config) = + ::find_best(&solver, p) + { + format!("{:?}", crate::traits::Problem::evaluate(p, &config)) + } else { + let invalid: <#ty as crate::traits::Problem>::Value = + crate::types::SolutionSize::Invalid; + format!("{:?}", invalid) + } + }, + SolverKind::Sat => quote! { + if let Some(config) = + ::find_satisfying( + &solver, p, + ) + { + format!("{:?}", crate::traits::Problem::evaluate(p, &config)) + } else { + format!("{:?}", false) + } + }, + }; + + let solve_witness_body = match entry.solver_kind { + SolverKind::Opt => quote! { + let config = + ::find_best(&solver, p)?; }, SolverKind::Sat => quote! { let config = ::find_satisfying(&solver, p)?; @@ -576,10 +602,17 @@ fn generate_declare_variants(input: &DeclareVariantsInput) -> syn::Result()?; Some(serde_json::to_value(p).expect("serialize failed")) }, - solve_fn: |any: &dyn std::any::Any| -> Option<(Vec, String)> { + solve_value_fn: |any: &dyn std::any::Any| -> String { + let p = any + .downcast_ref::<#ty>() + .expect("type-erased solve_value downcast failed"); + let solver = crate::solvers::BruteForce::new(); + #solve_value_body + }, + solve_witness_fn: |any: &dyn std::any::Any| -> Option<(Vec, String)> { let p = any.downcast_ref::<#ty>()?; let solver = crate::solvers::BruteForce::new(); - #solve_body + #solve_witness_body let evaluation = format!("{:?}", crate::traits::Problem::evaluate(p, &config)); Some((config, evaluation)) }, @@ -721,7 +754,14 @@ mod tests { tokens.contains("serialize_fn :"), "expected serialize_fn field" ); - assert!(tokens.contains("solve_fn :"), "expected solve_fn field"); + assert!( + tokens.contains("solve_value_fn :"), + "expected solve_value_fn field" + ); + assert!( + tokens.contains("solve_witness_fn :"), + "expected solve_witness_fn field" + ); assert!( !tokens.contains("factory : None"), "factory should not be None" @@ -731,8 +771,12 @@ mod tests { "serialize_fn should not be None" ); assert!( - !tokens.contains("solve_fn : None"), - "solve_fn should not be None" + !tokens.contains("solve_value_fn : None"), + "solve_value_fn should not be None" + ); + assert!( + !tokens.contains("solve_witness_fn : None"), + "solve_witness_fn should not be None" ); assert!(tokens.contains("find_best"), "expected find_best in tokens"); } @@ -748,7 +792,14 @@ mod tests { tokens.contains("serialize_fn :"), "expected serialize_fn field" ); - assert!(tokens.contains("solve_fn :"), "expected solve_fn field"); + assert!( + tokens.contains("solve_value_fn :"), + "expected solve_value_fn field" + ); + assert!( + tokens.contains("solve_witness_fn :"), + "expected solve_witness_fn field" + ); assert!( !tokens.contains("factory : None"), "factory should not be None" @@ -758,8 +809,12 @@ mod tests { "serialize_fn should not be None" ); assert!( - !tokens.contains("solve_fn : None"), - "solve_fn should not be None" + !tokens.contains("solve_value_fn : None"), + "solve_value_fn should not be None" + ); + assert!( + !tokens.contains("solve_witness_fn : None"), + "solve_witness_fn should not be None" ); assert!( tokens.contains("find_satisfying"), @@ -796,9 +851,11 @@ mod tests { let tokens = generate_declare_variants(&input).unwrap().to_string(); assert!(tokens.contains("factory :")); assert!(tokens.contains("serialize_fn :")); - assert!(tokens.contains("solve_fn :")); + assert!(tokens.contains("solve_value_fn :")); + assert!(tokens.contains("solve_witness_fn :")); assert!(!tokens.contains("factory : None")); assert!(!tokens.contains("serialize_fn : None")); - assert!(!tokens.contains("solve_fn : None")); + assert!(!tokens.contains("solve_value_fn : None")); + assert!(!tokens.contains("solve_witness_fn : None")); } } diff --git a/src/registry/dyn_problem.rs b/src/registry/dyn_problem.rs index 92f0f0005..0f3acf1c3 100644 --- a/src/registry/dyn_problem.rs +++ b/src/registry/dyn_problem.rs @@ -66,15 +66,19 @@ where } } -/// Function pointer type for brute-force solve dispatch. -pub type SolveFn = fn(&dyn Any) -> Option<(Vec, String)>; +/// Function pointer type for brute-force value solve dispatch. +pub type SolveValueFn = fn(&dyn Any) -> String; + +/// Function pointer type for brute-force witness solve dispatch. +pub type SolveWitnessFn = fn(&dyn Any) -> Option<(Vec, String)>; /// A loaded problem with type-erased solve capability. /// -/// Wraps a `Box` with a brute-force solve function pointer. +/// Wraps a `Box` with brute-force value and witness function pointers. pub struct LoadedDynProblem { inner: Box, - solve_fn: SolveFn, + solve_value_fn: SolveValueFn, + solve_witness_fn: SolveWitnessFn, } impl std::fmt::Debug for LoadedDynProblem { @@ -87,13 +91,31 @@ impl std::fmt::Debug for LoadedDynProblem { impl LoadedDynProblem { /// Create a new loaded dynamic problem. - pub fn new(inner: Box, solve_fn: SolveFn) -> Self { - Self { inner, solve_fn } + pub fn new( + inner: Box, + solve_value_fn: SolveValueFn, + solve_witness_fn: SolveWitnessFn, + ) -> Self { + Self { + inner, + solve_value_fn, + solve_witness_fn, + } + } + + /// Solve the problem using brute force and return its aggregate value string. + pub fn solve_brute_force_value(&self) -> String { + (self.solve_value_fn)(self.inner.as_any()) + } + + /// Solve the problem using brute force and return a witness when available. + pub fn solve_brute_force_witness(&self) -> Option<(Vec, String)> { + (self.solve_witness_fn)(self.inner.as_any()) } - /// Solve the problem using brute force. + /// Backward-compatible witness solve entry point. pub fn solve_brute_force(&self) -> Option<(Vec, String)> { - (self.solve_fn)(self.inner.as_any()) + self.solve_brute_force_witness() } } diff --git a/src/registry/mod.rs b/src/registry/mod.rs index d72609729..1c8c64570 100644 --- a/src/registry/mod.rs +++ b/src/registry/mod.rs @@ -51,7 +51,7 @@ pub mod problem_type; mod schema; pub mod variant; -pub use dyn_problem::{DynProblem, LoadedDynProblem, SolveFn}; +pub use dyn_problem::{DynProblem, LoadedDynProblem, SolveValueFn, SolveWitnessFn}; pub use info::{ComplexityClass, FieldInfo, ProblemInfo, ProblemMetadata}; 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}; @@ -81,7 +81,11 @@ pub fn load_dyn( let inner = (entry.factory)(data).map_err(|e| format!("Failed to deserialize `{name}`: {e}"))?; - Ok(LoadedDynProblem::new(inner, entry.solve_fn)) + Ok(LoadedDynProblem::new( + inner, + entry.solve_value_fn, + entry.solve_witness_fn, + )) } /// Serialize a `&dyn Any` by exact problem name and exact variant map. diff --git a/src/registry/variant.rs b/src/registry/variant.rs index a4e6fd35a..4bca0369e 100644 --- a/src/registry/variant.rs +++ b/src/registry/variant.rs @@ -3,7 +3,7 @@ use std::any::Any; use std::collections::BTreeMap; -use crate::registry::dyn_problem::{DynProblem, SolveFn}; +use crate::registry::dyn_problem::{DynProblem, SolveValueFn, SolveWitnessFn}; /// A registered problem variant entry. /// @@ -26,8 +26,10 @@ pub struct VariantEntry { pub factory: fn(serde_json::Value) -> Result, serde_json::Error>, /// Serialize: downcast `&dyn Any` and serialize to JSON. pub serialize_fn: fn(&dyn Any) -> Option, - /// Solve: downcast `&dyn Any` and brute-force solve. - pub solve_fn: SolveFn, + /// Solve value: downcast `&dyn Any` and brute-force solve to an aggregate string. + pub solve_value_fn: SolveValueFn, + /// Solve witness: downcast `&dyn Any` and brute-force recover a witness when available. + pub solve_witness_fn: SolveWitnessFn, } impl VariantEntry { diff --git a/src/unit_tests/example_db.rs b/src/unit_tests/example_db.rs index adca8f825..2abe80d6c 100644 --- a/src/unit_tests/example_db.rs +++ b/src/unit_tests/example_db.rs @@ -468,7 +468,7 @@ fn model_specs_are_optimal() { return None; } let entry = find_variant_entry(name, &variant)?; - let (config, _) = (entry.solve_fn)(spec.instance.as_any())?; + let (config, _) = (entry.solve_witness_fn)(spec.instance.as_any())?; Some(config) }) .unwrap_or_else(|| { diff --git a/src/unit_tests/registry/dispatch.rs b/src/unit_tests/registry/dispatch.rs index bb8889c77..931eb2b46 100644 --- a/src/unit_tests/registry/dispatch.rs +++ b/src/unit_tests/registry/dispatch.rs @@ -3,17 +3,64 @@ use crate::models::misc::SubsetSum; use crate::registry::variant::find_variant_entry; use crate::registry::{load_dyn, serialize_any, DynProblem, LoadedDynProblem}; use crate::topology::SimpleGraph; +use crate::types::Sum; use crate::{Problem, Solver}; use std::any::Any; use std::collections::BTreeMap; -fn solve_subset_sum(any: &dyn Any) -> Option<(Vec, String)> { +fn solve_subset_sum_value(any: &dyn Any) -> String { + let p = any.downcast_ref::().unwrap(); + if let Some(config) = crate::BruteForce::new().find_satisfying(p) { + format!("{:?}", p.evaluate(&config)) + } else { + "false".to_string() + } +} + +fn solve_subset_sum_witness(any: &dyn Any) -> Option<(Vec, String)> { let p = any.downcast_ref::()?; let config = crate::BruteForce::new().find_satisfying(p)?; let eval = format!("{:?}", p.evaluate(&config)); Some((config, eval)) } +#[derive(Clone, serde::Serialize)] +struct AggregateOnlyProblem { + weights: Vec, +} + +impl Problem for AggregateOnlyProblem { + const NAME: &'static str = "AggregateOnlyProblem"; + type Value = Sum; + + fn dims(&self) -> Vec { + vec![2; self.weights.len()] + } + + fn evaluate(&self, config: &[usize]) -> Self::Value { + Sum( + config + .iter() + .zip(&self.weights) + .map(|(&c, &w)| if c == 1 { w } else { 0 }) + .sum(), + ) + } + + fn variant() -> Vec<(&'static str, &'static str)> { + vec![] + } +} + +fn solve_aggregate_value(any: &dyn Any) -> String { + let p = any.downcast_ref::().unwrap(); + format!("{:?}", crate::BruteForce::new().solve(p)) +} + +fn solve_aggregate_witness(_: &dyn Any) -> Option<(Vec, String)> { + None +} + #[test] fn test_dyn_problem_blanket_impl_exposes_problem_metadata() { let problem = MaximumIndependentSet::new(SimpleGraph::new(3, vec![(0, 1)]), vec![1i32; 3]); @@ -27,16 +74,33 @@ fn test_dyn_problem_blanket_impl_exposes_problem_metadata() { } #[test] -fn test_loaded_dyn_problem_delegates_to_solve_fn() { +fn test_loaded_dyn_problem_delegates_to_value_and_witness_fns() { let problem = SubsetSum::new(vec![3u32, 7u32, 1u32], 4u32); - let loaded = LoadedDynProblem::new(Box::new(problem), solve_subset_sum); + let loaded = + LoadedDynProblem::new(Box::new(problem), solve_subset_sum_value, solve_subset_sum_witness); + + assert_eq!(loaded.solve_brute_force_value(), "true"); let solved = loaded - .solve_brute_force() + .solve_brute_force_witness() .expect("expected satisfying solution"); assert_eq!(solved.1, "true"); assert_eq!(solved.0.len(), 3); } +#[test] +fn loaded_dyn_problem_returns_none_for_aggregate_only_witness() { + let loaded = LoadedDynProblem::new( + Box::new(AggregateOnlyProblem { + weights: vec![1, 2, 4], + }), + solve_aggregate_value, + solve_aggregate_witness, + ); + + assert_eq!(loaded.solve_brute_force_value(), "Sum(28)"); + assert!(loaded.solve_brute_force_witness().is_none()); +} + #[test] fn test_find_variant_entry_requires_exact_variant() { let partial = BTreeMap::from([("graph".to_string(), "SimpleGraph".to_string())]); @@ -62,7 +126,8 @@ fn test_load_dyn_round_trips_maximum_independent_set() { loaded.serialize_json(), serde_json::to_value(&problem).unwrap() ); - assert!(loaded.solve_brute_force().is_some()); + assert!(!loaded.solve_brute_force_value().is_empty()); + assert!(loaded.solve_brute_force_witness().is_some()); } #[test] @@ -75,7 +140,9 @@ fn test_load_dyn_solves_subset_sum() { serde_json::to_value(&problem).unwrap(), ) .unwrap(); - let solved = loaded.solve_brute_force().unwrap(); + + assert_eq!(loaded.solve_brute_force_value(), "true"); + let solved = loaded.solve_brute_force_witness().unwrap(); assert_eq!(solved.1, "true"); } From 45bb1ef727a0eec91de1a2d118ac23959459e86f Mon Sep 17 00:00:00 2001 From: GiggleLiu Date: Mon, 23 Mar 2026 10:18:54 +0800 Subject: [PATCH 04/18] refactor: add aggregate reduction execution --- problemreductions-macros/src/lib.rs | 5 +- src/rules/graph.rs | 76 ++++++++++- src/rules/mod.rs | 8 +- src/rules/registry.rs | 15 +- src/rules/traits.rs | 72 ++++++++++ src/unit_tests/rules/graph.rs | 205 +++++++++++++++++++++++++++- src/unit_tests/rules/registry.rs | 45 +++++- src/unit_tests/rules/traits.rs | 102 +++++++++++++- 8 files changed, 507 insertions(+), 21 deletions(-) diff --git a/problemreductions-macros/src/lib.rs b/problemreductions-macros/src/lib.rs index 6e895f3e8..fb16b1567 100644 --- a/problemreductions-macros/src/lib.rs +++ b/problemreductions-macros/src/lib.rs @@ -319,7 +319,7 @@ fn generate_reduction_entry( target_variant_fn: || { #target_variant_body }, overhead_fn: || { #overhead }, module_path: module_path!(), - reduce_fn: |src: &dyn std::any::Any| -> Box { + reduce_fn: Some(|src: &dyn std::any::Any| -> Box { let src = src.downcast_ref::<#source_type>().unwrap_or_else(|| { panic!( "DynReductionResult: source type mismatch: expected `{}`, got `{}`", @@ -328,7 +328,8 @@ fn generate_reduction_entry( ) }); Box::new(<#source_type as crate::rules::ReduceTo<#target_type>>::reduce_to(src)) - }, + }), + reduce_aggregate_fn: None, overhead_eval_fn: #overhead_eval_fn, } } diff --git a/src/rules/graph.rs b/src/rules/graph.rs index fff45b23b..7375c5798 100644 --- a/src/rules/graph.rs +++ b/src/rules/graph.rs @@ -13,8 +13,10 @@ //! - JSON export for documentation and visualization use crate::rules::cost::PathCostFn; -use crate::rules::registry::{ReductionEntry, ReductionOverhead}; -use crate::rules::traits::DynReductionResult; +use crate::rules::registry::{ + AggregateReduceFn, ReduceFn, ReductionEntry, ReductionOverhead, +}; +use crate::rules::traits::{DynAggregateReductionResult, DynReductionResult}; use crate::types::ProblemSize; use ordered_float::OrderedFloat; use petgraph::algo::all_simple_paths; @@ -40,7 +42,8 @@ pub struct ReductionEdgeInfo { #[derive(Clone)] pub(crate) struct ReductionEdgeData { pub overhead: ReductionOverhead, - pub reduce_fn: fn(&dyn Any) -> Box, + pub reduce_fn: Option, + pub reduce_aggregate_fn: Option, } /// JSON-serializable representation of the reduction graph. @@ -361,6 +364,7 @@ impl ReductionGraph { ReductionEdgeData { overhead, reduce_fn: entry.reduce_fn, + reduce_aggregate_fn: entry.reduce_aggregate_fn, }, ); } @@ -1184,6 +1188,39 @@ impl ReductionChain { } } +/// 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() + } + + /// 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") + } + + /// 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)) + } +} + impl ReductionGraph { /// Execute a reduction path on a source problem instance. /// @@ -1212,7 +1249,7 @@ impl ReductionGraph { let src = self.lookup_node(&window[0].name, &window[0].variant)?; let dst = self.lookup_node(&window[1].name, &window[1].variant)?; let edge_idx = self.graph.find_edge(src, dst)?; - edge_fns.push(self.graph[edge_idx].reduce_fn); + edge_fns.push(self.graph[edge_idx].reduce_fn?); } // Execute the chain let mut steps: Vec> = Vec::new(); @@ -1227,6 +1264,37 @@ impl ReductionGraph { } Some(ReductionChain { steps }) } + + /// Execute an aggregate-value reduction path on a source problem instance. + pub fn reduce_aggregate_along_path( + &self, + path: &ReductionPath, + source: &dyn Any, + ) -> Option { + if path.steps.len() < 2 { + return None; + } + + let mut edge_fns = Vec::new(); + for window in path.steps.windows(2) { + let src = self.lookup_node(&window[0].name, &window[0].variant)?; + let dst = self.lookup_node(&window[1].name, &window[1].variant)?; + let edge_idx = self.graph.find_edge(src, dst)?; + edge_fns.push(self.graph[edge_idx].reduce_aggregate_fn?); + } + + 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); + } + Some(AggregateReductionChain { steps }) + } } #[cfg(test)] diff --git a/src/rules/mod.rs b/src/rules/mod.rs index c3971e0f7..b727ce979 100644 --- a/src/rules/mod.rs +++ b/src/rules/mod.rs @@ -97,10 +97,12 @@ pub(crate) mod steinertree_ilp; pub(crate) mod travelingsalesman_ilp; pub use graph::{ - NeighborInfo, NeighborTree, ReductionChain, ReductionEdgeInfo, ReductionGraph, ReductionPath, - ReductionStep, TraversalDirection, + AggregateReductionChain, NeighborInfo, NeighborTree, ReductionChain, ReductionEdgeInfo, + ReductionGraph, ReductionPath, ReductionStep, TraversalDirection, +}; +pub use traits::{ + AggregateReductionResult, ReduceTo, ReduceToAggregate, ReductionAutoCast, ReductionResult, }; -pub use traits::{ReduceTo, ReductionAutoCast, ReductionResult}; #[cfg(feature = "example-db")] pub(crate) fn canonical_rule_example_specs() -> Vec { diff --git a/src/rules/registry.rs b/src/rules/registry.rs index a61dd7852..680f36267 100644 --- a/src/rules/registry.rs +++ b/src/rules/registry.rs @@ -1,7 +1,7 @@ //! Automatic reduction registration via inventory. use crate::expr::Expr; -use crate::rules::traits::DynReductionResult; +use crate::rules::traits::{DynAggregateReductionResult, DynReductionResult}; use crate::types::ProblemSize; use std::any::Any; use std::collections::HashSet; @@ -83,6 +83,12 @@ impl ReductionOverhead { } } +/// Witness/config reduction executor stored in the inventory. +pub type ReduceFn = fn(&dyn Any) -> Box; + +/// Aggregate/value reduction executor stored in the inventory. +pub type AggregateReduceFn = fn(&dyn Any) -> Box; + /// A registered reduction entry for static inventory registration. /// Uses function pointers to lazily derive variant fields from `Problem::variant()`. pub struct ReductionEntry { @@ -101,7 +107,12 @@ pub struct ReductionEntry { /// Type-erased reduction executor. /// Takes a `&dyn Any` (must be `&SourceType`), calls `ReduceTo::reduce_to()`, /// and returns the result as a boxed `DynReductionResult`. - pub reduce_fn: fn(&dyn Any) -> Box, + pub reduce_fn: Option, + /// Type-erased aggregate reduction executor. + /// Takes a `&dyn Any` (must be `&SourceType`), calls + /// `ReduceToAggregate::reduce_to_aggregate()`, and returns the result as a + /// boxed `DynAggregateReductionResult`. + pub reduce_aggregate_fn: Option, /// Compiled overhead evaluation function. /// Takes a `&dyn Any` (must be `&SourceType`), calls getter methods directly, /// and returns the computed target problem size. diff --git a/src/rules/traits.rs b/src/rules/traits.rs index c4b672353..2a1df4281 100644 --- a/src/rules/traits.rs +++ b/src/rules/traits.rs @@ -1,6 +1,8 @@ //! Core traits for problem reductions. use crate::traits::Problem; +use serde::de::DeserializeOwned; +use serde::Serialize; use std::any::Any; use std::marker::PhantomData; @@ -62,6 +64,36 @@ pub trait ReduceTo: Problem { fn reduce_to(&self) -> 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) -> Self::Result; +} + /// Generic reduction result for natural-edge (subtype) reductions. /// /// Used when a problem on a specific graph type is trivially reducible to @@ -97,6 +129,19 @@ impl ReductionResult for ReductionAutoCast { } } +impl> AggregateReductionResult for ReductionAutoCast { + type Source = S; + type Target = T; + + fn target_problem(&self) -> &Self::Target { + &self.target + } + + fn extract_value(&self, target_value: T::Value) -> S::Value { + target_value + } +} + /// Type-erased reduction result for runtime-discovered paths. /// /// Implemented automatically for all `ReductionResult` types via blanket impl. @@ -120,6 +165,33 @@ where } } +/// 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 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") + } +} + #[cfg(test)] #[path = "../unit_tests/rules/traits.rs"] mod tests; diff --git a/src/unit_tests/rules/graph.rs b/src/unit_tests/rules/graph.rs index a7c5d9326..389d45aed 100644 --- a/src/unit_tests/rules/graph.rs +++ b/src/unit_tests/rules/graph.rs @@ -6,10 +6,128 @@ use crate::models::set::MaximumSetPacking; use crate::rules::cost::{Minimize, MinimizeSteps}; use crate::rules::graph::{classify_problem_category, ReductionStep}; use crate::rules::registry::ReductionEntry; +use crate::rules::traits::AggregateReductionResult; use crate::topology::SimpleGraph; use crate::traits::Problem; -use crate::types::{One, ProblemSize}; -use std::collections::BTreeMap; +use crate::types::{One, ProblemSize, Sum}; +use petgraph::graph::DiGraph; +use serde_json::json; +use std::any::Any; +use std::collections::{BTreeMap, HashMap}; + +#[derive(Clone)] +struct AggregateChainSource; + +#[derive(Clone)] +struct AggregateChainMiddle; + +#[derive(Clone)] +struct AggregateChainTarget; + +impl Problem for AggregateChainSource { + const NAME: &'static str = "AggregateChainSource"; + type Value = Sum; + + fn dims(&self) -> Vec { + vec![1] + } + + fn evaluate(&self, config: &[usize]) -> Self::Value { + Sum(config.iter().sum::() as u64) + } + + fn variant() -> Vec<(&'static str, &'static str)> { + vec![] + } +} + +impl Problem for AggregateChainMiddle { + const NAME: &'static str = "AggregateChainMiddle"; + type Value = Sum; + + fn dims(&self) -> Vec { + vec![1] + } + + fn evaluate(&self, config: &[usize]) -> Self::Value { + Sum(config.iter().sum::() as u64) + } + + fn variant() -> Vec<(&'static str, &'static str)> { + vec![] + } +} + +impl Problem for AggregateChainTarget { + const NAME: &'static str = "AggregateChainTarget"; + type Value = Sum; + + fn dims(&self) -> Vec { + vec![1] + } + + fn evaluate(&self, config: &[usize]) -> Self::Value { + Sum(config.iter().sum::() as u64) + } + + fn variant() -> Vec<(&'static str, &'static str)> { + vec![] + } +} + +struct SourceToMiddleAggregateResult { + target: AggregateChainMiddle, +} + +impl AggregateReductionResult for SourceToMiddleAggregateResult { + type Source = AggregateChainSource; + type Target = AggregateChainMiddle; + + fn target_problem(&self) -> &Self::Target { + &self.target + } + + fn extract_value(&self, target_value: Sum) -> Sum { + Sum(target_value.0 + 2) + } +} + +struct MiddleToTargetAggregateResult { + target: AggregateChainTarget, +} + +impl AggregateReductionResult for MiddleToTargetAggregateResult { + type Source = AggregateChainMiddle; + type Target = AggregateChainTarget; + + fn target_problem(&self) -> &Self::Target { + &self.target + } + + fn extract_value(&self, target_value: Sum) -> Sum { + Sum(target_value.0 + 3) + } +} + +fn reduce_source_to_middle_aggregate( + any: &dyn Any, +) -> Box { + any.downcast_ref::() + .expect("expected AggregateChainSource"); + Box::new(SourceToMiddleAggregateResult { + target: AggregateChainMiddle, + }) +} + +fn reduce_middle_to_target_aggregate( + any: &dyn Any, +) -> Box { + any.downcast_ref::() + .expect("expected AggregateChainMiddle"); + Box::new(MiddleToTargetAggregateResult { + target: AggregateChainTarget, + }) +} #[test] fn test_find_direct_path() { @@ -24,6 +142,89 @@ fn test_find_direct_path() { assert_eq!(shortest.len(), 1); // One reduction step } +#[test] +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: AggregateChainSource::NAME, + variant: source_variant.clone(), + complexity: "", + }, + VariantNode { + name: AggregateChainMiddle::NAME, + variant: middle_variant.clone(), + complexity: "", + }, + VariantNode { + name: AggregateChainTarget::NAME, + variant: target_variant.clone(), + complexity: "", + }, + ]; + + let mut graph = DiGraph::new(); + let source_idx = graph.add_node(0); + let middle_idx = graph.add_node(1); + let target_idx = graph.add_node(2); + + graph.add_edge( + source_idx, + middle_idx, + ReductionEdgeData { + overhead: crate::rules::registry::ReductionOverhead::default(), + reduce_fn: None, + reduce_aggregate_fn: Some(reduce_source_to_middle_aggregate), + }, + ); + graph.add_edge( + middle_idx, + target_idx, + ReductionEdgeData { + overhead: crate::rules::registry::ReductionOverhead::default(), + reduce_fn: None, + reduce_aggregate_fn: Some(reduce_middle_to_target_aggregate), + }, + ); + + let reduction_graph = ReductionGraph { + graph, + nodes, + name_to_nodes: HashMap::from([ + (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: AggregateChainSource::NAME.to_string(), + variant: source_variant, + }, + ReductionStep { + name: AggregateChainMiddle::NAME.to_string(), + variant: middle_variant, + }, + ReductionStep { + name: AggregateChainTarget::NAME.to_string(), + variant: target_variant, + }, + ], + }; + + let chain = reduction_graph + .reduce_aggregate_along_path(&path, &AggregateChainSource as &dyn Any) + .expect("expected aggregate reduction chain"); + + assert_eq!(chain.target_problem::().dims(), vec![1]); + assert_eq!(chain.extract_value_dyn(json!(7)), json!(12)); +} + #[test] fn test_find_indirect_path() { let graph = ReductionGraph::new(); diff --git a/src/unit_tests/rules/registry.rs b/src/unit_tests/rules/registry.rs index c6b0f4cbd..179abe2a2 100644 --- a/src/unit_tests/rules/registry.rs +++ b/src/unit_tests/rules/registry.rs @@ -7,6 +7,12 @@ fn dummy_reduce_fn(_: &dyn std::any::Any) -> Box Box { + unimplemented!("dummy reduce_aggregate_fn for testing") +} + fn dummy_overhead_eval_fn(_: &dyn std::any::Any) -> ProblemSize { ProblemSize::new(vec![]) } @@ -40,7 +46,8 @@ fn test_reduction_entry_overhead() { target_variant_fn: || vec![("graph", "SimpleGraph"), ("weight", "One")], overhead_fn: || ReductionOverhead::new(vec![("n", Expr::Const(2.0) * Expr::Var("n"))]), module_path: "test::module", - reduce_fn: dummy_reduce_fn, + reduce_fn: Some(dummy_reduce_fn), + reduce_aggregate_fn: None, overhead_eval_fn: dummy_overhead_eval_fn, }; @@ -59,7 +66,8 @@ fn test_reduction_entry_debug() { target_variant_fn: || vec![("graph", "SimpleGraph"), ("weight", "One")], overhead_fn: || ReductionOverhead::default(), module_path: "test::module", - reduce_fn: dummy_reduce_fn, + reduce_fn: Some(dummy_reduce_fn), + reduce_aggregate_fn: None, overhead_eval_fn: dummy_overhead_eval_fn, }; @@ -77,7 +85,8 @@ fn test_is_base_reduction_unweighted() { target_variant_fn: || vec![("graph", "SimpleGraph"), ("weight", "One")], overhead_fn: || ReductionOverhead::default(), module_path: "test::module", - reduce_fn: dummy_reduce_fn, + reduce_fn: Some(dummy_reduce_fn), + reduce_aggregate_fn: None, overhead_eval_fn: dummy_overhead_eval_fn, }; assert!(entry.is_base_reduction()); @@ -92,7 +101,8 @@ fn test_is_base_reduction_source_weighted() { target_variant_fn: || vec![("graph", "SimpleGraph"), ("weight", "One")], overhead_fn: || ReductionOverhead::default(), module_path: "test::module", - reduce_fn: dummy_reduce_fn, + reduce_fn: Some(dummy_reduce_fn), + reduce_aggregate_fn: None, overhead_eval_fn: dummy_overhead_eval_fn, }; assert!(!entry.is_base_reduction()); @@ -107,7 +117,8 @@ fn test_is_base_reduction_target_weighted() { target_variant_fn: || vec![("graph", "SimpleGraph"), ("weight", "f64")], overhead_fn: || ReductionOverhead::default(), module_path: "test::module", - reduce_fn: dummy_reduce_fn, + reduce_fn: Some(dummy_reduce_fn), + reduce_aggregate_fn: None, overhead_eval_fn: dummy_overhead_eval_fn, }; assert!(!entry.is_base_reduction()); @@ -122,7 +133,8 @@ fn test_is_base_reduction_both_weighted() { target_variant_fn: || vec![("graph", "SimpleGraph"), ("weight", "f64")], overhead_fn: || ReductionOverhead::default(), module_path: "test::module", - reduce_fn: dummy_reduce_fn, + reduce_fn: Some(dummy_reduce_fn), + reduce_aggregate_fn: None, overhead_eval_fn: dummy_overhead_eval_fn, }; assert!(!entry.is_base_reduction()); @@ -138,12 +150,31 @@ fn test_is_base_reduction_no_weight_key() { target_variant_fn: || vec![("graph", "SimpleGraph")], overhead_fn: || ReductionOverhead::default(), module_path: "test::module", - reduce_fn: dummy_reduce_fn, + reduce_fn: Some(dummy_reduce_fn), + reduce_aggregate_fn: None, overhead_eval_fn: dummy_overhead_eval_fn, }; assert!(entry.is_base_reduction()); } +#[test] +fn test_reduction_entry_can_store_aggregate_executor() { + let entry = ReductionEntry { + source_name: "A", + target_name: "B", + source_variant_fn: || vec![("graph", "SimpleGraph")], + target_variant_fn: || vec![("graph", "SimpleGraph")], + overhead_fn: || ReductionOverhead::default(), + module_path: "test::module", + reduce_fn: None, + reduce_aggregate_fn: Some(dummy_reduce_aggregate_fn), + overhead_eval_fn: dummy_overhead_eval_fn, + }; + + assert!(entry.reduce_fn.is_none()); + assert!(entry.reduce_aggregate_fn.is_some()); +} + #[test] fn test_reduction_entries_registered() { let entries: Vec<_> = inventory::iter::().collect(); diff --git a/src/unit_tests/rules/traits.rs b/src/unit_tests/rules/traits.rs index 7a273633d..a7c1acd69 100644 --- a/src/unit_tests/rules/traits.rs +++ b/src/unit_tests/rules/traits.rs @@ -3,8 +3,13 @@ fn test_traits_compile() { // Traits should compile - actual tests in reduction implementations } -use crate::rules::traits::{ReduceTo, ReductionResult}; +use crate::rules::traits::{ + AggregateReductionResult, DynAggregateReductionResult, ReduceTo, ReduceToAggregate, + ReductionResult, +}; use crate::traits::Problem; +use crate::types::Sum; +use serde_json::json; #[derive(Clone)] struct SourceProblem; @@ -72,3 +77,98 @@ fn test_reduction() { assert_eq!(target.evaluate(&[1, 1]), 2); assert_eq!(result.extract_solution(&[1, 0]), vec![1, 0]); } + +#[derive(Clone)] +struct AggregateSourceProblem; + +#[derive(Clone)] +struct AggregateTargetProblem; + +impl Problem for AggregateSourceProblem { + const NAME: &'static str = "AggregateSource"; + type Value = Sum; + + fn dims(&self) -> Vec { + vec![2] + } + + fn evaluate(&self, config: &[usize]) -> Self::Value { + Sum(config.iter().sum::() as u64) + } + + fn variant() -> Vec<(&'static str, &'static str)> { + vec![] + } +} + +impl Problem for AggregateTargetProblem { + const NAME: &'static str = "AggregateTarget"; + type Value = Sum; + + fn dims(&self) -> Vec { + vec![2] + } + + fn evaluate(&self, config: &[usize]) -> Self::Value { + Sum(config.iter().sum::() as u64) + } + + fn variant() -> Vec<(&'static str, &'static str)> { + vec![] + } +} + +struct TestAggregateReduction { + target: AggregateTargetProblem, + offset: u64, +} + +impl AggregateReductionResult for TestAggregateReduction { + type Source = AggregateSourceProblem; + type Target = AggregateTargetProblem; + + fn target_problem(&self) -> &Self::Target { + &self.target + } + + fn extract_value(&self, target_value: Sum) -> Sum { + Sum(target_value.0 + self.offset) + } +} + +impl ReduceToAggregate for AggregateSourceProblem { + type Result = TestAggregateReduction; + + fn reduce_to_aggregate(&self) -> Self::Result { + TestAggregateReduction { + target: AggregateTargetProblem, + offset: 3, + } + } +} + +#[test] +fn test_aggregate_reduction_extracts_value() { + let source = AggregateSourceProblem; + let result = + >::reduce_to_aggregate( + &source, + ); + + assert_eq!(result.extract_value(Sum(7)), Sum(10)); +} + +#[test] +fn test_dyn_aggregate_reduction_result_extracts_value() { + let result = TestAggregateReduction { + target: AggregateTargetProblem, + offset: 2, + }; + let dyn_result: &dyn DynAggregateReductionResult = &result; + + assert!(dyn_result + .target_problem_any() + .downcast_ref::() + .is_some()); + assert_eq!(dyn_result.extract_value_dyn(json!(7)), json!(9)); +} From daf0e58a1f3fa18b0251ba4955d5e14460197e7f Mon Sep 17 00:00:00 2001 From: GiggleLiu Date: Mon, 23 Mar 2026 10:35:23 +0800 Subject: [PATCH 05/18] refactor: make reduction paths capability-aware --- problemreductions-macros/src/lib.rs | 6 + src/rules/graph.rs | 199 +++++++++++++++++++++++- src/rules/mod.rs | 4 +- src/rules/registry.rs | 39 +++++ src/unit_tests/reduction_graph.rs | 66 +++++++- src/unit_tests/rules/graph.rs | 233 +++++++++++++++++++++++++++- src/unit_tests/rules/registry.rs | 8 + 7 files changed, 540 insertions(+), 15 deletions(-) diff --git a/problemreductions-macros/src/lib.rs b/problemreductions-macros/src/lib.rs index fb16b1567..acb94a4d6 100644 --- a/problemreductions-macros/src/lib.rs +++ b/problemreductions-macros/src/lib.rs @@ -275,6 +275,11 @@ 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 capabilities = if source_name == target_name { + quote! { crate::rules::EdgeCapabilities::both() } + } else { + quote! { crate::rules::EdgeCapabilities::witness_only() } + }; // Collect generic parameter info from the impl block let type_generics = collect_type_generic_names(&impl_block.generics); @@ -330,6 +335,7 @@ fn generate_reduction_entry( Box::new(<#source_type as crate::rules::ReduceTo<#target_type>>::reduce_to(src)) }), reduce_aggregate_fn: None, + capabilities: #capabilities, overhead_eval_fn: #overhead_eval_fn, } } diff --git a/src/rules/graph.rs b/src/rules/graph.rs index 7375c5798..26548c882 100644 --- a/src/rules/graph.rs +++ b/src/rules/graph.rs @@ -14,13 +14,13 @@ use crate::rules::cost::PathCostFn; use crate::rules::registry::{ - AggregateReduceFn, ReduceFn, ReductionEntry, ReductionOverhead, + AggregateReduceFn, EdgeCapabilities, ReduceFn, ReductionEntry, ReductionOverhead, }; use crate::rules::traits::{DynAggregateReductionResult, DynReductionResult}; use crate::types::ProblemSize; use ordered_float::OrderedFloat; use petgraph::algo::all_simple_paths; -use petgraph::graph::{DiGraph, NodeIndex}; +use petgraph::graph::{DiGraph, EdgeIndex, NodeIndex}; use petgraph::visit::EdgeRef; use serde::Serialize; use std::any::Any; @@ -36,6 +36,7 @@ pub struct ReductionEdgeInfo { pub target_name: &'static str, pub target_variant: BTreeMap, pub overhead: ReductionOverhead, + pub capabilities: EdgeCapabilities, } /// Internal edge data combining overhead and executable reduce function. @@ -44,6 +45,7 @@ pub(crate) struct ReductionEdgeData { pub overhead: ReductionOverhead, pub reduce_fn: Option, pub reduce_aggregate_fn: Option, + pub capabilities: EdgeCapabilities, } /// JSON-serializable representation of the reduction graph. @@ -111,6 +113,10 @@ pub(crate) struct EdgeJson { pub(crate) overhead: Vec, /// Relative rustdoc path for the reduction module. 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, } /// A path through the variant-level reduction graph. @@ -242,6 +248,13 @@ pub enum TraversalDirection { Both, } +/// Required capability for reduction path search. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum ReductionMode { + Witness, + Aggregate, +} + /// A tree node for neighbor traversal results. #[derive(Debug, Clone)] pub struct NeighborTree { @@ -365,6 +378,7 @@ impl ReductionGraph { overhead, reduce_fn: entry.reduce_fn, reduce_aggregate_fn: entry.reduce_aggregate_fn, + capabilities: entry.capabilities, }, ); } @@ -403,6 +417,21 @@ impl ReductionGraph { .copied() } + fn edge_supports_mode(edge: &ReductionEdgeData, mode: ReductionMode) -> bool { + match mode { + ReductionMode::Witness => edge.capabilities.witness, + ReductionMode::Aggregate => edge.capabilities.aggregate, + } + } + + fn node_path_supports_mode(&self, node_path: &[NodeIndex], mode: ReductionMode) -> bool { + node_path.windows(2).all(|pair| { + self.graph + .find_edge(pair[0], pair[1]) + .is_some_and(|edge_idx| Self::edge_supports_mode(&self.graph[edge_idx], mode)) + }) + } + /// Find the cheapest path between two specific problem variants. /// /// Uses Dijkstra's algorithm on the variant-level graph from the exact @@ -415,10 +444,33 @@ impl ReductionGraph { target_variant: &BTreeMap, input_size: &ProblemSize, cost_fn: &C, + ) -> Option { + self.find_cheapest_path_mode( + source, + source_variant, + target, + target_variant, + ReductionMode::Witness, + input_size, + cost_fn, + ) + } + + /// Find the cheapest path between two specific problem variants while + /// requiring a specific edge capability. + pub fn find_cheapest_path_mode( + &self, + source: &str, + source_variant: &BTreeMap, + target: &str, + target_variant: &BTreeMap, + mode: ReductionMode, + input_size: &ProblemSize, + cost_fn: &C, ) -> Option { let src = self.lookup_node(source, source_variant)?; let dst = self.lookup_node(target, target_variant)?; - let node_path = self.dijkstra(src, dst, input_size, cost_fn)?; + let node_path = self.dijkstra(src, dst, mode, input_size, cost_fn)?; Some(self.node_path_to_reduction_path(&node_path)) } @@ -427,6 +479,7 @@ impl ReductionGraph { &self, src: NodeIndex, dst: NodeIndex, + mode: ReductionMode, input_size: &ProblemSize, cost_fn: &C, ) -> Option> { @@ -462,6 +515,9 @@ impl ReductionGraph { }; for edge_ref in self.graph.edges(node) { + if !Self::edge_supports_mode(edge_ref.weight(), mode) { + continue; + } let overhead = &edge_ref.weight().overhead; let next = edge_ref.target(); @@ -506,6 +562,25 @@ impl ReductionGraph { source_variant: &BTreeMap, target: &str, target_variant: &BTreeMap, + ) -> Vec { + self.find_all_paths_mode( + source, + source_variant, + target, + target_variant, + ReductionMode::Witness, + ) + } + + /// Find all simple paths between two specific problem variants while + /// requiring a specific edge capability. + pub fn find_all_paths_mode( + &self, + source: &str, + source_variant: &BTreeMap, + target: &str, + target_variant: &BTreeMap, + mode: ReductionMode, ) -> Vec { let src = match self.lookup_node(source, source_variant) { Some(idx) => idx, @@ -525,6 +600,7 @@ impl ReductionGraph { paths .iter() + .filter(|p| self.node_path_supports_mode(p, mode)) .map(|p| self.node_path_to_reduction_path(p)) .collect() } @@ -540,6 +616,27 @@ impl ReductionGraph { target: &str, target_variant: &BTreeMap, limit: usize, + ) -> Vec { + self.find_paths_up_to_mode( + source, + source_variant, + target, + target_variant, + ReductionMode::Witness, + limit, + ) + } + + /// Like [`find_all_paths_mode`](Self::find_all_paths_mode) but stops + /// enumeration after collecting `limit` paths. + pub fn find_paths_up_to_mode( + &self, + source: &str, + source_variant: &BTreeMap, + target: &str, + target_variant: &BTreeMap, + mode: ReductionMode, + limit: usize, ) -> Vec { let src = match self.lookup_node(source, source_variant) { Some(idx) => idx, @@ -560,6 +657,7 @@ impl ReductionGraph { paths .iter() + .filter(|p| self.node_path_supports_mode(p, mode)) .map(|p| self.node_path_to_reduction_path(p)) .collect() } @@ -595,6 +693,45 @@ impl ReductionGraph { false } + /// Check if a direct reduction exists by name in a specific mode. + pub fn has_direct_reduction_by_name_mode( + &self, + src: &str, + dst: &str, + mode: ReductionMode, + ) -> bool { + let src_nodes = match self.name_to_nodes.get(src) { + Some(nodes) => nodes, + None => return false, + }; + let dst_nodes = match self.name_to_nodes.get(dst) { + Some(nodes) => nodes, + None => return false, + }; + + let dst_set: HashSet = dst_nodes.iter().copied().collect(); + + for &src_idx in src_nodes { + for edge_ref in self.graph.edges(src_idx) { + if dst_set.contains(&edge_ref.target()) + && Self::edge_supports_mode(edge_ref.weight(), mode) + { + return true; + } + } + } + + false + } + + /// Check if a direct reduction exists from S to T in a specific mode. + pub fn has_direct_reduction_mode( + &self, + mode: ReductionMode, + ) -> bool { + self.has_direct_reduction_by_name_mode(S::NAME, T::NAME, mode) + } + /// Get all registered problem type names (base names). pub fn problem_types(&self) -> Vec<&'static str> { self.name_to_nodes.keys().copied().collect() @@ -733,6 +870,7 @@ impl ReductionGraph { target_name: dst.name, target_variant: dst.variant.clone(), overhead: self.graph[e.id()].overhead.clone(), + capabilities: self.graph[e.id()].capabilities, } }) .collect() @@ -783,6 +921,7 @@ impl ReductionGraph { target_name: dst.name, target_variant: dst.variant.clone(), overhead: self.graph[e.id()].overhead.clone(), + capabilities: self.graph[e.id()].capabilities, } }) .collect() @@ -994,6 +1133,7 @@ impl ReductionGraph { let src_node_id = self.graph[edge_ref.source()]; let dst_node_id = self.graph[edge_ref.target()]; let overhead = &edge_ref.weight().overhead; + let capabilities = edge_ref.weight().capabilities; let overhead_fields = overhead .output_size @@ -1017,6 +1157,8 @@ impl ReductionGraph { target: old_to_new[&dst_node_id], overhead: overhead_fields, doc_path, + witness: capabilities.witness, + aggregate: capabilities.aggregate, }); } @@ -1221,7 +1363,45 @@ impl AggregateReductionChain { } } +struct WitnessBackedIdentityAggregateStep { + inner: Box, +} + +impl DynAggregateReductionResult for WitnessBackedIdentityAggregateStep { + fn target_problem_any(&self) -> &dyn Any { + self.inner.target_problem_any() + } + + fn extract_value_dyn(&self, target_value: serde_json::Value) -> serde_json::Value { + target_value + } +} + impl ReductionGraph { + fn execute_aggregate_edge( + &self, + edge_idx: EdgeIndex, + input: &dyn Any, + ) -> Option> { + let edge = &self.graph[edge_idx]; + if !Self::edge_supports_mode(edge, ReductionMode::Aggregate) { + return None; + } + + if let Some(edge_fn) = edge.reduce_aggregate_fn { + return Some(edge_fn(input)); + } + + if edge.capabilities.witness && edge.capabilities.aggregate { + let edge_fn = edge.reduce_fn?; + return Some(Box::new(WitnessBackedIdentityAggregateStep { + inner: edge_fn(input), + })); + } + + None + } + /// Execute a reduction path on a source problem instance. /// /// Looks up each edge's `reduce_fn`, chains them, and returns the @@ -1249,6 +1429,9 @@ impl ReductionGraph { let src = self.lookup_node(&window[0].name, &window[0].variant)?; let dst = self.lookup_node(&window[1].name, &window[1].variant)?; let edge_idx = self.graph.find_edge(src, dst)?; + if !Self::edge_supports_mode(&self.graph[edge_idx], ReductionMode::Witness) { + return None; + } edge_fns.push(self.graph[edge_idx].reduce_fn?); } // Execute the chain @@ -1275,21 +1458,21 @@ impl ReductionGraph { return None; } - let mut edge_fns = Vec::new(); + let mut edge_indices = Vec::new(); for window in path.steps.windows(2) { let src = self.lookup_node(&window[0].name, &window[0].variant)?; let dst = self.lookup_node(&window[1].name, &window[1].variant)?; let edge_idx = self.graph.find_edge(src, dst)?; - edge_fns.push(self.graph[edge_idx].reduce_aggregate_fn?); + edge_indices.push(edge_idx); } let mut steps: Vec> = Vec::new(); - let step = (edge_fns[0])(source); + let step = self.execute_aggregate_edge(edge_indices[0], source)?; steps.push(step); - for edge_fn in &edge_fns[1..] { + for &edge_idx in &edge_indices[1..] { let step = { let prev_target = steps.last().unwrap().target_problem_any(); - edge_fn(prev_target) + self.execute_aggregate_edge(edge_idx, prev_target)? }; steps.push(step); } diff --git a/src/rules/mod.rs b/src/rules/mod.rs index b727ce979..041d04599 100644 --- a/src/rules/mod.rs +++ b/src/rules/mod.rs @@ -4,7 +4,7 @@ pub mod analysis; pub mod cost; pub mod registry; pub use cost::{CustomCost, Minimize, MinimizeSteps, PathCostFn}; -pub use registry::{ReductionEntry, ReductionOverhead}; +pub use registry::{EdgeCapabilities, ReductionEntry, ReductionOverhead}; pub(crate) mod circuit_spinglass; mod closestvectorproblem_qubo; @@ -98,7 +98,7 @@ pub(crate) mod travelingsalesman_ilp; pub use graph::{ AggregateReductionChain, NeighborInfo, NeighborTree, ReductionChain, ReductionEdgeInfo, - ReductionGraph, ReductionPath, ReductionStep, TraversalDirection, + ReductionGraph, ReductionMode, ReductionPath, ReductionStep, TraversalDirection, }; pub use traits::{ AggregateReductionResult, ReduceTo, ReduceToAggregate, ReductionAutoCast, ReductionResult, diff --git a/src/rules/registry.rs b/src/rules/registry.rs index 680f36267..55043e2b3 100644 --- a/src/rules/registry.rs +++ b/src/rules/registry.rs @@ -89,6 +89,42 @@ pub type ReduceFn = fn(&dyn Any) -> Box; /// Aggregate/value reduction executor stored in the inventory. pub type AggregateReduceFn = fn(&dyn Any) -> Box; +/// 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, +} + +impl EdgeCapabilities { + pub const fn witness_only() -> Self { + Self { + witness: true, + aggregate: false, + } + } + + pub const fn aggregate_only() -> Self { + Self { + witness: false, + aggregate: true, + } + } + + pub const fn both() -> Self { + Self { + witness: true, + aggregate: true, + } + } +} + +impl Default for EdgeCapabilities { + fn default() -> Self { + Self::witness_only() + } +} + /// A registered reduction entry for static inventory registration. /// Uses function pointers to lazily derive variant fields from `Problem::variant()`. pub struct ReductionEntry { @@ -113,6 +149,8 @@ pub struct ReductionEntry { /// `ReduceToAggregate::reduce_to_aggregate()`, and returns the result as a /// boxed `DynAggregateReductionResult`. pub reduce_aggregate_fn: Option, + /// Capability metadata for runtime path filtering. + pub capabilities: EdgeCapabilities, /// Compiled overhead evaluation function. /// Takes a `&dyn Any` (must be `&SourceType`), calls getter methods directly, /// and returns the computed target problem size. @@ -162,6 +200,7 @@ impl std::fmt::Debug for ReductionEntry { .field("target_variant", &self.target_variant()) .field("overhead", &self.overhead()) .field("module_path", &self.module_path) + .field("capabilities", &self.capabilities) .finish() } } diff --git a/src/unit_tests/reduction_graph.rs b/src/unit_tests/reduction_graph.rs index dc636116e..4b10607c2 100644 --- a/src/unit_tests/reduction_graph.rs +++ b/src/unit_tests/reduction_graph.rs @@ -2,8 +2,8 @@ use crate::models::formula::KSatisfiability; use crate::prelude::*; -use crate::rules::{MinimizeSteps, ReductionGraph, TraversalDirection}; -use crate::topology::{SimpleGraph, TriangularSubgraph}; +use crate::rules::{MinimizeSteps, ReductionGraph, ReductionMode, TraversalDirection}; +use crate::topology::{KingsSubgraph, SimpleGraph, TriangularSubgraph, UnitDiskGraph}; use crate::types::ProblemSize; use crate::variant::K3; use std::collections::BTreeMap; @@ -97,6 +97,68 @@ fn test_multi_step_path() { ); } +#[test] +fn aggregate_mode_rejects_witness_only_real_edge() { + let graph = ReductionGraph::new(); + let src = ReductionGraph::variant_to_map(&MaximumIndependentSet::::variant()); + let dst = ReductionGraph::variant_to_map(&MinimumVertexCover::::variant()); + + assert!(graph + .find_cheapest_path_mode( + "MaximumIndependentSet", + &src, + "MinimumVertexCover", + &dst, + ReductionMode::Witness, + &ProblemSize::new(vec![]), + &MinimizeSteps, + ) + .is_some()); + assert!(graph + .find_cheapest_path_mode( + "MaximumIndependentSet", + &src, + "MinimumVertexCover", + &dst, + ReductionMode::Aggregate, + &ProblemSize::new(vec![]), + &MinimizeSteps, + ) + .is_none()); +} + +#[test] +fn natural_edge_supports_both_modes_public_api() { + let graph = ReductionGraph::new(); + let src = + ReductionGraph::variant_to_map(&MaximumIndependentSet::::variant()); + let dst = + ReductionGraph::variant_to_map(&MaximumIndependentSet::::variant()); + + assert!(graph + .find_cheapest_path_mode( + "MaximumIndependentSet", + &src, + "MaximumIndependentSet", + &dst, + ReductionMode::Witness, + &ProblemSize::new(vec![]), + &MinimizeSteps, + ) + .is_some()); + assert!(graph + .find_cheapest_path_mode( + "MaximumIndependentSet", + &src, + "MaximumIndependentSet", + &dst, + ReductionMode::Aggregate, + &ProblemSize::new(vec![]), + &MinimizeSteps, + ) + .is_some()); +} + #[test] fn test_problem_size_propagation() { let graph = ReductionGraph::new(); diff --git a/src/unit_tests/rules/graph.rs b/src/unit_tests/rules/graph.rs index 389d45aed..9228a7571 100644 --- a/src/unit_tests/rules/graph.rs +++ b/src/unit_tests/rules/graph.rs @@ -4,9 +4,9 @@ use crate::models::graph::{MaximumIndependentSet, MinimumVertexCover}; use crate::models::misc::Knapsack; use crate::models::set::MaximumSetPacking; use crate::rules::cost::{Minimize, MinimizeSteps}; -use crate::rules::graph::{classify_problem_category, ReductionStep}; -use crate::rules::registry::ReductionEntry; -use crate::rules::traits::AggregateReductionResult; +use crate::rules::graph::{classify_problem_category, ReductionMode, ReductionStep}; +use crate::rules::registry::{EdgeCapabilities, ReductionEntry}; +use crate::rules::traits::{AggregateReductionResult, ReductionResult}; use crate::topology::SimpleGraph; use crate::traits::Problem; use crate::types::{One, ProblemSize, Sum}; @@ -24,6 +24,9 @@ struct AggregateChainMiddle; #[derive(Clone)] struct AggregateChainTarget; +#[derive(Clone)] +struct NaturalVariantProblem; + impl Problem for AggregateChainSource { const NAME: &'static str = "AggregateChainSource"; type Value = Sum; @@ -75,6 +78,23 @@ impl Problem for AggregateChainTarget { } } +impl Problem for NaturalVariantProblem { + const NAME: &'static str = "NaturalVariantProblem"; + type Value = Sum; + + fn dims(&self) -> Vec { + vec![1] + } + + fn evaluate(&self, config: &[usize]) -> Self::Value { + Sum(config.iter().sum::() as u64) + } + + fn variant() -> Vec<(&'static str, &'static str)> { + vec![] + } +} + struct SourceToMiddleAggregateResult { target: AggregateChainMiddle, } @@ -129,6 +149,85 @@ fn reduce_middle_to_target_aggregate( }) } +struct SourceToMiddleWitnessResult { + target: AggregateChainMiddle, +} + +impl ReductionResult for SourceToMiddleWitnessResult { + type Source = AggregateChainSource; + type Target = AggregateChainMiddle; + + fn target_problem(&self) -> &Self::Target { + &self.target + } + + fn extract_solution(&self, target_solution: &[usize]) -> Vec { + target_solution.to_vec() + } +} + +fn reduce_source_to_middle_witness( + any: &dyn Any, +) -> Box { + any.downcast_ref::() + .expect("expected AggregateChainSource"); + Box::new(SourceToMiddleWitnessResult { + target: AggregateChainMiddle, + }) +} + +fn reduce_natural_variant_witness( + any: &dyn Any, +) -> Box { + let source = any + .downcast_ref::() + .expect("expected NaturalVariantProblem"); + Box::new(crate::rules::ReductionAutoCast::< + NaturalVariantProblem, + NaturalVariantProblem, + >::new(source.clone())) +} + +fn build_two_node_graph( + source_name: &'static str, + source_variant: BTreeMap, + target_name: &'static str, + target_variant: BTreeMap, + edge: ReductionEdgeData, +) -> ReductionGraph { + let nodes = vec![ + VariantNode { + name: source_name, + variant: source_variant.clone(), + complexity: "", + }, + VariantNode { + name: target_name, + variant: target_variant.clone(), + complexity: "", + }, + ]; + + let mut graph = DiGraph::new(); + let source_idx = graph.add_node(0); + let target_idx = graph.add_node(1); + graph.add_edge(source_idx, target_idx, edge); + + let mut name_to_nodes = HashMap::new(); + name_to_nodes.insert(source_name, vec![source_idx]); + name_to_nodes + .entry(target_name) + .or_insert_with(Vec::new) + .push(target_idx); + + ReductionGraph { + graph, + nodes, + name_to_nodes, + default_variants: HashMap::new(), + } +} + #[test] fn test_find_direct_path() { let graph = ReductionGraph::new(); @@ -178,6 +277,7 @@ fn test_aggregate_reduction_chain_extracts_value_backwards() { overhead: crate::rules::registry::ReductionOverhead::default(), reduce_fn: None, reduce_aggregate_fn: Some(reduce_source_to_middle_aggregate), + capabilities: EdgeCapabilities::aggregate_only(), }, ); graph.add_edge( @@ -187,6 +287,7 @@ fn test_aggregate_reduction_chain_extracts_value_backwards() { overhead: crate::rules::registry::ReductionOverhead::default(), reduce_fn: None, reduce_aggregate_fn: Some(reduce_middle_to_target_aggregate), + capabilities: EdgeCapabilities::aggregate_only(), }, ); @@ -225,6 +326,132 @@ fn test_aggregate_reduction_chain_extracts_value_backwards() { assert_eq!(chain.extract_value_dyn(json!(7)), json!(12)); } +#[test] +fn witness_path_search_rejects_aggregate_only_edge() { + let source_variant = BTreeMap::new(); + let target_variant = BTreeMap::new(); + let graph = build_two_node_graph( + AggregateChainSource::NAME, + source_variant.clone(), + AggregateChainMiddle::NAME, + target_variant.clone(), + ReductionEdgeData { + overhead: crate::rules::registry::ReductionOverhead::default(), + reduce_fn: None, + reduce_aggregate_fn: Some(reduce_source_to_middle_aggregate), + capabilities: EdgeCapabilities::aggregate_only(), + }, + ); + + assert!(graph + .find_cheapest_path_mode( + AggregateChainSource::NAME, + &source_variant, + AggregateChainMiddle::NAME, + &target_variant, + ReductionMode::Witness, + &ProblemSize::new(vec![]), + &MinimizeSteps, + ) + .is_none()); + assert!(graph + .find_cheapest_path_mode( + AggregateChainSource::NAME, + &source_variant, + AggregateChainMiddle::NAME, + &target_variant, + ReductionMode::Aggregate, + &ProblemSize::new(vec![]), + &MinimizeSteps, + ) + .is_some()); +} + +#[test] +fn aggregate_path_search_rejects_witness_only_edge() { + let source_variant = BTreeMap::new(); + let target_variant = BTreeMap::new(); + let graph = build_two_node_graph( + AggregateChainSource::NAME, + source_variant.clone(), + AggregateChainMiddle::NAME, + target_variant.clone(), + ReductionEdgeData { + overhead: crate::rules::registry::ReductionOverhead::default(), + reduce_fn: Some(reduce_source_to_middle_witness), + reduce_aggregate_fn: None, + capabilities: EdgeCapabilities::witness_only(), + }, + ); + + assert!(graph + .find_cheapest_path_mode( + AggregateChainSource::NAME, + &source_variant, + AggregateChainMiddle::NAME, + &target_variant, + ReductionMode::Aggregate, + &ProblemSize::new(vec![]), + &MinimizeSteps, + ) + .is_none()); + assert!(graph + .find_cheapest_path_mode( + AggregateChainSource::NAME, + &source_variant, + AggregateChainMiddle::NAME, + &target_variant, + ReductionMode::Witness, + &ProblemSize::new(vec![]), + &MinimizeSteps, + ) + .is_some()); +} + +#[test] +fn natural_edge_supports_both_modes() { + 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( + NaturalVariantProblem::NAME, + source_variant.clone(), + NaturalVariantProblem::NAME, + target_variant.clone(), + ReductionEdgeData { + overhead: crate::rules::registry::ReductionOverhead::default(), + reduce_fn: Some(reduce_natural_variant_witness), + reduce_aggregate_fn: None, + capabilities: EdgeCapabilities::both(), + }, + ); + + let witness_path = graph.find_cheapest_path_mode( + NaturalVariantProblem::NAME, + &source_variant, + NaturalVariantProblem::NAME, + &target_variant, + ReductionMode::Witness, + &ProblemSize::new(vec![]), + &MinimizeSteps, + ); + let aggregate_path = graph.find_cheapest_path_mode( + NaturalVariantProblem::NAME, + &source_variant, + NaturalVariantProblem::NAME, + &target_variant, + ReductionMode::Aggregate, + &ProblemSize::new(vec![]), + &MinimizeSteps, + ); + + assert!(witness_path.is_some()); + let aggregate_path = aggregate_path.expect("expected aggregate path"); + let chain = graph + .reduce_aggregate_along_path(&aggregate_path, &NaturalVariantProblem as &dyn Any) + .expect("expected aggregate chain"); + assert_eq!(chain.extract_value_dyn(json!(7)), json!(7)); +} + #[test] fn test_find_indirect_path() { let graph = ReductionGraph::new(); diff --git a/src/unit_tests/rules/registry.rs b/src/unit_tests/rules/registry.rs index 179abe2a2..474f88cb6 100644 --- a/src/unit_tests/rules/registry.rs +++ b/src/unit_tests/rules/registry.rs @@ -48,6 +48,7 @@ fn test_reduction_entry_overhead() { module_path: "test::module", reduce_fn: Some(dummy_reduce_fn), reduce_aggregate_fn: None, + capabilities: EdgeCapabilities::witness_only(), overhead_eval_fn: dummy_overhead_eval_fn, }; @@ -68,6 +69,7 @@ fn test_reduction_entry_debug() { module_path: "test::module", reduce_fn: Some(dummy_reduce_fn), reduce_aggregate_fn: None, + capabilities: EdgeCapabilities::witness_only(), overhead_eval_fn: dummy_overhead_eval_fn, }; @@ -87,6 +89,7 @@ fn test_is_base_reduction_unweighted() { module_path: "test::module", reduce_fn: Some(dummy_reduce_fn), reduce_aggregate_fn: None, + capabilities: EdgeCapabilities::witness_only(), overhead_eval_fn: dummy_overhead_eval_fn, }; assert!(entry.is_base_reduction()); @@ -103,6 +106,7 @@ fn test_is_base_reduction_source_weighted() { module_path: "test::module", reduce_fn: Some(dummy_reduce_fn), reduce_aggregate_fn: None, + capabilities: EdgeCapabilities::witness_only(), overhead_eval_fn: dummy_overhead_eval_fn, }; assert!(!entry.is_base_reduction()); @@ -119,6 +123,7 @@ fn test_is_base_reduction_target_weighted() { module_path: "test::module", reduce_fn: Some(dummy_reduce_fn), reduce_aggregate_fn: None, + capabilities: EdgeCapabilities::witness_only(), overhead_eval_fn: dummy_overhead_eval_fn, }; assert!(!entry.is_base_reduction()); @@ -135,6 +140,7 @@ fn test_is_base_reduction_both_weighted() { module_path: "test::module", reduce_fn: Some(dummy_reduce_fn), reduce_aggregate_fn: None, + capabilities: EdgeCapabilities::witness_only(), overhead_eval_fn: dummy_overhead_eval_fn, }; assert!(!entry.is_base_reduction()); @@ -152,6 +158,7 @@ fn test_is_base_reduction_no_weight_key() { module_path: "test::module", reduce_fn: Some(dummy_reduce_fn), reduce_aggregate_fn: None, + capabilities: EdgeCapabilities::witness_only(), overhead_eval_fn: dummy_overhead_eval_fn, }; assert!(entry.is_base_reduction()); @@ -168,6 +175,7 @@ fn test_reduction_entry_can_store_aggregate_executor() { module_path: "test::module", reduce_fn: None, reduce_aggregate_fn: Some(dummy_reduce_aggregate_fn), + capabilities: EdgeCapabilities::aggregate_only(), overhead_eval_fn: dummy_overhead_eval_fn, }; From 90a23658809bac88c16a012710678eab280d8ff1 Mon Sep 17 00:00:00 2001 From: GiggleLiu Date: Mon, 23 Mar 2026 11:02:31 +0800 Subject: [PATCH 06/18] save point 1 --- ...026-03-22-counting-problem-trait-design.md | 244 --------- ...26-03-22-generalized-aggregation-design.md | 477 ++++++++++++++++++ ...-generalized-aggregation-implementation.md | 452 +++++++++++++++++ problemreductions-cli/src/commands/reduce.rs | 15 +- problemreductions-cli/src/commands/solve.rs | 119 ++++- problemreductions-cli/src/dispatch.rs | 95 +++- problemreductions-cli/src/main.rs | 2 + problemreductions-cli/src/mcp/tests.rs | 39 ++ problemreductions-cli/src/mcp/tools.rs | 68 ++- problemreductions-cli/src/test_support.rs | 241 +++++++++ src/solvers/ilp/solver.rs | 133 ++++- 11 files changed, 1547 insertions(+), 338 deletions(-) delete mode 100644 docs/plans/2026-03-22-counting-problem-trait-design.md create mode 100644 docs/plans/2026-03-22-generalized-aggregation-design.md create mode 100644 docs/plans/2026-03-23-generalized-aggregation-implementation.md create mode 100644 problemreductions-cli/src/test_support.rs diff --git a/docs/plans/2026-03-22-counting-problem-trait-design.md b/docs/plans/2026-03-22-counting-problem-trait-design.md deleted file mode 100644 index 5aa94a2d3..000000000 --- a/docs/plans/2026-03-22-counting-problem-trait-design.md +++ /dev/null @@ -1,244 +0,0 @@ -# CountingProblem Trait — Supporting #P and PP-Complete Problems - -**Date:** 2026-03-22 -**Status:** Approved design, pending implementation - -## Problem - -The current trait hierarchy supports two problem families: - -- `OptimizationProblem` (`Metric = SolutionSize`) — find a config that maximizes/minimizes an objective -- `SatisfactionProblem` (`Metric = bool`) — find a config satisfying all constraints - -8 issues are blocked because they model problems where the answer depends on **aggregating over the entire configuration space** — counting feasible configs or summing weighted probabilities. These are #P-complete or PP-complete problems (not known to be in NP) that don't fit either existing trait. - -### Blocked issues - -**Models:** #235 NetworkReliability, #237 NetworkSurvivability, #404 KthLargestSubset, #405 KthLargestMTuple - -**Rules (blocked on models above):** #256 SteinerTree → NetworkReliability, #257 VertexCover → NetworkSurvivability, #394 SubsetSum → KthLargestSubset, #395 SubsetSum → KthLargestMTuple - -## Design - -### New type: `Weight` - -A newtype wrapper for per-configuration weights, parallel to `SolutionSize` for optimization problems. Infeasible configs have weight zero — no separate `Infeasible` variant needed (unlike `SolutionSize::Invalid`) because a zero-weight config contributes nothing to the sum. - -```rust -// src/types.rs -#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] -pub struct Weight(pub W); -``` - -### New trait: `CountingProblem` - -A marker trait parallel to `SatisfactionProblem`, binding `Metric = Weight`: - -```rust -// src/traits.rs -pub trait CountingProblem: Problem> { - /// The inner weight type (e.g., `u64` for unweighted counting, `f64` for probabilities). - type Value: Clone + AddAssign + Zero + PartialOrd + fmt::Debug + Serialize + DeserializeOwned; -} -``` - -The `evaluate(config) -> Weight` method (inherited from `Problem`) returns the weight of a single configuration. The "answer" to the problem is the sum of weights over all configurations. This is computed by the solver, not by `evaluate`. - -### Trait hierarchy (updated) - -``` -Problem (Metric: Clone) -├── OptimizationProblem (Metric = SolutionSize) — existing, unchanged -├── SatisfactionProblem (Metric = bool) — existing, unchanged -└── CountingProblem (Metric = Weight) — NEW -``` - -### Solver extension - -Add a separate `CountingSolver` trait (parallel to how problem families have distinct traits) rather than extending the existing `Solver` trait. This avoids forcing `ILPSolver` to implement a meaningless `count` method: - -```rust -// src/solvers/mod.rs (existing Solver trait unchanged) - -/// Solver trait for counting problems. -pub trait CountingSolver { - /// Compute the total weight (sum of evaluate over all configs). - fn count(&self, problem: &P) -> P::Value; -} - -// src/solvers/brute_force.rs -impl CountingSolver for BruteForce { - fn count(&self, problem: &P) -> P::Value { - let mut total = P::Value::zero(); - for config in DimsIterator::new(problem.dims()) { - total += problem.evaluate(&config).0; - } - total - } -} -``` - -`BruteForce` also gets a convenience method for testing: -```rust -/// Return all feasible configs and their weights alongside the total count. -pub fn count_with_configs(&self, problem: &P) - -> (P::Value, Vec<(Vec, P::Value)>); -``` - -### Reduction support - -Counting reductions preserve aggregate counts, not individual solutions. New traits parallel to `ReductionResult` / `ReduceTo`: - -```rust -// src/rules/traits.rs - -pub trait CountingReductionResult { - type Source: CountingProblem; - type Target: CountingProblem; - - /// Get a reference to the target problem. - fn target_problem(&self) -> &Self::Target; - - /// Transform the target's aggregate count back to the source's count. - /// - /// For parsimonious reductions (1-to-1 config mapping), this is identity. - /// For non-parsimonious reductions, this applies a correction factor - /// (e.g., divide by 2 if the reduction doubles feasible configs). - fn extract_count( - &self, - target_count: ::Value, - ) -> ::Value; -} - -pub trait ReduceToCount: CountingProblem { - type Result: CountingReductionResult; - fn reduce_to_count(&self) -> Self::Result; -} -``` - -### Registry and CLI integration - -#### `declare_variants!` macro - -Gets a new `count` keyword. The macro generates a `CountSolveFn` (instead of `SolveFn`) that calls `BruteForce::count()` and formats the result: - -```rust -crate::declare_variants! { - default count NetworkReliability => "2^num_edges * num_vertices", -} -``` - -The `count` keyword generates: -- A new `SolverKind::Count` variant in the proc macro's internal `SolverKind` enum (alongside existing `Opt` and `Sat`) -- A `solver_kind` field on `VariantEntry` to distinguish problem families at runtime (enum with `Optimization`, `Satisfaction`, `Counting` variants) -- A `count_fn: Option` field on `VariantEntry` where `CountSolveFn = fn(&dyn Any) -> String` -- The generated function downcasts `&dyn Any` to the concrete type, calls `BruteForce.count(&problem)`, and formats the result -- The existing `ProblemType` struct (which holds problem metadata, not a classification enum) is unchanged - -#### `LoadedDynProblem` - -Gets a new method: -```rust -pub fn solve_counting(&self) -> Option { - (self.count_fn?)(self.inner.as_any()) -} -``` - -The existing `solve_brute_force` remains unchanged for opt/sat problems. - -#### `pred solve` CLI - -The solve command checks `VariantEntry::solver_kind` to determine the dispatch path: -- `SolverKind::Optimization` / `SolverKind::Satisfaction` → existing `solve_brute_force()` -- `SolverKind::Counting` → new `solve_counting()`, displays `Total weight: ` - -#### `#[reduction]` proc macro - -The existing macro hardcodes `ReduceTo` trait detection. It must be extended to also recognize `ReduceToCount`: - -- When the macro sees `impl ReduceToCount for Source`, it generates a `reduce_count_fn` field on `ReductionEntry` -- The generated function returns a `Box` (new type-erased trait for counting reductions) -- `overhead` attribute works identically — overhead expressions are about problem size, not about solution type - -#### `ReductionEntry` changes - -`ReductionEntry` (in `src/rules/registry.rs`) gains new optional fields for counting reductions. A given entry has either `reduce_fn` (opt/sat) or `reduce_count_fn` (counting), never both: - -```rust -pub struct ReductionEntry { - // ... existing fields unchanged ... - pub reduce_fn: Option, // existing: opt/sat reductions - pub reduce_count_fn: Option, // NEW: counting reductions -} -``` - -Where `CountReduceFn = fn(&dyn Any) -> Box`. - -#### Reduction graph integration - -Counting edges and opt/sat edges coexist in the same `ReductionGraph`. The graph is about problem reachability — edge type doesn't affect pathfinding. The distinction matters only at solve time: - -- `ReductionEdgeData` gains an `edge_kind: EdgeKind` field (`enum EdgeKind { Standard, Counting }`) -- `reduce_along_path` checks edge kinds: a path must be homogeneous (all-standard or all-counting); mixed paths are invalid -- For all-counting paths, the runtime builds a `CountingReductionChain` instead of a `ReductionChain` - -#### Counting reduction chains - -For multi-hop counting paths (A →count→ B →count→ C): - -```rust -pub trait DynCountingReductionResult { - fn target_problem_any(&self) -> &dyn Any; - /// Transform target count to source count using serde_json::Value for type erasure. - fn extract_count_dyn(&self, target_count: serde_json::Value) -> serde_json::Value; -} -``` - -`CountingReductionChain` composes these: reduce A→B→C, solve C to get count as `serde_json::Value`, then call `extract_count_dyn` backwards through the chain. This parallels `ReductionChain` for opt/sat reductions. - -**Note on cross-type reductions:** When source and target have different `Value` types (e.g., `u64` → `f64`), the `extract_count` implementation is responsible for the type conversion. The `serde_json::Value` type erasure in `DynCountingReductionResult` handles this naturally at the runtime dispatch level. - -#### Exports - -Add to prelude and `lib.rs`: -- `CountingProblem`, `Weight`, `CountingSolver` traits/types -- `ReduceToCount`, `CountingReductionResult` traits - -### Concrete models - -All models store **only the counting problem data** — no decision thresholds (`k`, `q`). The threshold is part of the GJ decision formulation but not part of the counting problem we model. - -| Model | Value type | Fields | evaluate returns | -|---|---|---|---| -| `NetworkReliability` | `f64` | `graph`, `terminals`, `failure_probs` | `Weight(Π p_e^{x_e} · (1-p_e)^{1-x_e})` if terminals connected, else `Weight(0.0)` | -| `NetworkSurvivability` | `f64` | `graph`, `terminals`, `failure_probs` | Same pattern for survivability | -| `KthLargestSubset` | `u64` | `sizes`, `bound` | `Weight(1)` if subset sum ≤ bound, else `Weight(0)` | -| `KthLargestMTuple` | `u64` | `sizes`, `bound` | `Weight(1)` if m-tuple condition met, else `Weight(0)` | - -### `Weight` utility impls - -For ergonomics, `Weight` implements: -- `PartialOrd` where `W: PartialOrd` — delegates to inner value -- `Eq` where `W: Eq`, `Hash` where `W: Hash` — conditional impls (works for `u64`, not `f64`) -- `Add>` and `std::iter::Sum` where `W: Add` — enables `configs.map(evaluate).sum()` -- `Display` where `W: Display` — prints the inner value directly (e.g., `0.9832` not `Weight(0.9832)`) - -### What is NOT changed - -- `OptimizationProblem`, `SatisfactionProblem` — untouched -- `ReduceTo`, `ReductionResult` — untouched -- All existing models and rules — untouched -- Existing `Solver` trait — untouched (new `CountingSolver` is separate) - -## Alternatives considered - -1. **Generalized metric aggregation** (#737) — replace all three leaf traits with a single `Aggregation` enum. Elegant but large breaking refactor with no immediate payoff. Filed for future consideration. - -2. **Two-level trait** (`is_feasible` + `weight` methods) — more explicit but adds unnecessary surface area and boilerplate for unweighted counting. - -3. **`Metric = f64` without wrapper** — works but loses type safety. `Weight` follows the `SolutionSize` pattern and makes intent explicit. - -## Related issues - -- #737 — Generalized metric aggregation (future architecture) -- #748 — DefaultSolver per problem (future architecture) diff --git a/docs/plans/2026-03-22-generalized-aggregation-design.md b/docs/plans/2026-03-22-generalized-aggregation-design.md new file mode 100644 index 000000000..f467d20b3 --- /dev/null +++ b/docs/plans/2026-03-22-generalized-aggregation-design.md @@ -0,0 +1,477 @@ +# Generalized Aggregation -- Unified Problem Trait Hierarchy + +**Date:** 2026-03-22 +**Status:** Revised design, approved for implementation planning +**Supersedes:** `2026-03-22-counting-problem-trait-design.md` + +## Problem + +The current trait hierarchy hard-codes two witness-oriented problem families: + +- `OptimizationProblem` (`Metric = SolutionSize`, plus `direction()`) +- `SatisfactionProblem` (`Metric = bool`) + +That works for "find one config" workflows, but it does not scale to `#P` and probability problems where the answer is an aggregate over the whole configuration space. Adding a third parallel leaf trait for counting would unblock the immediate issues, but it would also duplicate the same branching in solvers, macros, registry dispatch, and reduction execution. + +The goal of this design is to unify value aggregation while preserving the existing witness-oriented workflows that the repo already depends on: + +- brute-force witness search +- solution extraction through reduction chains +- `pred reduce` bundles +- `pred solve bundle.json` +- ILP solve-via-reduction + +## Core idea + +Unify the **value layer**, not the **witness layer**. + +Each problem exposes a single aggregate value type. Solvers always know how to compute the final value by folding over all configurations. Some aggregate types also support recovering representative witness configurations; others do not. + +This keeps the mathematical core small while making the runtime honest about which operations are valid. + +## `Aggregate` trait + +`Aggregate` remains a monoid at its core, but it also exposes optional witness hooks with safe defaults. That is the minimal extra surface needed to keep dynamic witness APIs working without re-introducing a full parallel trait hierarchy. + +```rust +// src/types.rs +pub trait Aggregate: Clone + fmt::Debug + Serialize + DeserializeOwned { + /// Neutral element for folding over the configuration space. + fn identity() -> Self; + + /// Associative combine operation. + fn combine(self, other: Self) -> Self; + + /// Whether this aggregate admits representative witness configurations. + fn supports_witnesses() -> bool { + false + } + + /// Whether a per-configuration value belongs to the witness set + /// for the final aggregate value. + fn contributes_to_witnesses(_config_value: &Self, _total: &Self) -> bool { + false + } +} +``` + +The default witness behavior is deliberately conservative: + +- `Sum` and `And` remain value-only +- `Max`, `Min`, and `Or` opt in to witness recovery + +## Aggregate types + +Five concrete aggregate wrappers replace the current leaf-trait split: + +```rust +// src/types.rs +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +pub struct Max(pub Option); + +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +pub struct Min(pub Option); + +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +pub struct Sum(pub W); + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct Or(pub bool); + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct And(pub bool); +``` + +| Type | Identity | Combine | Witness support | Replaces | +|------|----------|---------|-----------------|----------| +| `Max` | `Max(None)` | keep larger `Some` | yes | `SolutionSize` + `Direction::Maximize` | +| `Min` | `Min(None)` | keep smaller `Some` | yes | `SolutionSize` + `Direction::Minimize` | +| `Sum` | `Sum(W::zero())` | numeric addition | no | counting / probability totals | +| `Or` | `Or(false)` | logical or | yes | `bool` existential problems | +| `And` | `And(true)` | logical and | no | universal / tautology-style problems | + +Witness semantics: + +- `Max` / `Min`: a config is a witness iff its aggregate value equals the final optimum and is feasible +- `Or`: a config is a witness iff it evaluates to `Or(true)` and the final total is `Or(true)` +- `Sum` / `And`: no single config is a representative witness, so witness APIs return `None` / empty + +## Unified `Problem` trait + +```rust +// src/traits.rs +pub trait Problem: Clone { + const NAME: &'static str; + type Value: Aggregate; + + fn dims(&self) -> Vec; + fn evaluate(&self, config: &[usize]) -> Self::Value; + + fn num_variables(&self) -> usize { + self.dims().len() + } + + fn variant() -> Vec<(&'static str, &'static str)>; + + fn problem_type() -> crate::registry::ProblemType { + crate::registry::find_problem_type(Self::NAME) + .unwrap_or_else(|| panic!("no catalog entry for Problem::NAME = {:?}", Self::NAME)) + } +} +``` + +Removed: + +- `OptimizationProblem` +- `SatisfactionProblem` +- `type Metric` +- `SolutionSize` +- `Direction` + +Unchanged: + +- `DeclaredVariant` +- `Problem::NAME` +- `dims()` +- `variant()` +- catalog bridge via `problem_type()` + +## Solvers + +### Value solving + +All problems support value solving through one fold: + +```rust +// src/solvers/mod.rs +pub trait Solver { + fn solve(&self, problem: &P) -> P::Value; +} +``` + +```rust +// src/solvers/brute_force.rs +impl Solver for BruteForce { + fn solve(&self, problem: &P) -> P::Value { + DimsIterator::new(problem.dims()) + .map(|config| problem.evaluate(&config)) + .fold(P::Value::identity(), P::Value::combine) + } +} +``` + +### Witness solving + +Witness APIs remain available, but only when the aggregate type opts in through the default hooks above: + +```rust +impl BruteForce { + pub fn find_witness(&self, problem: &P) -> Option>; + pub fn find_all_witnesses(&self, problem: &P) -> Vec>; + pub fn solve_with_witnesses(&self, problem: &P) + -> (P::Value, Vec>); +} +``` + +Behavior: + +- `Max` / `Min`: witnesses are the optimal configs +- `Or`: witnesses are satisfying configs +- `Sum` / `And`: `find_witness()` returns `None`, `find_all_witnesses()` returns `[]` + +This is the key distinction from the counting-only design: value aggregation is unified, but witness recovery is explicitly optional. + +## Dynamic solve surfaces + +The dynamic registry needs two solve entry points, not one: + +```rust +// src/registry/dyn_problem.rs +pub type SolveValueFn = fn(&dyn Any) -> String; +pub type SolveWitnessFn = fn(&dyn Any) -> Option<(Vec, String)>; +``` + +`VariantEntry` stores both: + +- `solve_value_fn` always exists +- `solve_witness_fn` always exists, but returns `None` for aggregate-only values (`Sum`, `And`) + +`LoadedDynProblem` mirrors that split: + +- `solve_brute_force_value() -> String` +- `solve_brute_force_witness() -> Option<(Vec, String)>` + +This keeps `declare_variants!` simple: + +- the `opt` / `sat` keywords disappear +- the generated value-solve closure always calls `Solver::solve()` +- the generated witness-solve closure always calls `BruteForce::find_witness()` + +No solver-kind branching is needed at variant registration time. + +## CLI behavior + +### `pred solve problem.json` + +Always computes the aggregate value. + +- If a witness exists, print both `Solution` and `Evaluation` +- If no witness exists, print only `Evaluation` + +Examples: + +- `Max(Some(42))` -> solution config + `Maximum: 42` +- `Or(true)` -> solution config + `Satisfiable: true` +- `Sum(0.9832)` -> no single solution config, print `Sum: 0.9832` +- `And(false)` -> no single solution config, print `Tautology: false` + +### `pred solve bundle.json` + +Remains a **witness-only** workflow in this design. + +Bundles exist to solve a target problem and map a target configuration back through `extract_solution`. That makes sense only for witness-capable problems and witness-capable reduction paths. + +If the target variant or the path is aggregate-only, bundle solving is rejected early with a clear error. + +### `--solver ilp` + +Also remains **witness-only**. + +ILP support in this repo is a witness-producing solve-via-reduction path. Aggregate-only problems (`Sum`, `And`) do not have an ILP mode unless a future design introduces a threshold or certificate-bearing witness formulation. + +The immediate design change is: + +- keep the ILP solver internals unchanged +- require witness-capable source problems +- require a witness-capable path from source to `ILP` + +## Reductions + +Two reduction traits remain necessary because config mapping and aggregate-value mapping are genuinely different operations. + +```rust +// src/rules/traits.rs +pub trait ReductionResult { + type Source: Problem; + type Target: Problem; + fn target_problem(&self) -> &Self::Target; + fn extract_solution(&self, target_solution: &[usize]) -> Vec; +} + +pub trait ReduceTo: Problem { + type Result: ReductionResult; + fn reduce_to(&self) -> Self::Result; +} + +pub trait AggregateReductionResult { + type Source: Problem; + type Target: Problem; + fn target_problem(&self) -> &Self::Target; + fn extract_value( + &self, + target_value: ::Value, + ) -> ::Value; +} + +pub trait ReduceToAggregate: Problem { + type Result: AggregateReductionResult; + fn reduce_to_aggregate(&self) -> Self::Result; +} +``` + +Type-erased runtime support likewise splits: + +- `DynReductionResult` for witness/config reductions +- `DynAggregateReductionResult` for aggregate/value reductions + +## `EdgeCapabilities` + +The reduction graph needs explicit edge-mode metadata so path search can reject incompatible paths before execution. + +```rust +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] +pub struct EdgeCapabilities { + pub witness: bool, + pub aggregate: bool, +} +``` + +Capability assignment: + +- `ReduceTo` edges -> `{ witness: true, aggregate: false }` +- `ReduceToAggregate` edges -> `{ witness: false, aggregate: true }` +- natural subtype / `ReductionAutoCast` edges -> `{ witness: true, aggregate: true }` + +Why the natural edges are both: + +- witness mode: the config mapping is identity +- aggregate mode: the value mapping is also identity because the problem semantics do not change + +## Mode-aware pathfinding + +Pathfinding stays on one graph, but it now receives a required capability: + +```rust +pub enum ReductionMode { + Witness, + Aggregate, +} +``` + +`ReductionGraph::find_cheapest_path(...)` becomes capability-aware: + +- witness callers traverse only edges with `capabilities.witness` +- aggregate callers traverse only edges with `capabilities.aggregate` + +This prevents "valid graph path, invalid runtime execution" failures. + +Mode usage: + +- `pred reduce` -> witness +- `pred solve bundle.json` -> witness +- ILP solve-via-reduction -> witness +- future aggregate chain execution -> aggregate +- graph export / inspection -> all edges, with capability metadata shown + +## Aggregate reduction chains + +Witness execution stays on `ReductionChain`. + +Aggregate execution gets its own chain: + +```rust +pub struct AggregateReductionChain { + steps: Vec>, +} +``` + +with: + +- `target_problem_any()` +- backwards composition of `extract_value_dyn(...)` + +The important point is that witness execution and aggregate execution are separate entry points over the same graph, selected by `ReductionMode`. + +## Registry and graph changes + +### `ReductionEntry` + +`ReductionEntry` gains: + +- `reduce_fn: Option` +- `reduce_aggregate_fn: Option` +- `capabilities: EdgeCapabilities` + +### `ReductionEdgeData` + +`ReductionEdgeData` gains: + +- `capabilities: EdgeCapabilities` +- optional witness executor +- optional aggregate executor + +### Graph export + +The JSON export includes: + +- `witness: bool` +- `aggregate: bool` + +instead of a single coarse edge-kind label. + +## Model migration examples + +### Optimization + +```rust +impl Problem for MaximumIndependentSet { + type Value = Max; + + fn evaluate(&self, config: &[usize]) -> Max { + if invalid { + Max(None) + } else { + Max(Some(size)) + } + } +} +``` + +### Satisfaction + +```rust +impl Problem for Satisfiability { + type Value = Or; + + fn evaluate(&self, config: &[usize]) -> Or { + Or(satisfies) + } +} +``` + +### Counting + +```rust +impl Problem for NetworkReliability { + type Value = Sum; + + fn evaluate(&self, config: &[usize]) -> Sum { + if terminals_connected { + Sum(probability_weight) + } else { + Sum(0.0) + } + } +} +``` + +## Migration scope + +| Area | Change | +|------|--------| +| `src/types.rs` | replace `SolutionSize` / `Direction` with aggregate wrappers and witness hooks | +| `src/traits.rs` | unify on `Problem` | +| `src/solvers/` | one value fold plus generic witness helpers | +| `src/registry/` | split value solve from witness solve | +| `problemreductions-macros/` | remove `opt` / `sat`, emit both dynamic solve closures | +| `src/rules/` | add aggregate reductions and capability-aware path execution | +| `problemreductions-cli/` | differentiate value-only vs witness-capable solve output | +| existing model/test files | mechanical `Metric -> Value` migration | + +## What is not changed + +- problem names, aliases, and variant resolution +- the overall CLI command set +- the catalog bridge via `ProblemType` +- the fact that ILP is a witness-oriented backend +- the paper format in `docs/paper/reductions.typ` + +## Deferred follow-up work + +Out of scope for this design revision: + +- threshold-specific decision wrappers for `Sum` problems +- a new aggregate-only bundle format +- universal counterexample extraction for `And` +- choosing default reduction modes in graph-inspection UX + +## Alternatives considered + +1. **Minimal `CountingProblem` extension** + - Lowest short-term diff + - Repeats the branching in solvers, registry dispatch, macros, and reductions + +2. **Unify value aggregation but keep witness-oriented runtime explicit** (chosen) + - Solves the architectural duplication + - Preserves the witness assumptions already embedded in the repo + +3. **Single edge kind with runtime rejection** + - Smaller patch + - Bad UX and bad API: pathfinding would still return paths that cannot be executed + +## Related issues + +- #737 -- original aggregation architecture issue +- #748 -- default solver per problem (future, orthogonal) +- #235, #237, #404, #405 -- counting models enabled by this refactor +- #256, #257, #394, #395 -- aggregate-value reductions enabled by this refactor diff --git a/docs/plans/2026-03-23-generalized-aggregation-implementation.md b/docs/plans/2026-03-23-generalized-aggregation-implementation.md new file mode 100644 index 000000000..248076808 --- /dev/null +++ b/docs/plans/2026-03-23-generalized-aggregation-implementation.md @@ -0,0 +1,452 @@ +# Generalized Aggregation Implementation Plan + +> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task. + +**Goal:** Replace the current optimization/satisfaction split with aggregate values, preserve witness workflows where they are meaningful, and make reduction-path search capability-aware via `EdgeCapabilities`. + +**Architecture:** Implement the refactor in layers. First add aggregate primitives and generic solver semantics, then split dynamic value solve from witness solve, then extend the reduction graph with aggregate reductions and capability-aware path search. After the infrastructure is stable, migrate the existing model/test surface mechanically from `Metric` to `Value`. + +**Tech Stack:** Rust, inventory-based registry dispatch, proc macros in `problemreductions-macros`, petgraph-based reduction graph, Cargo tests, repository `make` targets. + +--- + +### Task 1: Add aggregate core types + +**Files:** +- Modify: `src/types.rs` +- Test: `src/unit_tests/types.rs` + +**Step 1: Write the failing tests** + +Add focused tests for: + +- `Max::::identity()` and `combine()` +- `Min::::identity()` and `combine()` +- `Sum::::identity()` and `combine()` +- `Or::identity()` / `combine()` +- `And::identity()` / `combine()` +- witness defaults for `Sum` and `And` +- witness hooks for `Max`, `Min`, and `Or` + +Example test skeleton: + +```rust +#[test] +fn test_max_identity_and_combine() { + assert_eq!(Max::::identity(), Max(None)); + assert_eq!(Max(Some(7)).combine(Max(Some(3))), Max(Some(7))); +} +``` + +**Step 2: Run test to verify it fails** + +Run: `cargo test test_max_identity_and_combine --lib` +Expected: FAIL with missing `Max` / `Aggregate` definitions + +**Step 3: Write minimal implementation** + +In `src/types.rs`: + +- add `Aggregate` +- add `Max`, `Min`, `Sum`, `Or`, `And` +- add `Display` impls for user-facing formatting +- keep witness hooks on `Aggregate` with safe defaults + +**Step 4: Run tests to verify they pass** + +Run: `cargo test test_max_identity_and_combine --lib` +Expected: PASS + +Run: `cargo test test_sum_identity_and_combine --lib` +Expected: PASS + +**Step 5: Commit** + +```bash +git add src/types.rs src/unit_tests/types.rs +git commit -m "refactor: add aggregate value primitives" +``` + +### Task 2: Rewrite `Problem` and `BruteForce` around aggregate values + +**Files:** +- Modify: `src/traits.rs` +- Modify: `src/solvers/mod.rs` +- Modify: `src/solvers/brute_force.rs` +- Test: `src/unit_tests/traits.rs` +- Test: `src/unit_tests/solvers/brute_force.rs` + +**Step 1: Write the failing tests** + +Update the focused trait and solver tests to use: + +- `type Value` +- `Solver::solve()` +- `BruteForce::find_witness()` +- `BruteForce::find_all_witnesses()` + +Example solver test skeleton: + +```rust +#[test] +fn test_solver_solves_max_value() { + let solver = BruteForce::new(); + let total = solver.solve(&problem); + assert_eq!(total, Max(Some(6))); +} +``` + +**Step 2: Run test to verify it fails** + +Run: `cargo test test_solver_solves_max_value --lib` +Expected: FAIL because `Problem::Value` / `Solver::solve` do not exist yet + +**Step 3: Write minimal implementation** + +In the listed files: + +- rename `Metric` to `Value` +- remove `OptimizationProblem` / `SatisfactionProblem` +- make `Solver` expose only `solve` +- implement witness helpers in `BruteForce` using `Aggregate::contributes_to_witnesses` + +**Step 4: Run tests to verify they pass** + +Run: `cargo test test_solver_solves_max_value --lib` +Expected: PASS + +Run: `cargo test test_solver_find_witness --lib` +Expected: PASS + +**Step 5: Commit** + +```bash +git add src/traits.rs src/solvers/mod.rs src/solvers/brute_force.rs src/unit_tests/traits.rs src/unit_tests/solvers/brute_force.rs +git commit -m "refactor: unify core solving on aggregate values" +``` + +### Task 3: Split dynamic value solving from dynamic witness solving + +**Files:** +- Modify: `src/registry/dyn_problem.rs` +- Modify: `src/registry/variant.rs` +- Modify: `problemreductions-macros/src/lib.rs` +- Test: `src/unit_tests/registry/dispatch.rs` + +**Step 1: Write the failing tests** + +Add or update tests to cover: + +- `LoadedDynProblem::solve_brute_force_value()` +- `LoadedDynProblem::solve_brute_force_witness()` +- witness solve returns `None` for an aggregate-only dummy problem + +Example test skeleton: + +```rust +#[test] +fn loaded_dyn_problem_returns_none_for_aggregate_only_witness() { + let loaded = LoadedDynProblem::new(Box::new(problem), solve_value, solve_witness); + assert!(loaded.solve_brute_force_witness().is_none()); +} +``` + +**Step 2: Run test to verify it fails** + +Run: `cargo test loaded_dyn_problem_returns_none_for_aggregate_only_witness --lib` +Expected: FAIL because `solve_brute_force_value` / `solve_brute_force_witness` do not exist + +**Step 3: Write minimal implementation** + +In the listed files: + +- replace `SolveFn` with `SolveValueFn` and `SolveWitnessFn` +- store both function pointers on `VariantEntry` +- generate both closures in `declare_variants!` +- have the generated witness closure call `BruteForce::find_witness()` + +**Step 4: Run tests to verify they pass** + +Run: `cargo test loaded_dyn_problem_returns_none_for_aggregate_only_witness --lib` +Expected: PASS + +Run: `cargo test test_load_problem_alias_uses_registry_dispatch --lib` +Expected: PASS + +**Step 5: Commit** + +```bash +git add src/registry/dyn_problem.rs src/registry/variant.rs problemreductions-macros/src/lib.rs src/unit_tests/registry/dispatch.rs +git commit -m "refactor: split dynamic value solve from witness solve" +``` + +### Task 4: Add aggregate reduction traits and runtime chain support + +**Files:** +- Modify: `src/rules/traits.rs` +- Modify: `src/rules/registry.rs` +- Modify: `src/rules/graph.rs` +- Test: `src/unit_tests/rules/traits.rs` +- Test: `src/unit_tests/rules/registry.rs` +- Test: `src/unit_tests/rules/graph.rs` + +**Step 1: Write the failing tests** + +Add tests for: + +- `AggregateReductionResult::extract_value` +- type-erased `DynAggregateReductionResult` +- aggregate chain execution over a tiny dummy two-step path + +Example skeleton: + +```rust +#[test] +fn test_aggregate_reduction_chain_extracts_value_backwards() { + assert_eq!(chain.extract_value_dyn(json!(7)), json!(3)); +} +``` + +**Step 2: Run test to verify it fails** + +Run: `cargo test test_aggregate_reduction_chain_extracts_value_backwards --lib` +Expected: FAIL because aggregate reduction traits and chain types do not exist + +**Step 3: Write minimal implementation** + +In the listed files: + +- add `AggregateReductionResult` +- add `ReduceToAggregate` +- add `DynAggregateReductionResult` +- add aggregate-chain execution alongside the existing witness chain + +**Step 4: Run tests to verify they pass** + +Run: `cargo test test_aggregate_reduction_chain_extracts_value_backwards --lib` +Expected: PASS + +**Step 5: Commit** + +```bash +git add src/rules/traits.rs src/rules/registry.rs src/rules/graph.rs src/unit_tests/rules/traits.rs src/unit_tests/rules/registry.rs src/unit_tests/rules/graph.rs +git commit -m "refactor: add aggregate reduction execution" +``` + +### Task 5: Introduce `EdgeCapabilities` and capability-aware path search + +**Files:** +- Modify: `src/rules/registry.rs` +- Modify: `src/rules/graph.rs` +- Test: `src/unit_tests/reduction_graph.rs` +- Test: `src/unit_tests/rules/graph.rs` + +**Step 1: Write the failing tests** + +Add pathfinding tests that prove: + +- witness search ignores aggregate-only edges +- aggregate search ignores witness-only edges +- natural subtype edges remain usable in both modes + +Example skeleton: + +```rust +#[test] +fn witness_path_search_rejects_aggregate_only_edge() { + assert!(graph.find_cheapest_path(..., ReductionMode::Witness, ...).is_none()); +} +``` + +**Step 2: Run test to verify it fails** + +Run: `cargo test witness_path_search_rejects_aggregate_only_edge --lib` +Expected: FAIL because `EdgeCapabilities` / `ReductionMode` do not exist + +**Step 3: Write minimal implementation** + +In the listed files: + +- add `EdgeCapabilities` +- store capabilities on `ReductionEntry` and edge data +- thread `ReductionMode` through path search and path execution +- mark natural subtype edges as `{ witness: true, aggregate: true }` + +**Step 4: Run tests to verify they pass** + +Run: `cargo test witness_path_search_rejects_aggregate_only_edge --lib` +Expected: PASS + +Run: `cargo test natural_edge_supports_both_modes --lib` +Expected: PASS + +**Step 5: Commit** + +```bash +git add src/rules/registry.rs src/rules/graph.rs src/unit_tests/reduction_graph.rs src/unit_tests/rules/graph.rs +git commit -m "refactor: make reduction paths capability-aware" +``` + +### Task 6: Update CLI solve/reduce flows and ILP gating + +**Files:** +- Modify: `problemreductions-cli/src/dispatch.rs` +- Modify: `problemreductions-cli/src/commands/solve.rs` +- Modify: `problemreductions-cli/src/commands/reduce.rs` +- Modify: `problemreductions-cli/src/mcp/tools.rs` +- Modify: `src/solvers/ilp/solver.rs` +- Test: `problemreductions-cli/tests/cli_tests.rs` +- Test: `problemreductions-cli/src/mcp/tests.rs` + +**Step 1: Write the failing tests** + +Add tests for: + +- plain `pred solve` on a value-only problem prints evaluation without `Solution:` +- bundle solve rejects aggregate-only paths +- ILP solve rejects aggregate-only source problems with a clear error + +Example CLI assertion skeleton: + +```rust +assert!(stdout.contains("Evaluation: Sum(")); +assert!(!stdout.contains("Solution:")); +``` + +**Step 2: Run test to verify it fails** + +Run: `cargo test test_solve_value_only_problem_omits_solution --package problemreductions-cli` +Expected: FAIL because CLI still assumes every solve returns a config + +**Step 3: Write minimal implementation** + +In the listed files: + +- use `solve_brute_force_value()` for plain problem solves +- print `Solution` only when `solve_brute_force_witness()` succeeds +- make `pred reduce` / bundle solve witness-only +- keep ILP witness-only and improve the user-facing error + +**Step 4: Run tests to verify they pass** + +Run: `cargo test test_solve_value_only_problem_omits_solution --package problemreductions-cli` +Expected: PASS + +Run: `cargo test test_solve_bundle --package problemreductions-cli` +Expected: PASS + +**Step 5: Commit** + +```bash +git add problemreductions-cli/src/dispatch.rs problemreductions-cli/src/commands/solve.rs problemreductions-cli/src/commands/reduce.rs problemreductions-cli/src/mcp/tools.rs src/solvers/ilp/solver.rs problemreductions-cli/tests/cli_tests.rs problemreductions-cli/src/mcp/tests.rs +git commit -m "refactor: separate value solve from witness workflows" +``` + +### Task 7: Mechanically migrate existing models, rules, and tests + +**Files:** +- Modify: files returned by `rg -l 'type Metric|OptimizationProblem|SatisfactionProblem|SolutionSize|Direction|find_best|find_satisfying|find_all_best|find_all_satisfying' src tests problemreductions-cli` + +**Step 1: Write the failing tests** + +Pick one optimization file and one satisfaction file from the `rg` output first and update their adjacent tests before doing the bulk migration. + +Suggested first pair: + +- `src/models/graph/maximum_independent_set.rs` +- `src/models/formula/sat.rs` + +Update their direct test files first: + +- `src/unit_tests/models/graph/maximum_independent_set.rs` +- `src/unit_tests/models/formula/sat.rs` + +**Step 2: Run test to verify it fails** + +Run: `cargo test maximum_independent_set --lib` +Expected: FAIL until the model/test pair is migrated + +Run: `cargo test formula::sat --lib` +Expected: FAIL until the model/test pair is migrated + +**Step 3: Write minimal implementation** + +For each migrated file: + +- `type Metric = ...` -> `type Value = ...` +- `SolutionSize::Valid(x)` -> `Max(Some(x))` or `Min(Some(x))` +- `bool` satisfaction outputs -> `Or(...)` +- `find_best` / `find_all_best` -> `find_witness` / `find_all_witnesses` where appropriate +- `find_satisfying` / `find_all_satisfying` -> same witness helpers + +Then repeat the same mechanical transformation across the remaining `rg` result set in small reviewable batches. + +**Step 4: Run tests to verify they pass** + +Run: `cargo test maximum_independent_set --lib` +Expected: PASS + +Run: `cargo test sat --lib` +Expected: PASS + +After each batch: + +Run: `make test` +Expected: PASS + +**Step 5: Commit** + +```bash +git add src tests problemreductions-cli +git commit -m "refactor: migrate models and tests to aggregate values" +``` + +### Task 8: Final verification and cleanup + +**Files:** +- Modify: any stragglers found by verification commands +- Verify: `docs/plans/2026-03-22-generalized-aggregation-design.md` + +**Step 1: Write the failing checks** + +Use grep-style sweeps to confirm the removed surface is really gone: + +```bash +rg 'OptimizationProblem|SatisfactionProblem|SolutionSize|Direction|type Metric' src tests problemreductions-cli +``` + +**Step 2: Run checks to verify they fail before cleanup** + +Run the command above before the last cleanup pass. +Expected: remaining hits identify unfinished migration work + +**Step 3: Write minimal cleanup** + +- remove the final stale references +- align docs/comments/examples with the new witness/value vocabulary +- verify graph export includes both `witness` and `aggregate` booleans + +**Step 4: Run verification to verify it passes** + +Run: + +```bash +make fmt-check +make test +make clippy +rg 'OptimizationProblem|SatisfactionProblem|SolutionSize|Direction|type Metric' src tests problemreductions-cli +``` + +Expected: + +- `make fmt-check` passes +- `make test` passes +- `make clippy` passes +- final `rg` finds no remaining production-code references + +**Step 5: Commit** + +```bash +git add src tests problemreductions-cli docs/plans/2026-03-22-generalized-aggregation-design.md +git commit -m "refactor: finish generalized aggregation migration" +``` diff --git a/problemreductions-cli/src/commands/reduce.rs b/problemreductions-cli/src/commands/reduce.rs index 2fc5c9be5..7e579ed67 100644 --- a/problemreductions-cli/src/commands/reduce.rs +++ b/problemreductions-cli/src/commands/reduce.rs @@ -5,7 +5,9 @@ use crate::dispatch::{ use crate::output::OutputConfig; use crate::problem_name::resolve_problem_ref; use anyhow::{Context, Result}; -use problemreductions::rules::{MinimizeSteps, ReductionGraph, ReductionPath, ReductionStep}; +use problemreductions::rules::{ + MinimizeSteps, ReductionGraph, ReductionMode, ReductionPath, ReductionStep, +}; use problemreductions::types::ProblemSize; use std::collections::BTreeMap; use std::path::Path; @@ -112,18 +114,19 @@ pub fn reduce( // Auto-discover cheapest path let input_size = ProblemSize::new(vec![]); - let best_path = graph.find_cheapest_path( + let best_path = graph.find_cheapest_path_mode( source_name, &source_variant, &dst_ref.name, &dst_ref.variant, + ReductionMode::Witness, &input_size, &MinimizeSteps, ); best_path.ok_or_else(|| { anyhow::anyhow!( - "No reduction path from {} to {}\n\n\ + "No witness-capable reduction path from {} to {}\n\n\ Hint: generate a path file first, then pass it with --via:\n\ pred path {} {} -o path.json\n\ pred reduce {} --via path.json -o reduced.json", @@ -139,7 +142,11 @@ pub fn reduce( // 4. Execute reduction chain via reduce_along_path let chain = graph .reduce_along_path(&reduction_path, source.as_any()) - .ok_or_else(|| anyhow::anyhow!("Failed to execute reduction chain"))?; + .ok_or_else(|| { + anyhow::anyhow!( + "Reduction bundles require witness-capable paths; this path cannot produce a recoverable witness." + ) + })?; // 5. Serialize target let target_step = reduction_path.steps.last().unwrap(); diff --git a/problemreductions-cli/src/commands/solve.rs b/problemreductions-cli/src/commands/solve.rs index 663ee33b3..050badaf2 100644 --- a/problemreductions-cli/src/commands/solve.rs +++ b/problemreductions-cli/src/commands/solve.rs @@ -29,6 +29,42 @@ fn parse_input(path: &Path) -> Result { } } +fn solve_result_text(problem: &str, solver: &str, result: &crate::dispatch::SolveResult) -> String { + let mut text = format!("Problem: {}\nSolver: {}", problem, solver); + if let Some(config) = &result.config { + text.push_str(&format!("\nSolution: {:?}", config)); + } + text.push_str(&format!("\nEvaluation: {}", result.evaluation)); + text +} + +fn solve_result_json( + problem: &str, + solver: &str, + result: &crate::dispatch::SolveResult, +) -> serde_json::Value { + let mut json = serde_json::json!({ + "problem": problem, + "solver": solver, + "evaluation": result.evaluation, + }); + if let Some(config) = &result.config { + json["solution"] = serde_json::json!(config); + } + json +} + +fn plain_problem_output( + problem: &str, + solver: &str, + result: &crate::dispatch::SolveResult, +) -> (String, serde_json::Value) { + ( + solve_result_text(problem, solver, result), + solve_result_json(problem, solver, result), + ) +} + pub fn solve(input: &Path, solver_name: &str, timeout: u64, out: &OutputConfig) -> Result<()> { if solver_name != "brute-force" && solver_name != "ilp" { anyhow::bail!( @@ -79,17 +115,8 @@ fn solve_problem( match solver_name { "brute-force" => { - let result = problem.solve_brute_force()?; - let text = format!( - "Problem: {}\nSolver: brute-force\nSolution: {:?}\nEvaluation: {}", - name, result.config, result.evaluation, - ); - let json = serde_json::json!({ - "problem": name, - "solver": "brute-force", - "solution": result.config, - "evaluation": result.evaluation, - }); + let result = problem.solve_brute_force(); + let (text, json) = plain_problem_output(name, "brute-force", &result); let result = out.emit_with_default_name("", &text, &json); if out.output.is_none() && crate::output::stderr_is_tty() { out.info("\nHint: use -o to save full solution details as JSON."); @@ -103,16 +130,12 @@ fn solve_problem( } else { "ilp (via ILP)".to_string() }; - let text = format!( - "Problem: {}\nSolver: {}\nSolution: {:?}\nEvaluation: {}", - name, solver_desc, result.config, result.evaluation, - ); - let mut json = serde_json::json!({ - "problem": name, - "solver": "ilp", - "solution": result.config, - "evaluation": result.evaluation, - }); + let result = crate::dispatch::SolveResult { + config: Some(result.config), + evaluation: result.evaluation, + }; + let text = solve_result_text(name, &solver_desc, &result); + let mut json = solve_result_json(name, "ilp", &result); if name != "ILP" { json["reduced_to"] = serde_json::json!("ILP"); } @@ -138,7 +161,12 @@ fn solve_bundle(bundle: ReductionBundle, solver_name: &str, out: &OutputConfig) // 2. Solve the target problem let target_result = match solver_name { - "brute-force" => target.solve_brute_force()?, + "brute-force" => target.solve_brute_force_witness().ok_or_else(|| { + anyhow::anyhow!( + "Bundle solving requires a witness-capable target problem and witness-capable reduction path; {} only supports aggregate-value solving.", + target_name + ) + })?, "ilp" => target.solve_with_ilp().map_err(add_ilp_solver_hint)?, _ => unreachable!(), }; @@ -167,9 +195,9 @@ fn solve_bundle(bundle: ReductionBundle, solver_name: &str, out: &OutputConfig) let chain = graph .reduce_along_path(&reduction_path, source.as_any()) - .ok_or_else(|| { - anyhow::anyhow!("Failed to re-execute reduction chain for solution extraction") - })?; + .ok_or_else(|| anyhow::anyhow!( + "Bundle solving requires a witness-capable reduction path; this bundle cannot recover a source solution." + ))?; // 4. Extract solution back to source problem space let source_config = chain.extract_solution(&target_result.config); @@ -207,7 +235,48 @@ fn add_ilp_solver_hint(err: anyhow::Error) -> anyhow::Error { anyhow::anyhow!( "{message}\n\nHint: try `--solver brute-force` for direct exhaustive search on small instances." ) + } else if message.contains("witness-capable") { + anyhow::anyhow!( + "{message}\n\nHint: try `--solver brute-force` for direct exhaustive search on small instances." + ) } else { err } } + +#[cfg(test)] +mod tests { + use super::*; + use crate::dispatch::SolveResult; + use crate::output::OutputConfig; + use crate::test_support::aggregate_bundle; + + #[test] + fn test_solve_value_only_problem_omits_solution() { + let result = SolveResult { + config: None, + evaluation: "Sum(56)".to_string(), + }; + let (text, json) = plain_problem_output("CliTestAggregateValueSource", "brute-force", &result); + assert!(text.contains("Evaluation: Sum(56)"), "{text}"); + assert!(!text.contains("Solution:"), "{text}"); + assert!(json.get("solution").is_none(), "{json}"); + } + + #[test] + fn test_solve_bundle_rejects_aggregate_only_path() { + let bundle = aggregate_bundle(); + let out = OutputConfig { + output: None, + quiet: true, + json: false, + auto_json: false, + }; + + let err = solve_bundle(bundle, "brute-force", &out).unwrap_err(); + assert!( + err.to_string().contains("witness"), + "unexpected error: {err}" + ); + } +} diff --git a/problemreductions-cli/src/dispatch.rs b/problemreductions-cli/src/dispatch.rs index 53774c0f3..7a66c7fc7 100644 --- a/problemreductions-cli/src/dispatch.rs +++ b/problemreductions-cli/src/dispatch.rs @@ -1,6 +1,6 @@ use anyhow::{Context, Result}; use problemreductions::registry::{DynProblem, LoadedDynProblem}; -use problemreductions::rules::{MinimizeSteps, ReductionGraph}; +use problemreductions::rules::{MinimizeSteps, ReductionGraph, ReductionMode}; use problemreductions::solvers::ILPSolver; use problemreductions::types::ProblemSize; use serde_json::Value; @@ -37,12 +37,19 @@ impl std::ops::Deref for LoadedProblem { } impl LoadedProblem { - pub fn solve_brute_force(&self) -> Result { - let (config, evaluation) = self - .inner - .solve_brute_force() - .ok_or_else(|| anyhow::anyhow!("No solution found"))?; - Ok(SolveResult { config, evaluation }) + pub fn solve_brute_force_value(&self) -> String { + self.inner.solve_brute_force_value() + } + + pub fn solve_brute_force_witness(&self) -> Option { + let (config, evaluation) = self.inner.solve_brute_force_witness()?; + Some(WitnessSolveResult { config, evaluation }) + } + + pub fn solve_brute_force(&self) -> SolveResult { + let evaluation = self.solve_brute_force_value(); + let config = self.solve_brute_force_witness().map(|result| result.config); + SolveResult { config, evaluation } } pub fn supports_ilp_solver(&self) -> bool { @@ -54,28 +61,39 @@ impl LoadedProblem { let input_size = ProblemSize::new(vec![]); ilp_variants.iter().any(|dv| { graph - .find_cheapest_path(name, &variant, "ILP", dv, &input_size, &MinimizeSteps) + .find_cheapest_path_mode( + name, + &variant, + "ILP", + dv, + ReductionMode::Witness, + &input_size, + &MinimizeSteps, + ) .is_some() }) } } + #[cfg_attr(not(feature = "mcp"), allow(dead_code))] + pub fn available_solvers(&self) -> Vec<&'static str> { + let mut solvers = vec!["brute-force"]; + if self.supports_ilp_solver() { + solvers.push("ilp"); + } + solvers + } + /// Solve using the ILP solver. If the problem is not ILP, auto-reduce to ILP first. - pub fn solve_with_ilp(&self) -> Result { + pub fn solve_with_ilp(&self) -> Result { let name = self.problem_name(); let variant = self.variant_map(); let solver = ILPSolver::new(); let config = solver - .solve_via_reduction(name, &variant, self.as_any()) - .ok_or_else(|| { - anyhow::anyhow!( - "No reduction path from {} to ILP or ILP solver found no solution. \ - Try `--solver brute-force`.", - name - ) - })?; + .try_solve_via_reduction(name, &variant, self.as_any()) + .map_err(|err| anyhow::anyhow!(err))?; let evaluation = self.evaluate_dyn(&config); - Ok(SolveResult { config, evaluation }) + Ok(WitnessSolveResult { config, evaluation }) } } @@ -140,7 +158,17 @@ pub struct PathStep { } /// Result of solving a problem. +#[derive(Debug, Clone, PartialEq, Eq)] pub struct SolveResult { + /// The solution configuration when the problem supports witness extraction. + pub config: Option>, + /// Evaluation of the solution. + pub evaluation: String, +} + +/// Result of solving a witness-capable problem. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct WitnessSolveResult { /// The solution configuration. pub config: Vec, /// Evaluation of the solution. @@ -150,6 +178,7 @@ pub struct SolveResult { #[cfg(test)] mod tests { use super::*; + use crate::test_support::{AggregateValueSource, AGGREGATE_SOURCE_NAME}; use problemreductions::models::graph::MaximumIndependentSet; use problemreductions::models::misc::BinPacking; use problemreductions::topology::SimpleGraph; @@ -226,4 +255,34 @@ mod tests { "unexpected error: {err}" ); } + + #[test] + fn test_solve_brute_force_value_only_problem_has_no_witness() { + let loaded = load_problem( + AGGREGATE_SOURCE_NAME, + &BTreeMap::new(), + serde_json::to_value(AggregateValueSource::sample()).unwrap(), + ) + .unwrap(); + + let result = loaded.solve_brute_force(); + assert_eq!(result.config, None); + assert_eq!(result.evaluation, "Sum(56)"); + } + + #[test] + fn test_solve_with_ilp_rejects_aggregate_only_problem() { + let loaded = load_problem( + AGGREGATE_SOURCE_NAME, + &BTreeMap::new(), + serde_json::to_value(AggregateValueSource::sample()).unwrap(), + ) + .unwrap(); + + let err = loaded.solve_with_ilp().unwrap_err(); + assert!( + err.to_string().contains("witness-capable"), + "unexpected error: {err}" + ); + } } diff --git a/problemreductions-cli/src/main.rs b/problemreductions-cli/src/main.rs index b1f75d20f..ce4362132 100644 --- a/problemreductions-cli/src/main.rs +++ b/problemreductions-cli/src/main.rs @@ -5,6 +5,8 @@ mod dispatch; mod mcp; mod output; mod problem_name; +#[cfg(test)] +mod test_support; mod util; use clap::{CommandFactory, Parser}; diff --git a/problemreductions-cli/src/mcp/tests.rs b/problemreductions-cli/src/mcp/tests.rs index 715588dcd..117dd82c5 100644 --- a/problemreductions-cli/src/mcp/tests.rs +++ b/problemreductions-cli/src/mcp/tests.rs @@ -1,6 +1,7 @@ #[cfg(test)] mod tests { use crate::mcp::tools::McpServer; + use crate::test_support::{aggregate_bundle, aggregate_problem_json}; #[test] fn test_list_problems_returns_json() { @@ -430,4 +431,42 @@ mod tests { let json: serde_json::Value = serde_json::from_str(&result.unwrap()).unwrap(); assert_eq!(json["solver"], "brute-force"); } + + #[test] + fn test_reduce_rejects_aggregate_only_path() { + let server = McpServer::new(); + let result = server.reduce_inner(&aggregate_problem_json(), "CliTestAggregateValueTarget"); + assert!(result.is_err()); + let err = result.unwrap_err().to_string(); + assert!(err.contains("witness"), "unexpected error: {err}"); + } + + #[test] + fn test_solve_aggregate_only_problem_omits_solution() { + let server = McpServer::new(); + let result = server.solve_inner(&aggregate_problem_json(), Some("brute-force"), None); + assert!(result.is_ok()); + let json: serde_json::Value = serde_json::from_str(&result.unwrap()).unwrap(); + assert_eq!(json["evaluation"], "Sum(56)"); + assert!(json.get("solution").is_none(), "{json}"); + } + + #[test] + fn test_solve_ilp_rejects_aggregate_only_problem() { + let server = McpServer::new(); + let result = server.solve_inner(&aggregate_problem_json(), Some("ilp"), None); + assert!(result.is_err()); + let err = result.unwrap_err().to_string(); + assert!(err.contains("witness-capable"), "unexpected error: {err}"); + } + + #[test] + fn test_solve_bundle_rejects_aggregate_only_path() { + let server = McpServer::new(); + let bundle_json = serde_json::to_string(&aggregate_bundle()).unwrap(); + let result = server.solve_inner(&bundle_json, Some("brute-force"), None); + assert!(result.is_err()); + let err = result.unwrap_err().to_string(); + assert!(err.contains("witness"), "unexpected error: {err}"); + } } diff --git a/problemreductions-cli/src/mcp/tools.rs b/problemreductions-cli/src/mcp/tools.rs index 1172c36cb..045e10292 100644 --- a/problemreductions-cli/src/mcp/tools.rs +++ b/problemreductions-cli/src/mcp/tools.rs @@ -7,7 +7,9 @@ use problemreductions::models::graph::{ }; use problemreductions::models::misc::Factoring; use problemreductions::registry::collect_schemas; -use problemreductions::rules::{CustomCost, MinimizeSteps, ReductionGraph, TraversalDirection}; +use problemreductions::rules::{ + CustomCost, MinimizeSteps, ReductionGraph, ReductionMode, TraversalDirection, +}; use problemreductions::topology::{ Graph, KingsSubgraph, SimpleGraph, TriangularSubgraph, UnitDiskGraph, }; @@ -811,23 +813,32 @@ impl McpServer { // Auto-discover cheapest path let input_size = ProblemSize::new(vec![]); - let best_path = graph.find_cheapest_path( + let best_path = graph.find_cheapest_path_mode( source_name, &source_variant, &dst_ref.name, &dst_ref.variant, + ReductionMode::Witness, &input_size, &MinimizeSteps, ); let reduction_path = best_path.ok_or_else(|| { - anyhow::anyhow!("No reduction path from {} to {}", source_name, dst_ref.name) + anyhow::anyhow!( + "No witness-capable reduction path from {} to {}", + source_name, + dst_ref.name + ) })?; // Execute reduction chain let chain = graph .reduce_along_path(&reduction_path, source.as_any()) - .ok_or_else(|| anyhow::anyhow!("Failed to execute reduction chain"))?; + .ok_or_else(|| { + anyhow::anyhow!( + "Reduction bundles require witness-capable paths; this path cannot produce a recoverable witness." + ) + })?; // Serialize target let target_step = reduction_path.steps.last().unwrap(); @@ -1147,6 +1158,22 @@ fn ser(problem: T) -> anyhow::Result { util::ser(problem) } +fn solve_result_json( + problem: &str, + solver: &str, + result: &crate::dispatch::SolveResult, +) -> serde_json::Value { + let mut json = serde_json::json!({ + "problem": problem, + "solver": solver, + "evaluation": result.evaluation, + }); + if let Some(config) = &result.config { + json["solution"] = serde_json::json!(config); + } + json +} + fn variant_map(pairs: &[(&str, &str)]) -> BTreeMap { util::variant_map(pairs) } @@ -1445,23 +1472,17 @@ fn solve_problem_inner( match solver_name { "brute-force" => { - let result = problem.solve_brute_force()?; - let json = serde_json::json!({ - "problem": name, - "solver": "brute-force", - "solution": result.config, - "evaluation": result.evaluation, - }); + let result = problem.solve_brute_force(); + let json = solve_result_json(name, "brute-force", &result); Ok(serde_json::to_string_pretty(&json)?) } "ilp" => { let result = problem.solve_with_ilp()?; - let mut json = serde_json::json!({ - "problem": name, - "solver": "ilp", - "solution": result.config, - "evaluation": result.evaluation, - }); + let result = crate::dispatch::SolveResult { + config: Some(result.config), + evaluation: result.evaluation, + }; + let mut json = solve_result_json(name, "ilp", &result); if name != "ILP" { json["reduced_to"] = serde_json::json!("ILP"); } @@ -1481,7 +1502,12 @@ fn solve_bundle_inner(bundle: ReductionBundle, solver_name: &str) -> anyhow::Res let target_name = target.problem_name(); let target_result = match solver_name { - "brute-force" => target.solve_brute_force()?, + "brute-force" => target.solve_brute_force_witness().ok_or_else(|| { + anyhow::anyhow!( + "Bundle solving requires a witness-capable target problem and witness-capable reduction path; {} only supports aggregate-value solving.", + target_name + ) + })?, "ilp" => target.solve_with_ilp()?, _ => unreachable!(), }; @@ -1508,9 +1534,9 @@ fn solve_bundle_inner(bundle: ReductionBundle, solver_name: &str) -> anyhow::Res let chain = graph .reduce_along_path(&reduction_path, source.as_any()) - .ok_or_else(|| { - anyhow::anyhow!("Failed to re-execute reduction chain for solution extraction") - })?; + .ok_or_else(|| anyhow::anyhow!( + "Bundle solving requires a witness-capable reduction path; this bundle cannot recover a source solution." + ))?; let source_config = chain.extract_solution(&target_result.config); let source_eval = source.evaluate_dyn(&source_config); diff --git a/problemreductions-cli/src/test_support.rs b/problemreductions-cli/src/test_support.rs new file mode 100644 index 000000000..17d847a19 --- /dev/null +++ b/problemreductions-cli/src/test_support.rs @@ -0,0 +1,241 @@ +use crate::dispatch::{PathStep, ProblemJsonOutput, ReductionBundle}; +use problemreductions::models::algebraic::{ILP, ObjectiveSense}; +use problemreductions::registry::VariantEntry; +use problemreductions::rules::registry::{EdgeCapabilities, ReductionEntry, ReductionOverhead}; +use problemreductions::rules::{AggregateReductionResult, ReductionAutoCast}; +use problemreductions::solvers::{BruteForce, Solver}; +use problemreductions::traits::Problem; +use problemreductions::types::{ProblemSize, SolutionSize, Sum}; +use serde::{Deserialize, Serialize}; +use std::any::Any; +use std::collections::BTreeMap; + +pub(crate) const AGGREGATE_SOURCE_NAME: &str = "CliTestAggregateValueSource"; +pub(crate) const AGGREGATE_TARGET_NAME: &str = "CliTestAggregateValueTarget"; + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub(crate) struct AggregateValueSource { + values: Vec, +} + +impl AggregateValueSource { + pub(crate) fn sample() -> Self { + Self { + values: vec![2, 5, 7], + } + } +} + +impl Problem for AggregateValueSource { + const NAME: &'static str = AGGREGATE_SOURCE_NAME; + type Value = Sum; + + fn dims(&self) -> Vec { + vec![2; self.values.len()] + } + + fn evaluate(&self, config: &[usize]) -> Self::Value { + let total = self + .values + .iter() + .zip(config.iter().copied()) + .filter_map(|(value, bit)| (bit == 1).then_some(*value)) + .sum(); + Sum(total) + } + + fn variant() -> Vec<(&'static str, &'static str)> { + vec![] + } +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub(crate) struct AggregateValueTarget { + base: u64, +} + +impl AggregateValueTarget { + pub(crate) fn sample() -> Self { + Self { base: 11 } + } +} + +impl Problem for AggregateValueTarget { + const NAME: &'static str = AGGREGATE_TARGET_NAME; + type Value = Sum; + + fn dims(&self) -> Vec { + vec![2] + } + + fn evaluate(&self, config: &[usize]) -> Self::Value { + Sum(self.base + config.iter().sum::() as u64) + } + + fn variant() -> Vec<(&'static str, &'static str)> { + vec![] + } +} + +#[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: SolutionSize) -> Sum { + Sum(0) + } +} + +fn solve_value

(any: &dyn Any) -> String +where + P: Problem + Serialize + 'static, + P::Value: problemreductions::types::Aggregate, +{ + let problem = any + .downcast_ref::

() + .expect("test solve_value downcast failed"); + let solver = BruteForce::new(); + format!("{:?}", solver.solve(problem)) +} + +fn solve_witness

(any: &dyn Any) -> Option<(Vec, String)> +where + P: Problem + Serialize + 'static, + P::Value: problemreductions::types::Aggregate, +{ + let problem = any.downcast_ref::

()?; + let solver = BruteForce::new(); + let config = solver.find_witness(problem)?; + let evaluation = format!("{:?}", problem.evaluate(&config)); + Some((config, evaluation)) +} + +problemreductions::inventory::submit! { + VariantEntry { + name: AggregateValueSource::NAME, + variant_fn: AggregateValueSource::variant, + complexity: "2^num_values", + complexity_eval_fn: |_| 1.0, + is_default: true, + factory: |data| { + let problem: AggregateValueSource = serde_json::from_value(data)?; + Ok(Box::new(problem)) + }, + serialize_fn: |any| { + let problem = any.downcast_ref::()?; + Some(serde_json::to_value(problem).expect("serialize AggregateValueSource failed")) + }, + solve_value_fn: solve_value::, + solve_witness_fn: solve_witness::, + } +} + +problemreductions::inventory::submit! { + VariantEntry { + name: AggregateValueTarget::NAME, + variant_fn: AggregateValueTarget::variant, + complexity: "2", + complexity_eval_fn: |_| 1.0, + is_default: true, + factory: |data| { + let problem: AggregateValueTarget = serde_json::from_value(data)?; + Ok(Box::new(problem)) + }, + serialize_fn: |any| { + let problem = any.downcast_ref::()?; + Some(serde_json::to_value(problem).expect("serialize AggregateValueTarget failed")) + }, + solve_value_fn: solve_value::, + solve_witness_fn: solve_witness::, + } +} + +problemreductions::inventory::submit! { + ReductionEntry { + source_name: AggregateValueSource::NAME, + target_name: AggregateValueTarget::NAME, + source_variant_fn: AggregateValueSource::variant, + target_variant_fn: AggregateValueTarget::variant, + overhead_fn: || ReductionOverhead::default(), + module_path: module_path!(), + reduce_fn: None, + reduce_aggregate_fn: Some(|any: &dyn Any| { + let source = any + .downcast_ref::() + .expect("aggregate reduction downcast failed"); + Box::new(ReductionAutoCast::::new( + AggregateValueTarget { + base: source.values.iter().sum(), + }, + )) + }), + capabilities: EdgeCapabilities::aggregate_only(), + overhead_eval_fn: |_| ProblemSize::new(vec![]), + } +} + +problemreductions::inventory::submit! { + ReductionEntry { + source_name: AggregateValueSource::NAME, + target_name: ILP::::NAME, + source_variant_fn: AggregateValueSource::variant, + target_variant_fn: ILP::::variant, + overhead_fn: || ReductionOverhead::default(), + 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"); + Box::new(AggregateValueToIlpReduction { + target: ILP::new(0, vec![], vec![], ObjectiveSense::Minimize), + }) + }), + capabilities: EdgeCapabilities::aggregate_only(), + overhead_eval_fn: |_| ProblemSize::new(vec![]), + } +} + +#[cfg_attr(not(feature = "mcp"), allow(dead_code))] +pub(crate) fn aggregate_problem_json() -> String { + serde_json::to_string(&ProblemJsonOutput { + problem_type: AggregateValueSource::NAME.to_string(), + variant: BTreeMap::new(), + data: serde_json::to_value(AggregateValueSource::sample()).unwrap(), + }) + .unwrap() +} + +pub(crate) fn aggregate_bundle() -> ReductionBundle { + ReductionBundle { + source: ProblemJsonOutput { + problem_type: AggregateValueSource::NAME.to_string(), + variant: BTreeMap::new(), + data: serde_json::to_value(AggregateValueSource::sample()).unwrap(), + }, + target: ProblemJsonOutput { + problem_type: AggregateValueTarget::NAME.to_string(), + variant: BTreeMap::new(), + data: serde_json::to_value(AggregateValueTarget::sample()).unwrap(), + }, + path: vec![ + PathStep { + name: AggregateValueSource::NAME.to_string(), + variant: BTreeMap::new(), + }, + PathStep { + name: AggregateValueTarget::NAME.to_string(), + variant: BTreeMap::new(), + }, + ], + } +} diff --git a/src/solvers/ilp/solver.rs b/src/solvers/ilp/solver.rs index a2ba9b985..995d08370 100644 --- a/src/solvers/ilp/solver.rs +++ b/src/solvers/ilp/solver.rs @@ -2,7 +2,7 @@ use crate::models::algebraic::{Comparison, ObjectiveSense, VariableDomain, ILP}; use crate::models::misc::TimetableDesign; -use crate::rules::{ReduceTo, ReductionResult}; +use crate::rules::{ReduceTo, ReductionMode, ReductionResult}; #[cfg(not(feature = "ilp-highs"))] use good_lp::default_solver; #[cfg(feature = "ilp-highs")] @@ -40,6 +40,33 @@ pub struct ILPSolver { pub time_limit: Option, } +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum SolveViaReductionError { + WitnessPathRequired { name: String }, + NoReductionPath { name: String }, + NoSolution { name: String }, +} + +impl std::fmt::Display for SolveViaReductionError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + SolveViaReductionError::WitnessPathRequired { name } => write!( + f, + "ILP solving requires a witness-capable source problem and reduction path; only aggregate-value solving is available for {}.", + name + ), + SolveViaReductionError::NoReductionPath { name } => { + write!(f, "No reduction path from {} to ILP", name) + } + SolveViaReductionError::NoSolution { name } => { + write!(f, "ILP solver found no solution for {}", name) + } + } + } +} + +impl std::error::Error for SolveViaReductionError {} + impl ILPSolver { /// Create a new ILP solver with default settings. pub fn new() -> Self { @@ -188,36 +215,33 @@ impl ILPSolver { None } - /// Solve a type-erased problem by finding a reduction path to ILP. - /// - /// Tries all ILP variants, picks the cheapest path, reduces, solves, - /// and extracts the solution back. Falls back to direct ILP solve if - /// the problem is already an ILP type. - /// - /// Returns `None` if no path to ILP exists or the solver finds no solution. - pub fn solve_via_reduction( + fn supports_direct_dyn(&self, any: &dyn std::any::Any) -> bool { + any.is::>() || any.is::>() || any.is::() + } + + fn best_path_to_ilp( &self, + graph: &crate::rules::ReductionGraph, name: &str, variant: &std::collections::BTreeMap, - instance: &dyn std::any::Any, - ) -> Option> { - // Direct ILP solve if the problem is already ILP - if let Some(config) = self.solve_dyn(instance) { - return Some(config); - } - - use crate::rules::{MinimizeSteps, ReductionGraph}; + mode: ReductionMode, + ) -> Option { use crate::types::ProblemSize; - let graph = ReductionGraph::new(); let ilp_variants = graph.variants_for("ILP"); let input_size = ProblemSize::new(vec![]); - let mut best_path = None; + for dv in &ilp_variants { - if let Some(path) = - graph.find_cheapest_path(name, variant, "ILP", dv, &input_size, &MinimizeSteps) - { + if let Some(path) = graph.find_cheapest_path_mode( + name, + variant, + "ILP", + dv, + mode, + &input_size, + &crate::rules::MinimizeSteps, + ) { let is_better = best_path .as_ref() .is_none_or(|current: &crate::rules::ReductionPath| path.len() < current.len()); @@ -227,10 +251,67 @@ impl ILPSolver { } } - let path = best_path?; - let chain = graph.reduce_along_path(&path, instance)?; - let ilp_solution = self.solve_dyn(chain.target_problem_any())?; - Some(chain.extract_solution(&ilp_solution)) + best_path + } + + pub fn try_solve_via_reduction( + &self, + name: &str, + variant: &std::collections::BTreeMap, + instance: &dyn std::any::Any, + ) -> Result, SolveViaReductionError> { + if self.supports_direct_dyn(instance) { + return self + .solve_dyn(instance) + .ok_or_else(|| SolveViaReductionError::NoSolution { + name: name.to_string(), + }); + } + + let graph = crate::rules::ReductionGraph::new(); + + let Some(path) = self.best_path_to_ilp(&graph, name, variant, ReductionMode::Witness) else { + if self + .best_path_to_ilp(&graph, name, variant, ReductionMode::Aggregate) + .is_some() + { + return Err(SolveViaReductionError::WitnessPathRequired { + name: name.to_string(), + }); + } + + return Err(SolveViaReductionError::NoReductionPath { + name: name.to_string(), + }); + }; + + let chain = graph.reduce_along_path(&path, instance).ok_or_else(|| { + SolveViaReductionError::WitnessPathRequired { + name: name.to_string(), + } + })?; + let ilp_solution = + self.solve_dyn(chain.target_problem_any()) + .ok_or_else(|| SolveViaReductionError::NoSolution { + name: name.to_string(), + })?; + Ok(chain.extract_solution(&ilp_solution)) + } + + /// Solve a type-erased problem by finding a reduction path to ILP. + /// + /// Tries all ILP variants, picks the cheapest path, reduces, solves, + /// and extracts the solution back. Falls back to direct ILP solve if + /// the problem is already an ILP type. + /// + /// Returns `None` if no path to ILP exists or the solver finds no solution. + pub fn solve_via_reduction( + &self, + name: &str, + variant: &std::collections::BTreeMap, + instance: &dyn std::any::Any, + ) -> Option> { + self.try_solve_via_reduction(name, variant, instance).ok() } } From 4c086fbc9ff7b2aed2b908b6bb23be296b6fc258 Mon Sep 17 00:00:00 2001 From: GiggleLiu Date: Mon, 23 Mar 2026 11:56:16 +0800 Subject: [PATCH 07/18] refactor: finish generalized aggregation migration --- problemreductions-cli/src/commands/graph.rs | 16 +- problemreductions-cli/src/commands/solve.rs | 3 +- problemreductions-cli/src/mcp/tools.rs | 12 +- problemreductions-cli/src/test_support.rs | 6 +- problemreductions-macros/src/lib.rs | 79 ++++--- src/lib.rs | 15 +- src/models/algebraic/bmf.rs | 20 +- .../algebraic/closest_vector_problem.rs | 18 +- .../consecutive_block_minimization.rs | 6 +- .../algebraic/consecutive_ones_submatrix.rs | 6 +- src/models/algebraic/ilp.rs | 32 ++- src/models/algebraic/quadratic_assignment.rs | 26 +-- src/models/algebraic/qubo.rs | 20 +- .../algebraic/sparse_matrix_compression.rs | 4 +- src/models/formula/circuit.rs | 6 +- src/models/formula/ksat.rs | 6 +- src/models/formula/nae_satisfiability.rs | 4 +- src/models/formula/qbf.rs | 4 +- src/models/formula/sat.rs | 6 +- src/models/graph/acyclic_partition.rs | 7 +- .../balanced_complete_bipartite_subgraph.rs | 4 +- src/models/graph/biclique_cover.rs | 22 +- .../graph/biconnectivity_augmentation.rs | 4 +- .../graph/bottleneck_traveling_salesman.rs | 22 +- .../bounded_component_spanning_forest.rs | 4 +- .../directed_two_commodity_integral_flow.rs | 6 +- src/models/graph/disjoint_connecting_paths.rs | 4 +- src/models/graph/generalized_hex.rs | 4 +- src/models/graph/graph_partitioning.rs | 30 +-- src/models/graph/hamiltonian_circuit.rs | 6 +- src/models/graph/hamiltonian_path.rs | 6 +- src/models/graph/integral_flow_bundles.rs | 4 +- .../graph/integral_flow_homologous_arcs.rs | 4 +- .../graph/integral_flow_with_multipliers.rs | 4 +- src/models/graph/isomorphic_spanning_tree.rs | 6 +- src/models/graph/kclique.rs | 4 +- src/models/graph/kcoloring.rs | 6 +- src/models/graph/kth_best_spanning_tree.rs | 4 +- .../graph/length_bounded_disjoint_paths.rs | 4 +- src/models/graph/longest_circuit.rs | 4 +- src/models/graph/longest_path.rs | 20 +- src/models/graph/max_cut.rs | 24 +- src/models/graph/maximal_is.rs | 22 +- src/models/graph/maximum_clique.rs | 22 +- src/models/graph/maximum_independent_set.rs | 24 +- src/models/graph/maximum_matching.rs | 22 +- src/models/graph/min_max_multicenter.rs | 6 +- .../graph/minimum_cut_into_bounded_sets.rs | 4 +- src/models/graph/minimum_dominating_set.rs | 22 +- .../graph/minimum_dummy_activities_pert.rs | 24 +- src/models/graph/minimum_feedback_arc_set.rs | 22 +- .../graph/minimum_feedback_vertex_set.rs | 24 +- src/models/graph/minimum_multiway_cut.rs | 20 +- src/models/graph/minimum_sum_multicenter.rs | 24 +- src/models/graph/minimum_vertex_cover.rs | 22 +- src/models/graph/mixed_chinese_postman.rs | 4 +- src/models/graph/multiple_choice_branching.rs | 4 +- .../graph/multiple_copy_file_allocation.rs | 4 +- .../graph/optimal_linear_arrangement.rs | 6 +- .../graph/partition_into_paths_of_length_2.rs | 6 +- src/models/graph/partition_into_triangles.rs | 6 +- .../graph/path_constrained_network_flow.rs | 4 +- src/models/graph/rooted_tree_arrangement.rs | 4 +- src/models/graph/rural_postman.rs | 4 +- .../graph/shortest_weight_constrained_path.rs | 4 +- src/models/graph/spin_glass.rs | 20 +- src/models/graph/steiner_tree.rs | 20 +- src/models/graph/steiner_tree_in_graphs.rs | 24 +- .../graph/strong_connectivity_augmentation.rs | 4 +- src/models/graph/subgraph_isomorphism.rs | 6 +- src/models/graph/traveling_salesman.rs | 20 +- .../graph/undirected_flow_lower_bounds.rs | 4 +- .../undirected_two_commodity_integral_flow.rs | 4 +- src/models/misc/additional_key.rs | 6 +- src/models/misc/bin_packing.rs | 22 +- .../misc/boyce_codd_normal_form_violation.rs | 4 +- src/models/misc/capacity_assignment.rs | 4 +- src/models/misc/conjunctive_boolean_query.rs | 6 +- .../misc/conjunctive_query_foldability.rs | 6 +- ...onsistency_of_database_frequency_tables.rs | 4 +- src/models/misc/ensemble_computation.rs | 4 +- src/models/misc/expected_retrieval_cost.rs | 4 +- src/models/misc/factoring.rs | 20 +- src/models/misc/flow_shop_scheduling.rs | 6 +- src/models/misc/knapsack.rs | 26 +-- src/models/misc/longest_common_subsequence.rs | 4 +- .../misc/minimum_tardiness_sequencing.rs | 26 +-- src/models/misc/multiprocessor_scheduling.rs | 6 +- src/models/misc/paintshop.rs | 20 +- src/models/misc/partially_ordered_knapsack.rs | 28 +-- src/models/misc/partition.rs | 6 +- .../misc/precedence_constrained_scheduling.rs | 6 +- .../misc/rectilinear_picture_compression.rs | 6 +- .../misc/resource_constrained_scheduling.rs | 6 +- .../scheduling_with_individual_deadlines.rs | 4 +- ...ing_to_minimize_maximum_cumulative_cost.rs | 4 +- ...ng_to_minimize_weighted_completion_time.rs | 24 +- ...quencing_to_minimize_weighted_tardiness.rs | 6 +- ...encing_with_release_times_and_deadlines.rs | 6 +- .../misc/sequencing_within_intervals.rs | 6 +- .../misc/shortest_common_supersequence.rs | 6 +- src/models/misc/stacker_crane.rs | 4 +- src/models/misc/staff_scheduling.rs | 4 +- .../misc/string_to_string_correction.rs | 6 +- src/models/misc/subset_sum.rs | 6 +- src/models/misc/sum_of_squares_partition.rs | 6 +- src/models/misc/timetable_design.rs | 4 +- src/models/set/comparative_containment.rs | 4 +- src/models/set/consecutive_sets.rs | 6 +- src/models/set/exact_cover_by_3_sets.rs | 6 +- src/models/set/maximum_set_packing.rs | 22 +- src/models/set/minimum_cardinality_key.rs | 4 +- src/models/set/minimum_hitting_set.rs | 22 +- src/models/set/minimum_set_covering.rs | 22 +- src/models/set/prime_attribute_name.rs | 6 +- .../set/rooted_tree_storage_assignment.rs | 4 +- src/models/set/set_basis.rs | 4 +- .../set/two_dimensional_consecutive_sets.rs | 4 +- src/rules/graph.rs | 29 +-- src/rules/mod.rs | 2 +- src/rules/test_helpers.rs | 146 ++++++------ src/rules/traits.rs | 6 +- src/solvers/brute_force.rs | 89 ++++---- src/solvers/ilp/solver.rs | 13 +- src/solvers/mod.rs | 12 +- src/topology/directed_graph.rs | 2 +- src/traits.rs | 6 +- src/types.rs | 209 +++++++++++------- src/unit_tests/example_db.rs | 2 +- src/unit_tests/export.rs | 8 +- src/unit_tests/graph_models.rs | 70 +++--- src/unit_tests/models/algebraic/bmf.rs | 48 ++-- .../algebraic/closest_vector_problem.rs | 22 +- .../consecutive_block_minimization.rs | 2 +- .../algebraic/consecutive_ones_submatrix.rs | 18 +- src/unit_tests/models/algebraic/ilp.rs | 75 ++++--- .../models/algebraic/quadratic_assignment.rs | 50 ++--- src/unit_tests/models/algebraic/qubo.rs | 26 +-- .../algebraic/sparse_matrix_compression.rs | 6 +- src/unit_tests/models/formula/circuit.rs | 6 +- src/unit_tests/models/formula/ksat.rs | 8 +- .../models/formula/nae_satisfiability.rs | 10 +- src/unit_tests/models/formula/qbf.rs | 8 +- src/unit_tests/models/formula/sat.rs | 16 +- .../models/graph/acyclic_partition.rs | 8 +- .../balanced_complete_bipartite_subgraph.rs | 10 +- src/unit_tests/models/graph/biclique_cover.rs | 47 ++-- .../graph/biconnectivity_augmentation.rs | 14 +- .../graph/bottleneck_traveling_salesman.rs | 25 +-- .../bounded_component_spanning_forest.rs | 8 +- .../directed_two_commodity_integral_flow.rs | 10 +- .../models/graph/disjoint_connecting_paths.rs | 6 +- .../models/graph/generalized_hex.rs | 8 +- .../models/graph/graph_partitioning.rs | 36 +-- .../models/graph/hamiltonian_circuit.rs | 12 +- .../models/graph/hamiltonian_path.rs | 14 +- .../models/graph/integral_flow_bundles.rs | 2 +- .../graph/integral_flow_homologous_arcs.rs | 10 +- .../graph/integral_flow_with_multipliers.rs | 8 +- .../models/graph/isomorphic_spanning_tree.rs | 12 +- src/unit_tests/models/graph/kclique.rs | 8 +- src/unit_tests/models/graph/kcoloring.rs | 16 +- .../models/graph/kth_best_spanning_tree.rs | 10 +- .../graph/length_bounded_disjoint_paths.rs | 8 +- .../models/graph/longest_circuit.rs | 8 +- src/unit_tests/models/graph/longest_path.rs | 61 ++--- src/unit_tests/models/graph/max_cut.rs | 12 +- src/unit_tests/models/graph/maximal_is.rs | 12 +- src/unit_tests/models/graph/maximum_clique.rs | 50 ++--- .../models/graph/maximum_independent_set.rs | 12 +- .../models/graph/maximum_matching.rs | 16 +- .../models/graph/min_max_multicenter.rs | 2 +- .../graph/minimum_cut_into_bounded_sets.rs | 16 +- .../models/graph/minimum_dominating_set.rs | 14 +- .../graph/minimum_dummy_activities_pert.rs | 26 +-- .../models/graph/minimum_feedback_arc_set.rs | 16 +- .../graph/minimum_feedback_vertex_set.rs | 14 +- .../models/graph/minimum_multiway_cut.rs | 30 +-- .../models/graph/minimum_sum_multicenter.rs | 16 +- .../models/graph/minimum_vertex_cover.rs | 14 +- .../models/graph/mixed_chinese_postman.rs | 8 +- .../models/graph/multiple_choice_branching.rs | 10 +- .../graph/multiple_copy_file_allocation.rs | 8 +- .../graph/optimal_linear_arrangement.rs | 16 +- .../graph/partition_into_paths_of_length_2.rs | 6 +- .../models/graph/partition_into_triangles.rs | 10 +- .../graph/path_constrained_network_flow.rs | 8 +- .../models/graph/rooted_tree_arrangement.rs | 4 +- src/unit_tests/models/graph/rural_postman.rs | 14 +- .../graph/shortest_weight_constrained_path.rs | 10 +- src/unit_tests/models/graph/spin_glass.rs | 10 +- src/unit_tests/models/graph/steiner_tree.rs | 26 +-- .../models/graph/steiner_tree_in_graphs.rs | 16 +- .../graph/strong_connectivity_augmentation.rs | 6 +- .../models/graph/subgraph_isomorphism.rs | 10 +- .../models/graph/traveling_salesman.rs | 40 ++-- .../graph/undirected_flow_lower_bounds.rs | 8 +- .../undirected_two_commodity_integral_flow.rs | 6 +- src/unit_tests/models/misc/additional_key.rs | 8 +- src/unit_tests/models/misc/bin_packing.rs | 16 +- .../misc/boyce_codd_normal_form_violation.rs | 6 +- .../models/misc/capacity_assignment.rs | 4 +- .../models/misc/conjunctive_boolean_query.rs | 8 +- .../misc/conjunctive_query_foldability.rs | 8 +- ...onsistency_of_database_frequency_tables.rs | 6 +- .../models/misc/ensemble_computation.rs | 6 +- .../models/misc/expected_retrieval_cost.rs | 6 +- src/unit_tests/models/misc/factoring.rs | 8 +- .../models/misc/flow_shop_scheduling.rs | 12 +- src/unit_tests/models/misc/knapsack.rs | 52 ++--- .../models/misc/longest_common_subsequence.rs | 10 +- .../misc/minimum_tardiness_sequencing.rs | 50 +++-- .../models/misc/multiprocessor_scheduling.rs | 12 +- src/unit_tests/models/misc/paintshop.rs | 16 +- .../models/misc/partially_ordered_knapsack.rs | 65 +++--- src/unit_tests/models/misc/partition.rs | 12 +- .../misc/precedence_constrained_scheduling.rs | 10 +- .../misc/rectilinear_picture_compression.rs | 12 +- .../misc/resource_constrained_scheduling.rs | 8 +- .../scheduling_with_individual_deadlines.rs | 15 +- ...ing_to_minimize_maximum_cumulative_cost.rs | 8 +- ...ng_to_minimize_weighted_completion_time.rs | 38 ++-- ...quencing_to_minimize_weighted_tardiness.rs | 12 +- ...encing_with_release_times_and_deadlines.rs | 10 +- .../misc/sequencing_within_intervals.rs | 14 +- .../misc/shortest_common_supersequence.rs | 16 +- src/unit_tests/models/misc/stacker_crane.rs | 8 +- .../models/misc/staff_scheduling.rs | 8 +- .../misc/string_to_string_correction.rs | 10 +- src/unit_tests/models/misc/subset_sum.rs | 8 +- .../models/misc/sum_of_squares_partition.rs | 10 +- .../models/misc/timetable_design.rs | 4 +- .../models/set/comparative_containment.rs | 6 +- src/unit_tests/models/set/consecutive_sets.rs | 6 +- .../models/set/exact_cover_by_3_sets.rs | 6 +- .../models/set/maximum_set_packing.rs | 18 +- .../models/set/minimum_cardinality_key.rs | 6 +- .../models/set/minimum_hitting_set.rs | 37 ++-- .../models/set/minimum_set_covering.rs | 14 +- .../models/set/prime_attribute_name.rs | 4 +- .../set/rooted_tree_storage_assignment.rs | 6 +- src/unit_tests/models/set/set_basis.rs | 6 +- .../set/two_dimensional_consecutive_sets.rs | 6 +- src/unit_tests/property.rs | 16 +- src/unit_tests/reduction_graph.rs | 18 +- src/unit_tests/registry/dispatch.rs | 23 +- src/unit_tests/rules/binpacking_ilp.rs | 16 +- src/unit_tests/rules/circuit_ilp.rs | 2 +- src/unit_tests/rules/circuit_spinglass.rs | 13 +- .../rules/closestvectorproblem_qubo.rs | 2 +- src/unit_tests/rules/coloring_ilp.rs | 2 +- src/unit_tests/rules/coloring_qubo.rs | 6 +- src/unit_tests/rules/factoring_circuit.rs | 2 +- src/unit_tests/rules/factoring_ilp.rs | 2 +- src/unit_tests/rules/graph.rs | 19 +- src/unit_tests/rules/graphpartitioning_ilp.rs | 12 +- .../hamiltoniancircuit_travelingsalesman.rs | 15 +- src/unit_tests/rules/ilp_bool_ilp_i32.rs | 4 +- src/unit_tests/rules/ilp_qubo.rs | 10 +- .../rules/integralflowbundles_ilp.rs | 4 +- src/unit_tests/rules/knapsack_qubo.rs | 6 +- src/unit_tests/rules/ksatisfiability_qubo.rs | 14 +- .../rules/ksatisfiability_subsetsum.rs | 10 +- .../rules/longestcommonsubsequence_ilp.rs | 4 +- src/unit_tests/rules/longestpath_ilp.rs | 12 +- .../maximumclique_maximumindependentset.rs | 10 +- .../rules/maximumindependentset_gridgraph.rs | 2 +- .../rules/maximumindependentset_ilp.rs | 6 +- .../maximumindependentset_maximumclique.rs | 6 +- ...maximumindependentset_maximumsetpacking.rs | 14 +- .../rules/maximumindependentset_qubo.rs | 14 +- src/unit_tests/rules/maximummatching_ilp.rs | 30 +-- .../maximummatching_maximumsetpacking.rs | 25 +-- .../rules/maximumsetpacking_casts.rs | 6 +- src/unit_tests/rules/maximumsetpacking_ilp.rs | 14 +- .../rules/maximumsetpacking_qubo.rs | 6 +- .../rules/minimumdominatingset_ilp.rs | 26 +-- .../rules/minimumfeedbackvertexset_ilp.rs | 22 +- .../rules/minimummultiwaycut_ilp.rs | 16 +- .../rules/minimummultiwaycut_qubo.rs | 12 +- .../rules/minimumsetcovering_ilp.rs | 16 +- .../rules/minimumvertexcover_ilp.rs | 6 +- ...inimumvertexcover_maximumindependentset.rs | 4 +- ...mumvertexcover_minimumfeedbackvertexset.rs | 6 +- .../minimumvertexcover_minimumsetcovering.rs | 10 +- .../rules/minimumvertexcover_qubo.rs | 14 +- src/unit_tests/rules/partition_knapsack.rs | 8 +- src/unit_tests/rules/qubo_ilp.rs | 4 +- src/unit_tests/rules/reduction_path_parity.rs | 6 +- src/unit_tests/rules/sat_circuitsat.rs | 2 +- src/unit_tests/rules/sat_coloring.rs | 6 +- src/unit_tests/rules/sat_ksat.rs | 25 +-- .../rules/sat_maximumindependentset.rs | 4 +- .../rules/sat_minimumdominatingset.rs | 2 +- ...ingtominimizeweightedcompletiontime_ilp.rs | 12 +- src/unit_tests/rules/spinglass_maxcut.rs | 8 +- src/unit_tests/rules/spinglass_qubo.rs | 10 +- src/unit_tests/rules/steinertree_ilp.rs | 10 +- .../rules/subsetsum_closestvectorproblem.rs | 17 +- src/unit_tests/rules/travelingsalesman_ilp.rs | 10 +- .../rules/travelingsalesman_qubo.rs | 10 +- src/unit_tests/solvers/brute_force.rs | 18 +- src/unit_tests/solvers/ilp/solver.rs | 4 +- src/unit_tests/trait_consistency.rs | 38 ++-- src/unit_tests/types.rs | 107 ++++----- tests/suites/integration.rs | 62 +++--- tests/suites/reductions.rs | 62 +++--- 307 files changed, 2250 insertions(+), 2267 deletions(-) diff --git a/problemreductions-cli/src/commands/graph.rs b/problemreductions-cli/src/commands/graph.rs index d409efaa4..e27caaaa1 100644 --- a/problemreductions-cli/src/commands/graph.rs +++ b/problemreductions-cli/src/commands/graph.rs @@ -2,7 +2,7 @@ use crate::output::OutputConfig; use crate::problem_name::{aliases_for, parse_problem_spec, resolve_problem_ref}; use anyhow::{Context, Result}; use problemreductions::registry::collect_schemas; -use problemreductions::rules::{Minimize, MinimizeSteps, ReductionGraph, TraversalDirection}; +use problemreductions::rules::{Minimize, MinimizeSteps, ReductionGraph, TraversalFlow}; use problemreductions::types::ProblemSize; use problemreductions::{big_o_normal_form, Expr}; use std::collections::BTreeMap; @@ -693,11 +693,11 @@ pub fn export(out: &OutputConfig) -> Result<()> { out.emit_with_default_name("reduction_graph.json", &text, &json) } -fn parse_direction(s: &str) -> Result { +fn parse_direction(s: &str) -> Result { match s { - "out" => Ok(TraversalDirection::Outgoing), - "in" => Ok(TraversalDirection::Incoming), - "both" => Ok(TraversalDirection::Both), + "out" => Ok(TraversalFlow::Outgoing), + "in" => Ok(TraversalFlow::Incoming), + "both" => Ok(TraversalFlow::Both), _ => anyhow::bail!("Unknown direction: {}. Use 'out', 'in', or 'both'.", s), } } @@ -718,9 +718,9 @@ pub fn neighbors( let neighbors = graph.k_neighbors(&spec_name, &variant, max_hops, direction); let dir_label = match direction { - TraversalDirection::Outgoing => "outgoing", - TraversalDirection::Incoming => "incoming", - TraversalDirection::Both => "both directions", + TraversalFlow::Outgoing => "outgoing", + TraversalFlow::Incoming => "incoming", + TraversalFlow::Both => "both directions", }; // Build tree structure via BFS with parent tracking diff --git a/problemreductions-cli/src/commands/solve.rs b/problemreductions-cli/src/commands/solve.rs index 050badaf2..66cff99b5 100644 --- a/problemreductions-cli/src/commands/solve.rs +++ b/problemreductions-cli/src/commands/solve.rs @@ -257,7 +257,8 @@ mod tests { config: None, evaluation: "Sum(56)".to_string(), }; - let (text, json) = plain_problem_output("CliTestAggregateValueSource", "brute-force", &result); + let (text, json) = + plain_problem_output("CliTestAggregateValueSource", "brute-force", &result); assert!(text.contains("Evaluation: Sum(56)"), "{text}"); assert!(!text.contains("Solution:"), "{text}"); assert!(json.get("solution").is_none(), "{json}"); diff --git a/problemreductions-cli/src/mcp/tools.rs b/problemreductions-cli/src/mcp/tools.rs index 045e10292..8454efbbe 100644 --- a/problemreductions-cli/src/mcp/tools.rs +++ b/problemreductions-cli/src/mcp/tools.rs @@ -8,7 +8,7 @@ use problemreductions::models::graph::{ use problemreductions::models::misc::Factoring; use problemreductions::registry::collect_schemas; use problemreductions::rules::{ - CustomCost, MinimizeSteps, ReductionGraph, ReductionMode, TraversalDirection, + CustomCost, MinimizeSteps, ReductionGraph, ReductionMode, TraversalFlow, }; use problemreductions::topology::{ Graph, KingsSubgraph, SimpleGraph, TriangularSubgraph, UnitDiskGraph, @@ -41,7 +41,7 @@ pub struct NeighborsParams { pub problem: String, #[schemars(description = "Number of hops to explore (default: 1)")] pub hops: Option, - #[schemars(description = "Direction: out (default), in, or both")] + #[schemars(description = "ExtremumSense: out (default), in, or both")] pub direction: Option, } @@ -1105,11 +1105,11 @@ impl rmcp::ServerHandler for McpServer { // helpers // --------------------------------------------------------------------------- -fn parse_direction(s: &str) -> anyhow::Result { +fn parse_direction(s: &str) -> anyhow::Result { match s { - "out" => Ok(TraversalDirection::Outgoing), - "in" => Ok(TraversalDirection::Incoming), - "both" => Ok(TraversalDirection::Both), + "out" => Ok(TraversalFlow::Outgoing), + "in" => Ok(TraversalFlow::Incoming), + "both" => Ok(TraversalFlow::Both), _ => anyhow::bail!("Unknown direction: {}. Use 'out', 'in', or 'both'.", s), } } diff --git a/problemreductions-cli/src/test_support.rs b/problemreductions-cli/src/test_support.rs index 17d847a19..0528ee124 100644 --- a/problemreductions-cli/src/test_support.rs +++ b/problemreductions-cli/src/test_support.rs @@ -1,11 +1,11 @@ use crate::dispatch::{PathStep, ProblemJsonOutput, ReductionBundle}; -use problemreductions::models::algebraic::{ILP, ObjectiveSense}; +use problemreductions::models::algebraic::{ObjectiveSense, ILP}; use problemreductions::registry::VariantEntry; use problemreductions::rules::registry::{EdgeCapabilities, ReductionEntry, ReductionOverhead}; use problemreductions::rules::{AggregateReductionResult, ReductionAutoCast}; use problemreductions::solvers::{BruteForce, Solver}; use problemreductions::traits::Problem; -use problemreductions::types::{ProblemSize, SolutionSize, Sum}; +use problemreductions::types::{Extremum, ProblemSize, Sum}; use serde::{Deserialize, Serialize}; use std::any::Any; use std::collections::BTreeMap; @@ -90,7 +90,7 @@ impl AggregateReductionResult for AggregateValueToIlpReduction { &self.target } - fn extract_value(&self, _target_value: SolutionSize) -> Sum { + fn extract_value(&self, _target_value: Extremum) -> Sum { Sum(0) } } diff --git a/problemreductions-macros/src/lib.rs b/problemreductions-macros/src/lib.rs index acb94a4d6..e3e9351ab 100644 --- a/problemreductions-macros/src/lib.rs +++ b/problemreductions-macros/src/lib.rs @@ -380,9 +380,9 @@ fn extract_target_from_trait(path: &Path) -> syn::Result { /// Solver kind for dispatch generation. #[derive(Debug, Clone, Copy)] enum SolverKind { - /// Optimization problem — uses `find_best`. + /// Optimization problem marker. Opt, - /// Satisfaction problem — uses `find_satisfying`. + /// Satisfaction problem marker. Sat, } @@ -394,7 +394,6 @@ struct DeclareVariantsInput { /// A single entry: `[default] opt|sat Type => "complexity_string"`. struct DeclareVariantEntry { is_default: bool, - solver_kind: SolverKind, ty: Type, complexity: syn::LitStr, } @@ -410,7 +409,7 @@ impl syn::parse::Parse for DeclareVariantsInput { } // Require `opt` or `sat` keyword - let solver_kind = if input.peek(syn::Ident) { + let _solver_kind = if input.peek(syn::Ident) { let fork = input.fork(); if let Ok(ident) = fork.parse::() { match ident.to_string().as_str() { @@ -441,7 +440,6 @@ impl syn::parse::Parse for DeclareVariantsInput { let complexity: syn::LitStr = input.parse()?; entries.push(DeclareVariantEntry { is_default, - solver_kind, ty, complexity, }); @@ -564,40 +562,14 @@ fn generate_declare_variants(input: &DeclareVariantsInput) -> syn::Result quote! { - if let Some(config) = - ::find_best(&solver, p) - { - format!("{:?}", crate::traits::Problem::evaluate(p, &config)) - } else { - let invalid: <#ty as crate::traits::Problem>::Value = - crate::types::SolutionSize::Invalid; - format!("{:?}", invalid) - } - }, - SolverKind::Sat => quote! { - if let Some(config) = - ::find_satisfying( - &solver, p, - ) - { - format!("{:?}", crate::traits::Problem::evaluate(p, &config)) - } else { - format!("{:?}", false) - } - }, + // Generate dispatch fields based on aggregate value solving plus optional witnesses. + let solve_value_body = quote! { + let total = ::solve(&solver, p); + format!("{:?}", total) }; - let solve_witness_body = match entry.solver_kind { - SolverKind::Opt => quote! { - let config = - ::find_best(&solver, p)?; - }, - SolverKind::Sat => quote! { - let config = ::find_satisfying(&solver, p)?; - }, + let solve_witness_body = quote! { + let config = crate::solvers::BruteForce::find_witness(&solver, p)?; }; let dispatch_fields = quote! { @@ -751,7 +723,7 @@ mod tests { } #[test] - fn declare_variants_generates_find_best_for_opt_entries() { + fn declare_variants_generates_aggregate_value_and_witness_dispatch_for_opt_entries() { let input: DeclareVariantsInput = syn::parse_quote! { default opt Foo => "1", }; @@ -785,11 +757,26 @@ mod tests { !tokens.contains("solve_witness_fn : None"), "solve_witness_fn should not be None" ); - assert!(tokens.contains("find_best"), "expected find_best in tokens"); + assert!( + tokens.contains("let total ="), + "expected aggregate value solve" + ); + assert!( + tokens.contains("find_witness"), + "expected find_witness in tokens" + ); + assert!( + !tokens.contains("find_best"), + "did not expect legacy find_best in tokens" + ); + assert!( + !tokens.contains("SolutionSize :: Invalid"), + "did not expect legacy invalid fallback in tokens" + ); } #[test] - fn declare_variants_generates_find_satisfying_for_sat_entries() { + fn declare_variants_generates_aggregate_value_and_witness_dispatch_for_sat_entries() { let input: DeclareVariantsInput = syn::parse_quote! { default sat Foo => "1", }; @@ -824,8 +811,16 @@ mod tests { "solve_witness_fn should not be None" ); assert!( - tokens.contains("find_satisfying"), - "expected find_satisfying in tokens" + tokens.contains("let total ="), + "expected aggregate value solve" + ); + assert!( + tokens.contains("find_witness"), + "expected find_witness in tokens" + ); + assert!( + !tokens.contains("find_satisfying"), + "did not expect legacy find_satisfying in tokens" ); } diff --git a/src/lib.rs b/src/lib.rs index 217ff0a67..49a8629b9 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -11,8 +11,8 @@ //! | [`rules`] | Reduction rules, [`ReductionGraph`](rules::ReductionGraph) for path search | //! | [`solvers`] | [`BruteForce`] and [`ILPSolver`](solvers::ILPSolver) | //! | [`topology`] | Graph types — [`SimpleGraph`](topology::SimpleGraph), [`UnitDiskGraph`](topology::UnitDiskGraph), etc. | -//! | [`traits`] | Core traits — [`Problem`], [`OptimizationProblem`], [`SatisfactionProblem`] | -//! | [`types`] | [`SolutionSize`], [`Direction`], [`ProblemSize`], [`WeightElement`] | +//! | [`traits`] | Core traits — [`Problem`], [`ObjectiveProblem`], [`WitnessProblem`] | +//! | [`types`] | [`Max`], [`Min`], [`Extremum`], [`ExtremumSense`], [`ProblemSize`], [`WeightElement`] | //! | [`variant`] | Variant parameter system for problem type parameterization | //! //! Use [`prelude`] for convenient imports. @@ -87,11 +87,13 @@ pub mod prelude { // Core traits pub use crate::rules::{ReduceTo, ReductionResult}; pub use crate::solvers::{BruteForce, Solver}; - pub use crate::traits::{OptimizationProblem, Problem, SatisfactionProblem}; + pub use crate::traits::{ObjectiveProblem, Problem, WitnessProblem}; // Types pub use crate::error::{ProblemError, Result}; - pub use crate::types::{Direction, One, ProblemSize, SolutionSize, Unweighted}; + pub use crate::types::{ + And, Extremum, ExtremumSense, Max, Min, One, Or, ProblemSize, Sum, Unweighted, + }; } // Re-export commonly used items at crate root @@ -101,9 +103,10 @@ pub use error::{ProblemError, Result}; pub use expr::{asymptotic_normal_form, AsymptoticAnalysisError, CanonicalizationError, Expr}; pub use registry::{ComplexityClass, ProblemInfo}; pub use solvers::{BruteForce, Solver}; -pub use traits::{OptimizationProblem, Problem, SatisfactionProblem}; +pub use traits::{ObjectiveProblem, Problem, WitnessProblem}; pub use types::{ - Direction, NumericSize, One, ProblemSize, SolutionSize, Unweighted, WeightElement, + And, Extremum, ExtremumSense, Max, Min, NumericSize, One, Or, ProblemSize, Sum, Unweighted, + WeightElement, }; // Re-export proc macros for reduction registration and variant declaration diff --git a/src/models/algebraic/bmf.rs b/src/models/algebraic/bmf.rs index 8f3516307..8f0c424ef 100644 --- a/src/models/algebraic/bmf.rs +++ b/src/models/algebraic/bmf.rs @@ -5,8 +5,8 @@ //! The boolean product `(B * C)[i,j] = OR_k (B[i,k] AND C[k,j])`. use crate::registry::{FieldInfo, ProblemSchemaEntry}; -use crate::traits::{OptimizationProblem, Problem}; -use crate::types::{Direction, SolutionSize}; +use crate::traits::{ObjectiveProblem, Problem}; +use crate::types::{ExtremumSense, Min}; use serde::{Deserialize, Serialize}; inventory::submit! { @@ -46,7 +46,7 @@ inventory::submit! { /// let problem = BMF::new(a, 1); /// /// let solver = BruteForce::new(); -/// let solutions = solver.find_all_best(&problem); +/// let solutions = solver.find_all_witnesses(&problem); /// /// // Check the error /// for sol in &solutions { @@ -205,17 +205,17 @@ pub(crate) fn matrix_hamming_distance(a: &[Vec], b: &[Vec]) -> usize impl Problem for BMF { const NAME: &'static str = "BMF"; - type Value = SolutionSize; + type Value = Min; fn dims(&self) -> Vec { // B: m*k + C: k*n binary variables vec![2; self.m * self.k + self.k * self.n] } - fn evaluate(&self, config: &[usize]) -> SolutionSize { + fn evaluate(&self, config: &[usize]) -> Min { // Minimize Hamming distance between A and B*C. // All configurations are valid -- the distance is the objective. - SolutionSize::Valid(self.hamming_distance(config) as i32) + Min(Some(self.hamming_distance(config) as i32)) } fn variant() -> Vec<(&'static str, &'static str)> { @@ -223,11 +223,11 @@ impl Problem for BMF { } } -impl OptimizationProblem for BMF { +impl ObjectiveProblem for BMF { type Objective = i32; - fn direction(&self) -> Direction { - Direction::Minimize + fn direction(&self) -> ExtremumSense { + ExtremumSense::Minimize } } @@ -248,7 +248,7 @@ pub(crate) fn canonical_model_example_specs() -> Vec; + type Value = Min; fn dims(&self) -> Vec { self.bounds @@ -253,7 +253,7 @@ where .collect() } - fn evaluate(&self, config: &[usize]) -> SolutionSize { + fn evaluate(&self, config: &[usize]) -> Min { let values = self.config_to_values(config); let m = self.ambient_dimension(); let mut diff = vec![0.0f64; m]; @@ -266,7 +266,7 @@ where *d -= t; } let norm = diff.iter().map(|d| d * d).sum::().sqrt(); - SolutionSize::Valid(norm) + Min(Some(norm)) } fn variant() -> Vec<(&'static str, &'static str)> { @@ -274,7 +274,7 @@ where } } -impl OptimizationProblem for ClosestVectorProblem +impl ObjectiveProblem for ClosestVectorProblem where T: Clone + Into @@ -286,8 +286,8 @@ where { type Objective = f64; - fn direction(&self) -> Direction { - Direction::Minimize + fn direction(&self) -> ExtremumSense { + ExtremumSense::Minimize } } @@ -306,7 +306,7 @@ pub(crate) fn canonical_model_example_specs() -> Vec "factorial(num_cols) * num_rows * num_cols", diff --git a/src/models/algebraic/consecutive_ones_submatrix.rs b/src/models/algebraic/consecutive_ones_submatrix.rs index 615a17fc3..0b012dc8e 100644 --- a/src/models/algebraic/consecutive_ones_submatrix.rs +++ b/src/models/algebraic/consecutive_ones_submatrix.rs @@ -7,7 +7,7 @@ //! transformation from Hamiltonian Path. use crate::registry::{FieldInfo, ProblemSchemaEntry}; -use crate::traits::{Problem, SatisfactionProblem}; +use crate::traits::{Problem, WitnessProblem}; use serde::{Deserialize, Serialize}; inventory::submit! { @@ -54,7 +54,7 @@ inventory::submit! { /// ]; /// let problem = ConsecutiveOnesSubmatrix::new(matrix, 3); /// let solver = BruteForce::new(); -/// let solution = solver.find_satisfying(&problem); +/// let solution = solver.find_witness(&problem); /// assert!(solution.is_some()); /// ``` #[derive(Debug, Clone, Serialize, Deserialize)] @@ -204,7 +204,7 @@ impl Problem for ConsecutiveOnesSubmatrix { } } -impl SatisfactionProblem for ConsecutiveOnesSubmatrix {} +impl WitnessProblem for ConsecutiveOnesSubmatrix {} crate::declare_variants! { default sat ConsecutiveOnesSubmatrix => "2^(num_cols) * (num_rows + num_cols)", diff --git a/src/models/algebraic/ilp.rs b/src/models/algebraic/ilp.rs index 876f44bb7..285ee3946 100644 --- a/src/models/algebraic/ilp.rs +++ b/src/models/algebraic/ilp.rs @@ -8,8 +8,8 @@ //! - `ILP`: non-negative integer variables (0..2^31-1) use crate::registry::{FieldInfo, ProblemSchemaEntry, VariantDimension}; -use crate::traits::{OptimizationProblem, Problem}; -use crate::types::{Direction, SolutionSize}; +use crate::traits::{ObjectiveProblem, Problem}; +use crate::types::{Extremum, ExtremumSense}; use serde::{Deserialize, Serialize}; use std::marker::PhantomData; @@ -244,18 +244,25 @@ impl ILP { impl Problem for ILP { const NAME: &'static str = "ILP"; - type Value = SolutionSize; + type Value = Extremum; fn dims(&self) -> Vec { vec![V::DIMS_PER_VAR; self.num_vars] } - fn evaluate(&self, config: &[usize]) -> SolutionSize { + fn evaluate(&self, config: &[usize]) -> Extremum { let values = self.config_to_values(config); if !self.is_feasible(&values) { - return SolutionSize::Invalid; + return match self.sense { + ObjectiveSense::Maximize => Extremum::maximize(None), + ObjectiveSense::Minimize => Extremum::minimize(None), + }; + } + let objective = self.evaluate_objective(&values); + match self.sense { + ObjectiveSense::Maximize => Extremum::maximize(Some(objective)), + ObjectiveSense::Minimize => Extremum::minimize(Some(objective)), } - SolutionSize::Valid(self.evaluate_objective(&values)) } fn variant() -> Vec<(&'static str, &'static str)> { @@ -263,13 +270,13 @@ impl Problem for ILP { } } -impl OptimizationProblem for ILP { +impl ObjectiveProblem for ILP { type Objective = f64; - fn direction(&self) -> Direction { + fn direction(&self) -> ExtremumSense { match self.sense { - ObjectiveSense::Maximize => Direction::Maximize, - ObjectiveSense::Minimize => Direction::Minimize, + ObjectiveSense::Maximize => ExtremumSense::Maximize, + ObjectiveSense::Minimize => ExtremumSense::Minimize, } } } @@ -293,7 +300,10 @@ pub(crate) fn canonical_model_example_specs() -> Vec; + type Value = Min; fn dims(&self) -> Vec { vec![self.num_locations(); self.num_facilities()] } - fn evaluate(&self, config: &[usize]) -> SolutionSize { + fn evaluate(&self, config: &[usize]) -> Min { let n = self.num_facilities(); let m = self.num_locations(); // Check config length matches number of facilities if config.len() != n { - return SolutionSize::Invalid; + return Min(None); } // Check that all assignments are valid locations for &loc in config { if loc >= m { - return SolutionSize::Invalid; + return Min(None); } } @@ -141,7 +141,7 @@ impl Problem for QuadraticAssignment { let mut used = vec![false; m]; for &loc in config { if used[loc] { - return SolutionSize::Invalid; + return Min(None); } used[loc] = true; } @@ -156,7 +156,7 @@ impl Problem for QuadraticAssignment { } } - SolutionSize::Valid(total) + Min(Some(total)) } fn variant() -> Vec<(&'static str, &'static str)> { @@ -164,11 +164,11 @@ impl Problem for QuadraticAssignment { } } -impl OptimizationProblem for QuadraticAssignment { +impl ObjectiveProblem for QuadraticAssignment { type Objective = i64; - fn direction(&self) -> Direction { - Direction::Minimize + fn direction(&self) -> ExtremumSense { + ExtremumSense::Minimize } } @@ -195,7 +195,7 @@ pub(crate) fn canonical_model_example_specs() -> Vec, { const NAME: &'static str = "QUBO"; - type Value = SolutionSize; + type Value = Min; fn dims(&self) -> Vec { vec![2; self.num_vars] } - fn evaluate(&self, config: &[usize]) -> SolutionSize { - SolutionSize::Valid(self.evaluate(config).to_sum()) + fn evaluate(&self, config: &[usize]) -> Min { + Min(Some(self.evaluate(config).to_sum())) } fn variant() -> Vec<(&'static str, &'static str)> { @@ -173,7 +173,7 @@ where } } -impl OptimizationProblem for QUBO +impl ObjectiveProblem for QUBO where W: WeightElement + crate::variant::VariantParam @@ -186,8 +186,8 @@ where { type Objective = W::Sum; - fn direction(&self) -> Direction { - Direction::Minimize + fn direction(&self) -> ExtremumSense { + ExtremumSense::Minimize } } @@ -205,7 +205,7 @@ pub(crate) fn canonical_model_example_specs() -> Vec "(bound_k ^ num_rows) * num_rows * num_cols", diff --git a/src/models/formula/circuit.rs b/src/models/formula/circuit.rs index a47e3ba68..6c764f82a 100644 --- a/src/models/formula/circuit.rs +++ b/src/models/formula/circuit.rs @@ -4,7 +4,7 @@ //! The goal is to find variable assignments that satisfy the circuit constraints. use crate::registry::{FieldInfo, ProblemSchemaEntry}; -use crate::traits::{Problem, SatisfactionProblem}; +use crate::traits::{Problem, WitnessProblem}; use serde::{Deserialize, Serialize}; use std::collections::HashMap; @@ -210,7 +210,7 @@ impl Circuit { /// /// let problem = CircuitSAT::new(circuit); /// let solver = BruteForce::new(); -/// let solutions = solver.find_all_satisfying(&problem); +/// let solutions = solver.find_all_witnesses(&problem); /// /// // Multiple satisfying assignments exist /// assert!(!solutions.is_empty()); @@ -304,7 +304,7 @@ impl Problem for CircuitSAT { } } -impl SatisfactionProblem for CircuitSAT {} +impl WitnessProblem for CircuitSAT {} crate::declare_variants! { default sat CircuitSAT => "2^num_variables", diff --git a/src/models/formula/ksat.rs b/src/models/formula/ksat.rs index c90393b3f..043a00055 100644 --- a/src/models/formula/ksat.rs +++ b/src/models/formula/ksat.rs @@ -6,7 +6,7 @@ //! MaxKSatisfiability type (if available). use crate::registry::{FieldInfo, ProblemSchemaEntry, VariantDimension}; -use crate::traits::{Problem, SatisfactionProblem}; +use crate::traits::{Problem, WitnessProblem}; use crate::variant::{KValue, K2, K3, KN}; use serde::{Deserialize, Serialize}; @@ -54,7 +54,7 @@ inventory::submit! { /// ); /// /// let solver = BruteForce::new(); -/// let solutions = solver.find_all_satisfying(&problem); +/// let solutions = solver.find_all_witnesses(&problem); /// assert!(!solutions.is_empty()); /// ``` #[derive(Debug, Clone, Serialize, Deserialize)] @@ -184,7 +184,7 @@ impl Problem for KSatisfiability { } } -impl SatisfactionProblem for KSatisfiability {} +impl WitnessProblem for KSatisfiability {} crate::declare_variants! { default sat KSatisfiability => "2^num_variables", diff --git a/src/models/formula/nae_satisfiability.rs b/src/models/formula/nae_satisfiability.rs index a8a0d757c..95721c243 100644 --- a/src/models/formula/nae_satisfiability.rs +++ b/src/models/formula/nae_satisfiability.rs @@ -4,7 +4,7 @@ //! contains at least one true literal and at least one false literal. use crate::registry::{FieldInfo, ProblemSchemaEntry}; -use crate::traits::{Problem, SatisfactionProblem}; +use crate::traits::{Problem, WitnessProblem}; use serde::{Deserialize, Serialize}; use super::CNFClause; @@ -151,7 +151,7 @@ impl Problem for NAESatisfiability { } } -impl SatisfactionProblem for NAESatisfiability {} +impl WitnessProblem for NAESatisfiability {} crate::declare_variants! { default sat NAESatisfiability => "2^num_variables", diff --git a/src/models/formula/qbf.rs b/src/models/formula/qbf.rs index 8e35180fa..69f30a48d 100644 --- a/src/models/formula/qbf.rs +++ b/src/models/formula/qbf.rs @@ -10,7 +10,7 @@ use crate::models::formula::CNFClause; use crate::registry::{FieldInfo, ProblemSchemaEntry}; -use crate::traits::{Problem, SatisfactionProblem}; +use crate::traits::{Problem, WitnessProblem}; use serde::{Deserialize, Serialize}; inventory::submit! { @@ -175,7 +175,7 @@ impl Problem for QuantifiedBooleanFormulas { } } -impl SatisfactionProblem for QuantifiedBooleanFormulas {} +impl WitnessProblem for QuantifiedBooleanFormulas {} crate::declare_variants! { default sat QuantifiedBooleanFormulas => "2^num_vars", diff --git a/src/models/formula/sat.rs b/src/models/formula/sat.rs index c85319738..a021c65ff 100644 --- a/src/models/formula/sat.rs +++ b/src/models/formula/sat.rs @@ -6,7 +6,7 @@ //! the separate MaxSatisfiability type (if available). use crate::registry::{FieldInfo, ProblemSchemaEntry}; -use crate::traits::{Problem, SatisfactionProblem}; +use crate::traits::{Problem, WitnessProblem}; use serde::{Deserialize, Serialize}; inventory::submit! { @@ -106,7 +106,7 @@ impl CNFClause { /// ); /// /// let solver = BruteForce::new(); -/// let solutions = solver.find_all_satisfying(&problem); +/// let solutions = solver.find_all_witnesses(&problem); /// /// // Verify solutions satisfy all clauses /// for sol in solutions { @@ -196,7 +196,7 @@ impl Problem for Satisfiability { } } -impl SatisfactionProblem for Satisfiability {} +impl WitnessProblem for Satisfiability {} crate::declare_variants! { default sat Satisfiability => "2^num_variables", diff --git a/src/models/graph/acyclic_partition.rs b/src/models/graph/acyclic_partition.rs index d20fa16ee..5620d61ac 100644 --- a/src/models/graph/acyclic_partition.rs +++ b/src/models/graph/acyclic_partition.rs @@ -7,7 +7,7 @@ use crate::registry::{FieldInfo, ProblemSchemaEntry, ProblemSizeFieldEntry, VariantDimension}; use crate::topology::DirectedGraph; -use crate::traits::{Problem, SatisfactionProblem}; +use crate::traits::{Problem, WitnessProblem}; use crate::types::WeightElement; use num_traits::Zero; use serde::{Deserialize, Serialize}; @@ -178,10 +178,7 @@ where } } -impl SatisfactionProblem for AcyclicPartition where - W: WeightElement + crate::variant::VariantParam -{ -} +impl WitnessProblem for AcyclicPartition where W: WeightElement + crate::variant::VariantParam {} fn is_valid_acyclic_partition( graph: &DirectedGraph, diff --git a/src/models/graph/balanced_complete_bipartite_subgraph.rs b/src/models/graph/balanced_complete_bipartite_subgraph.rs index 3d39f188b..578596b79 100644 --- a/src/models/graph/balanced_complete_bipartite_subgraph.rs +++ b/src/models/graph/balanced_complete_bipartite_subgraph.rs @@ -1,6 +1,6 @@ use crate::registry::{FieldInfo, ProblemSchemaEntry}; use crate::topology::BipartiteGraph; -use crate::traits::{Problem, SatisfactionProblem}; +use crate::traits::{Problem, WitnessProblem}; use serde::{Deserialize, Serialize}; use std::collections::HashSet; @@ -129,7 +129,7 @@ impl Problem for BalancedCompleteBipartiteSubgraph { } } -impl SatisfactionProblem for BalancedCompleteBipartiteSubgraph {} +impl WitnessProblem for BalancedCompleteBipartiteSubgraph {} #[derive(Deserialize)] struct BalancedCompleteBipartiteSubgraphRepr { diff --git a/src/models/graph/biclique_cover.rs b/src/models/graph/biclique_cover.rs index 42c9a465c..8779ecf1e 100644 --- a/src/models/graph/biclique_cover.rs +++ b/src/models/graph/biclique_cover.rs @@ -5,8 +5,8 @@ use crate::registry::{FieldInfo, ProblemSchemaEntry}; use crate::topology::BipartiteGraph; -use crate::traits::{OptimizationProblem, Problem}; -use crate::types::{Direction, SolutionSize}; +use crate::traits::{ObjectiveProblem, Problem}; +use crate::types::{ExtremumSense, Min}; use serde::{Deserialize, Serialize}; use std::collections::HashSet; @@ -45,7 +45,7 @@ inventory::submit! { /// let problem = BicliqueCover::new(graph, 2); /// /// let solver = BruteForce::new(); -/// let solutions = solver.find_all_best(&problem); +/// let solutions = solver.find_all_witnesses(&problem); /// /// // Check coverage /// for sol in &solutions { @@ -219,18 +219,18 @@ pub(crate) fn is_biclique_cover( impl Problem for BicliqueCover { const NAME: &'static str = "BicliqueCover"; - type Value = SolutionSize; + type Value = Min; fn dims(&self) -> Vec { // Each vertex has k binary variables (one per biclique) vec![2; self.num_vertices() * self.k] } - fn evaluate(&self, config: &[usize]) -> SolutionSize { + fn evaluate(&self, config: &[usize]) -> Min { if !self.is_valid_cover(config) { - return SolutionSize::Invalid; + return Min(None); } - SolutionSize::Valid(self.total_biclique_size(config) as i32) + Min(Some(self.total_biclique_size(config) as i32)) } fn variant() -> Vec<(&'static str, &'static str)> { @@ -238,11 +238,11 @@ impl Problem for BicliqueCover { } } -impl OptimizationProblem for BicliqueCover { +impl ObjectiveProblem for BicliqueCover { type Objective = i32; - fn direction(&self) -> Direction { - Direction::Minimize + fn direction(&self) -> ExtremumSense { + ExtremumSense::Minimize } } @@ -260,7 +260,7 @@ pub(crate) fn canonical_model_example_specs() -> Vec SatisfactionProblem for BiconnectivityAugmentation +impl WitnessProblem for BiconnectivityAugmentation where G: Graph + crate::variant::VariantParam, W: WeightElement + crate::variant::VariantParam, diff --git a/src/models/graph/bottleneck_traveling_salesman.rs b/src/models/graph/bottleneck_traveling_salesman.rs index 203642450..03e5e18df 100644 --- a/src/models/graph/bottleneck_traveling_salesman.rs +++ b/src/models/graph/bottleneck_traveling_salesman.rs @@ -5,8 +5,8 @@ use crate::registry::{FieldInfo, ProblemSchemaEntry}; use crate::topology::{Graph, SimpleGraph}; -use crate::traits::{OptimizationProblem, Problem}; -use crate::types::{Direction, SolutionSize}; +use crate::traits::{ObjectiveProblem, Problem}; +use crate::types::{ExtremumSense, Min}; use serde::{Deserialize, Serialize}; inventory::submit! { @@ -98,7 +98,7 @@ impl BottleneckTravelingSalesman { impl Problem for BottleneckTravelingSalesman { const NAME: &'static str = "BottleneckTravelingSalesman"; - type Value = SolutionSize; + type Value = Min; fn variant() -> Vec<(&'static str, &'static str)> { crate::variant_params![] @@ -108,14 +108,14 @@ impl Problem for BottleneckTravelingSalesman { vec![2; self.graph.num_edges()] } - fn evaluate(&self, config: &[usize]) -> SolutionSize { + fn evaluate(&self, config: &[usize]) -> Min { if config.len() != self.graph.num_edges() { - return SolutionSize::Invalid; + return Min(None); } let selected: Vec = config.iter().map(|&s| s == 1).collect(); if !super::traveling_salesman::is_hamiltonian_cycle(&self.graph, &selected) { - return SolutionSize::Invalid; + return Min(None); } let bottleneck = config @@ -125,15 +125,15 @@ impl Problem for BottleneckTravelingSalesman { .max() .expect("valid Hamiltonian cycle selects at least one edge"); - SolutionSize::Valid(bottleneck) + Min(Some(bottleneck)) } } -impl OptimizationProblem for BottleneckTravelingSalesman { +impl ObjectiveProblem for BottleneckTravelingSalesman { type Objective = i32; - fn direction(&self) -> Direction { - Direction::Minimize + fn direction(&self) -> ExtremumSense { + ExtremumSense::Minimize } } @@ -160,7 +160,7 @@ pub(crate) fn canonical_model_example_specs() -> Vec SatisfactionProblem for BoundedComponentSpanningForest +impl WitnessProblem for BoundedComponentSpanningForest where G: Graph + crate::variant::VariantParam, W: WeightElement + crate::variant::VariantParam, diff --git a/src/models/graph/directed_two_commodity_integral_flow.rs b/src/models/graph/directed_two_commodity_integral_flow.rs index 8d9bfc293..0dd3f0ed2 100644 --- a/src/models/graph/directed_two_commodity_integral_flow.rs +++ b/src/models/graph/directed_two_commodity_integral_flow.rs @@ -8,7 +8,7 @@ use crate::registry::{FieldInfo, ProblemSchemaEntry}; use crate::topology::DirectedGraph; -use crate::traits::{Problem, SatisfactionProblem}; +use crate::traits::{Problem, WitnessProblem}; use serde::{Deserialize, Serialize}; inventory::submit! { @@ -64,7 +64,7 @@ inventory::submit! { /// graph, vec![1; 8], 0, 4, 1, 5, 1, 1, /// ); /// let solver = BruteForce::new(); -/// assert!(solver.find_satisfying(&problem).is_some()); +/// assert!(solver.find_witness(&problem).is_some()); /// ``` #[derive(Debug, Clone, Serialize, Deserialize)] pub struct DirectedTwoCommodityIntegralFlow { @@ -263,7 +263,7 @@ impl Problem for DirectedTwoCommodityIntegralFlow { } } -impl SatisfactionProblem for DirectedTwoCommodityIntegralFlow {} +impl WitnessProblem for DirectedTwoCommodityIntegralFlow {} crate::declare_variants! { default sat DirectedTwoCommodityIntegralFlow => "(max_capacity + 1)^(2 * num_arcs)", diff --git a/src/models/graph/disjoint_connecting_paths.rs b/src/models/graph/disjoint_connecting_paths.rs index 1ff212f2c..e1dd76e84 100644 --- a/src/models/graph/disjoint_connecting_paths.rs +++ b/src/models/graph/disjoint_connecting_paths.rs @@ -5,7 +5,7 @@ use crate::registry::{FieldInfo, ProblemSchemaEntry, VariantDimension}; use crate::topology::{Graph, SimpleGraph}; -use crate::traits::{Problem, SatisfactionProblem}; +use crate::traits::{Problem, WitnessProblem}; use crate::variant::VariantParam; use serde::{Deserialize, Serialize}; use std::collections::BTreeSet; @@ -132,7 +132,7 @@ where } } -impl SatisfactionProblem for DisjointConnectingPaths {} +impl WitnessProblem for DisjointConnectingPaths {} fn canonical_edges(graph: &G) -> Vec<(usize, usize)> { let mut edges = graph diff --git a/src/models/graph/generalized_hex.rs b/src/models/graph/generalized_hex.rs index 67f9389ac..79b016fa2 100644 --- a/src/models/graph/generalized_hex.rs +++ b/src/models/graph/generalized_hex.rs @@ -9,7 +9,7 @@ use serde::{Deserialize, Serialize}; use crate::registry::{FieldInfo, ProblemSchemaEntry, VariantDimension}; use crate::topology::{Graph, SimpleGraph}; -use crate::traits::{Problem, SatisfactionProblem}; +use crate::traits::{Problem, WitnessProblem}; use crate::variant::VariantParam; inventory::submit! { @@ -261,7 +261,7 @@ where } } -impl SatisfactionProblem for GeneralizedHex where G: Graph + VariantParam {} +impl WitnessProblem for GeneralizedHex where G: Graph + VariantParam {} crate::declare_variants! { default sat GeneralizedHex => "3^num_playable_vertices", diff --git a/src/models/graph/graph_partitioning.rs b/src/models/graph/graph_partitioning.rs index 6930fc775..694420868 100644 --- a/src/models/graph/graph_partitioning.rs +++ b/src/models/graph/graph_partitioning.rs @@ -5,8 +5,8 @@ use crate::registry::{FieldInfo, ProblemSchemaEntry, VariantDimension}; use crate::topology::{Graph, SimpleGraph}; -use crate::traits::{OptimizationProblem, Problem}; -use crate::types::{Direction, SolutionSize}; +use crate::traits::{ObjectiveProblem, Problem}; +use crate::types::{ExtremumSense, Min}; use serde::{Deserialize, Serialize}; inventory::submit! { @@ -40,7 +40,7 @@ inventory::submit! { /// ``` /// use problemreductions::models::graph::GraphPartitioning; /// use problemreductions::topology::SimpleGraph; -/// use problemreductions::types::SolutionSize; +/// use problemreductions::types::Min; /// use problemreductions::{Problem, Solver, BruteForce}; /// /// // Square graph: 0-1, 1-2, 2-3, 3-0 @@ -48,12 +48,12 @@ inventory::submit! { /// let problem = GraphPartitioning::new(graph); /// /// let solver = BruteForce::new(); -/// let solutions = solver.find_all_best(&problem); +/// let solutions = solver.find_all_witnesses(&problem); /// /// // Minimum bisection of a 4-cycle: cut = 2 /// for sol in solutions { /// let size = problem.evaluate(&sol); -/// assert_eq!(size, SolutionSize::Valid(2)); +/// assert_eq!(size, Min(Some(2))); /// } /// ``` #[derive(Debug, Clone, Serialize, Deserialize)] @@ -92,7 +92,7 @@ where G: Graph + crate::variant::VariantParam, { const NAME: &'static str = "GraphPartitioning"; - type Value = SolutionSize; + type Value = Min; fn variant() -> Vec<(&'static str, &'static str)> { crate::variant_params![G] @@ -102,19 +102,19 @@ where vec![2; self.graph.num_vertices()] } - fn evaluate(&self, config: &[usize]) -> SolutionSize { + fn evaluate(&self, config: &[usize]) -> Min { let n = self.graph.num_vertices(); if config.len() != n { - return SolutionSize::Invalid; + return Min(None); } // Balanced bisection requires even n if !n.is_multiple_of(2) { - return SolutionSize::Invalid; + return Min(None); } // Check balanced: exactly n/2 vertices in partition 1 let count_ones = config.iter().filter(|&&x| x == 1).count(); if count_ones != n / 2 { - return SolutionSize::Invalid; + return Min(None); } // Count crossing edges let mut cut = 0i32; @@ -123,18 +123,18 @@ where cut += 1; } } - SolutionSize::Valid(cut) + Min(Some(cut)) } } -impl OptimizationProblem for GraphPartitioning +impl ObjectiveProblem for GraphPartitioning where G: Graph + crate::variant::VariantParam, { type Objective = i32; - fn direction(&self) -> Direction { - Direction::Minimize + fn direction(&self) -> ExtremumSense { + ExtremumSense::Minimize } } @@ -163,7 +163,7 @@ pub(crate) fn canonical_model_example_specs() -> Vec(graph: &G, config: &[usize] true } -impl SatisfactionProblem for HamiltonianCircuit {} +impl WitnessProblem for HamiltonianCircuit {} #[cfg(feature = "example-db")] pub(crate) fn canonical_model_example_specs() -> Vec { diff --git a/src/models/graph/hamiltonian_path.rs b/src/models/graph/hamiltonian_path.rs index 94ca1248e..29b446045 100644 --- a/src/models/graph/hamiltonian_path.rs +++ b/src/models/graph/hamiltonian_path.rs @@ -5,7 +5,7 @@ use crate::registry::{FieldInfo, ProblemSchemaEntry, VariantDimension}; use crate::topology::{Graph, SimpleGraph}; -use crate::traits::{Problem, SatisfactionProblem}; +use crate::traits::{Problem, WitnessProblem}; use crate::variant::VariantParam; use serde::{Deserialize, Serialize}; @@ -58,7 +58,7 @@ inventory::submit! { /// let problem = HamiltonianPath::new(graph); /// /// let solver = BruteForce::new(); -/// let solution = solver.find_satisfying(&problem); +/// let solution = solver.find_witness(&problem); /// assert!(solution.is_some()); /// ``` #[derive(Debug, Clone, Serialize, Deserialize)] @@ -115,7 +115,7 @@ where } } -impl SatisfactionProblem for HamiltonianPath {} +impl WitnessProblem for HamiltonianPath {} /// Check if a configuration represents a valid Hamiltonian path in the graph. /// diff --git a/src/models/graph/integral_flow_bundles.rs b/src/models/graph/integral_flow_bundles.rs index 493839e96..69e39f4c6 100644 --- a/src/models/graph/integral_flow_bundles.rs +++ b/src/models/graph/integral_flow_bundles.rs @@ -5,7 +5,7 @@ use crate::registry::{FieldInfo, ProblemSchemaEntry, ProblemSizeFieldEntry}; use crate::topology::DirectedGraph; -use crate::traits::{Problem, SatisfactionProblem}; +use crate::traits::{Problem, WitnessProblem}; use serde::{Deserialize, Serialize}; use std::collections::BTreeSet; @@ -264,7 +264,7 @@ impl Problem for IntegralFlowBundles { } } -impl SatisfactionProblem for IntegralFlowBundles {} +impl WitnessProblem for IntegralFlowBundles {} crate::declare_variants! { default sat IntegralFlowBundles => "2^num_arcs", diff --git a/src/models/graph/integral_flow_homologous_arcs.rs b/src/models/graph/integral_flow_homologous_arcs.rs index bce5b8b81..ece6bb465 100644 --- a/src/models/graph/integral_flow_homologous_arcs.rs +++ b/src/models/graph/integral_flow_homologous_arcs.rs @@ -6,7 +6,7 @@ use crate::registry::{FieldInfo, ProblemSchemaEntry, ProblemSizeFieldEntry}; use crate::topology::DirectedGraph; -use crate::traits::{Problem, SatisfactionProblem}; +use crate::traits::{Problem, WitnessProblem}; use serde::{Deserialize, Serialize}; inventory::submit! { @@ -205,7 +205,7 @@ impl Problem for IntegralFlowHomologousArcs { } } -impl SatisfactionProblem for IntegralFlowHomologousArcs {} +impl WitnessProblem for IntegralFlowHomologousArcs {} crate::declare_variants! { default sat IntegralFlowHomologousArcs => "(max_capacity + 1)^num_arcs", diff --git a/src/models/graph/integral_flow_with_multipliers.rs b/src/models/graph/integral_flow_with_multipliers.rs index 3412e7ee2..325f8f6be 100644 --- a/src/models/graph/integral_flow_with_multipliers.rs +++ b/src/models/graph/integral_flow_with_multipliers.rs @@ -6,7 +6,7 @@ use crate::registry::{FieldInfo, ProblemSchemaEntry, ProblemSizeFieldEntry}; use crate::topology::DirectedGraph; -use crate::traits::{Problem, SatisfactionProblem}; +use crate::traits::{Problem, WitnessProblem}; use serde::{Deserialize, Serialize}; inventory::submit! { @@ -213,7 +213,7 @@ impl Problem for IntegralFlowWithMultipliers { } } -impl SatisfactionProblem for IntegralFlowWithMultipliers {} +impl WitnessProblem for IntegralFlowWithMultipliers {} crate::declare_variants! { default sat IntegralFlowWithMultipliers => "(max_capacity + 1)^num_arcs", diff --git a/src/models/graph/isomorphic_spanning_tree.rs b/src/models/graph/isomorphic_spanning_tree.rs index f200edc83..33fb6ebb6 100644 --- a/src/models/graph/isomorphic_spanning_tree.rs +++ b/src/models/graph/isomorphic_spanning_tree.rs @@ -6,7 +6,7 @@ use crate::registry::{FieldInfo, ProblemSchemaEntry}; use crate::topology::{Graph, SimpleGraph}; -use crate::traits::{Problem, SatisfactionProblem}; +use crate::traits::{Problem, WitnessProblem}; use serde::{Deserialize, Serialize}; inventory::submit! { @@ -44,7 +44,7 @@ inventory::submit! { /// let problem = IsomorphicSpanningTree::new(graph, tree); /// /// let solver = BruteForce::new(); -/// let sol = solver.find_satisfying(&problem); +/// let sol = solver.find_witness(&problem); /// assert!(sol.is_some()); /// ``` #[derive(Debug, Clone, Serialize, Deserialize)] @@ -163,7 +163,7 @@ impl Problem for IsomorphicSpanningTree { } } -impl SatisfactionProblem for IsomorphicSpanningTree {} +impl WitnessProblem for IsomorphicSpanningTree {} #[cfg(feature = "example-db")] pub(crate) fn canonical_model_example_specs() -> Vec { diff --git a/src/models/graph/kclique.rs b/src/models/graph/kclique.rs index 6cb19fac9..930bc8385 100644 --- a/src/models/graph/kclique.rs +++ b/src/models/graph/kclique.rs @@ -5,7 +5,7 @@ use crate::registry::{FieldInfo, ProblemSchemaEntry, VariantDimension}; use crate::topology::{Graph, SimpleGraph}; -use crate::traits::{Problem, SatisfactionProblem}; +use crate::traits::{Problem, WitnessProblem}; use serde::{Deserialize, Serialize}; inventory::submit! { @@ -88,7 +88,7 @@ where } } -impl SatisfactionProblem for KClique where G: Graph + crate::variant::VariantParam {} +impl WitnessProblem for KClique where G: Graph + crate::variant::VariantParam {} fn is_kclique_config(graph: &G, config: &[usize], k: usize) -> bool { if config.len() != graph.num_vertices() { diff --git a/src/models/graph/kcoloring.rs b/src/models/graph/kcoloring.rs index 382d75c88..4c9a8f679 100644 --- a/src/models/graph/kcoloring.rs +++ b/src/models/graph/kcoloring.rs @@ -5,7 +5,7 @@ use crate::registry::{FieldInfo, ProblemSchemaEntry, VariantDimension}; use crate::topology::{Graph, SimpleGraph}; -use crate::traits::{Problem, SatisfactionProblem}; +use crate::traits::{Problem, WitnessProblem}; use crate::variant::{KValue, VariantParam, K2, K3, K4, K5, KN}; use serde::{Deserialize, Serialize}; @@ -49,7 +49,7 @@ inventory::submit! { /// let problem = KColoring::::new(graph); /// /// let solver = BruteForce::new(); -/// let solutions = solver.find_all_satisfying(&problem); +/// let solutions = solver.find_all_witnesses(&problem); /// /// // Verify all solutions are valid colorings /// for sol in &solutions { @@ -160,7 +160,7 @@ where } } -impl SatisfactionProblem for KColoring {} +impl WitnessProblem for KColoring {} /// Check if a coloring is valid for a graph. /// diff --git a/src/models/graph/kth_best_spanning_tree.rs b/src/models/graph/kth_best_spanning_tree.rs index db61c1713..476566cad 100644 --- a/src/models/graph/kth_best_spanning_tree.rs +++ b/src/models/graph/kth_best_spanning_tree.rs @@ -5,7 +5,7 @@ use crate::registry::{FieldInfo, ProblemSchemaEntry, VariantDimension}; use crate::topology::{Graph, SimpleGraph}; -use crate::traits::{Problem, SatisfactionProblem}; +use crate::traits::{Problem, WitnessProblem}; use crate::types::WeightElement; use num_traits::Zero; use serde::{Deserialize, Serialize}; @@ -222,7 +222,7 @@ where } } -impl SatisfactionProblem for KthBestSpanningTree where +impl WitnessProblem for KthBestSpanningTree where W: WeightElement + crate::variant::VariantParam { } diff --git a/src/models/graph/length_bounded_disjoint_paths.rs b/src/models/graph/length_bounded_disjoint_paths.rs index c82ac0093..692e9d163 100644 --- a/src/models/graph/length_bounded_disjoint_paths.rs +++ b/src/models/graph/length_bounded_disjoint_paths.rs @@ -5,7 +5,7 @@ use crate::registry::{FieldInfo, ProblemSchemaEntry, VariantDimension}; use crate::topology::{Graph, SimpleGraph}; -use crate::traits::{Problem, SatisfactionProblem}; +use crate::traits::{Problem, WitnessProblem}; use crate::variant::VariantParam; use serde::{Deserialize, Serialize}; @@ -150,7 +150,7 @@ where } } -impl SatisfactionProblem for LengthBoundedDisjointPaths {} +impl WitnessProblem for LengthBoundedDisjointPaths {} fn is_valid_path_collection( graph: &G, diff --git a/src/models/graph/longest_circuit.rs b/src/models/graph/longest_circuit.rs index 69593fafc..d4ede94d0 100644 --- a/src/models/graph/longest_circuit.rs +++ b/src/models/graph/longest_circuit.rs @@ -5,7 +5,7 @@ use crate::registry::{FieldInfo, ProblemSchemaEntry, VariantDimension}; use crate::topology::{Graph, SimpleGraph}; -use crate::traits::{Problem, SatisfactionProblem}; +use crate::traits::{Problem, WitnessProblem}; use crate::types::WeightElement; use num_traits::Zero; use serde::{Deserialize, Serialize}; @@ -242,7 +242,7 @@ pub(crate) fn is_simple_circuit(graph: &G, config: &[usize]) -> bool { visited_selected_vertices == selected_vertices.len() } -impl SatisfactionProblem for LongestCircuit +impl WitnessProblem for LongestCircuit where G: Graph + crate::variant::VariantParam, W: WeightElement + crate::variant::VariantParam, diff --git a/src/models/graph/longest_path.rs b/src/models/graph/longest_path.rs index 524b43669..12bf7f5c9 100644 --- a/src/models/graph/longest_path.rs +++ b/src/models/graph/longest_path.rs @@ -5,8 +5,8 @@ use crate::registry::{FieldInfo, ProblemSchemaEntry, VariantDimension}; use crate::topology::{Graph, SimpleGraph}; -use crate::traits::{OptimizationProblem, Problem}; -use crate::types::{Direction, One, SolutionSize, WeightElement}; +use crate::traits::{ObjectiveProblem, Problem}; +use crate::types::{ExtremumSense, Max, One, WeightElement}; use num_traits::Zero; use serde::{Deserialize, Serialize}; use std::collections::VecDeque; @@ -150,7 +150,7 @@ where W: WeightElement + crate::variant::VariantParam, { const NAME: &'static str = "LongestPath"; - type Value = SolutionSize; + type Value = Max; fn variant() -> Vec<(&'static str, &'static str)> { crate::variant_params![G, W] @@ -160,9 +160,9 @@ where vec![2; self.graph.num_edges()] } - fn evaluate(&self, config: &[usize]) -> SolutionSize { + fn evaluate(&self, config: &[usize]) -> Max { if !self.is_valid_solution(config) { - return SolutionSize::Invalid; + return Max(None); } let mut total = W::Sum::zero(); @@ -171,19 +171,19 @@ where total += self.edge_lengths[idx].to_sum(); } } - SolutionSize::Valid(total) + Max(Some(total)) } } -impl OptimizationProblem for LongestPath +impl ObjectiveProblem for LongestPath where G: Graph + crate::variant::VariantParam, W: WeightElement + crate::variant::VariantParam, { type Objective = W::Sum; - fn direction(&self) -> Direction { - Direction::Maximize + fn direction(&self) -> ExtremumSense { + ExtremumSense::Maximize } } @@ -294,7 +294,7 @@ pub(crate) fn canonical_model_example_specs() -> Vec; + type Value = Max; fn variant() -> Vec<(&'static str, &'static str)> { crate::variant_params![G, W] @@ -181,22 +181,22 @@ where vec![2; self.graph.num_vertices()] } - fn evaluate(&self, config: &[usize]) -> SolutionSize { + fn evaluate(&self, config: &[usize]) -> Max { // All cuts are valid, so always return Valid let partition: Vec = config.iter().map(|&c| c != 0).collect(); - SolutionSize::Valid(cut_size(&self.graph, &self.edge_weights, &partition)) + Max(Some(cut_size(&self.graph, &self.edge_weights, &partition))) } } -impl OptimizationProblem for MaxCut +impl ObjectiveProblem for MaxCut where G: Graph + crate::variant::VariantParam, W: WeightElement + crate::variant::VariantParam, { type Objective = W::Sum; - fn direction(&self) -> Direction { - Direction::Maximize + fn direction(&self) -> ExtremumSense { + ExtremumSense::Maximize } } @@ -233,7 +233,7 @@ pub(crate) fn canonical_model_example_specs() -> Vec; + type Value = Max; fn variant() -> Vec<(&'static str, &'static str)> { crate::variant_params![G, W] @@ -163,9 +163,9 @@ where vec![2; self.graph.num_vertices()] } - fn evaluate(&self, config: &[usize]) -> SolutionSize { + fn evaluate(&self, config: &[usize]) -> Max { if !self.is_maximal(config) { - return SolutionSize::Invalid; + return Max(None); } let mut total = W::Sum::zero(); for (i, &selected) in config.iter().enumerate() { @@ -173,19 +173,19 @@ where total += self.weights[i].to_sum(); } } - SolutionSize::Valid(total) + Max(Some(total)) } } -impl OptimizationProblem for MaximalIS +impl ObjectiveProblem for MaximalIS where G: Graph + crate::variant::VariantParam, W: WeightElement + crate::variant::VariantParam, { type Objective = W::Sum; - fn direction(&self) -> Direction { - Direction::Maximize + fn direction(&self) -> ExtremumSense { + ExtremumSense::Maximize } } @@ -198,7 +198,7 @@ pub(crate) fn canonical_model_example_specs() -> Vec() == 3)); @@ -119,7 +119,7 @@ where W: WeightElement + crate::variant::VariantParam, { const NAME: &'static str = "MaximumClique"; - type Value = SolutionSize; + type Value = Max; fn variant() -> Vec<(&'static str, &'static str)> { crate::variant_params![G, W] @@ -129,9 +129,9 @@ where vec![2; self.graph.num_vertices()] } - fn evaluate(&self, config: &[usize]) -> SolutionSize { + fn evaluate(&self, config: &[usize]) -> Max { if !is_clique_config(&self.graph, config) { - return SolutionSize::Invalid; + return Max(None); } let mut total = W::Sum::zero(); for (i, &selected) in config.iter().enumerate() { @@ -139,19 +139,19 @@ where total += self.weights[i].to_sum(); } } - SolutionSize::Valid(total) + Max(Some(total)) } } -impl OptimizationProblem for MaximumClique +impl ObjectiveProblem for MaximumClique where G: Graph + crate::variant::VariantParam, W: WeightElement + crate::variant::VariantParam, { type Objective = W::Sum; - fn direction(&self) -> Direction { - Direction::Maximize + fn direction(&self) -> ExtremumSense { + ExtremumSense::Maximize } } @@ -189,7 +189,7 @@ pub(crate) fn canonical_model_example_specs() -> Vec() == 1)); @@ -119,7 +119,7 @@ where W: WeightElement + crate::variant::VariantParam, { const NAME: &'static str = "MaximumIndependentSet"; - type Value = SolutionSize; + type Value = Max; fn variant() -> Vec<(&'static str, &'static str)> { crate::variant_params![G, W] @@ -129,9 +129,9 @@ where vec![2; self.graph.num_vertices()] } - fn evaluate(&self, config: &[usize]) -> SolutionSize { + fn evaluate(&self, config: &[usize]) -> Max { if !is_independent_set_config(&self.graph, config) { - return SolutionSize::Invalid; + return Max(None); } let mut total = W::Sum::zero(); for (i, &selected) in config.iter().enumerate() { @@ -139,19 +139,19 @@ where total += self.weights[i].to_sum(); } } - SolutionSize::Valid(total) + Max(Some(total)) } } -impl OptimizationProblem for MaximumIndependentSet +impl ObjectiveProblem for MaximumIndependentSet where G: Graph + crate::variant::VariantParam, W: WeightElement + crate::variant::VariantParam, { type Objective = W::Sum; - fn direction(&self) -> Direction { - Direction::Maximize + fn direction(&self) -> ExtremumSense { + ExtremumSense::Maximize } } @@ -204,7 +204,7 @@ pub(crate) fn canonical_model_example_specs() -> Vec Vec::unit_weights(graph); /// /// let solver = BruteForce::new(); -/// let solutions = solver.find_all_best(&problem); +/// let solutions = solver.find_all_witnesses(&problem); /// /// // Maximum matching has 1 edge /// for sol in &solutions { @@ -187,7 +187,7 @@ where W: WeightElement + crate::variant::VariantParam, { const NAME: &'static str = "MaximumMatching"; - type Value = SolutionSize; + type Value = Max; fn variant() -> Vec<(&'static str, &'static str)> { crate::variant_params![G, W] @@ -197,9 +197,9 @@ where vec![2; self.graph.num_edges()] } - fn evaluate(&self, config: &[usize]) -> SolutionSize { + fn evaluate(&self, config: &[usize]) -> Max { if !self.is_valid_matching(config) { - return SolutionSize::Invalid; + return Max(None); } let mut total = W::Sum::zero(); for (idx, &selected) in config.iter().enumerate() { @@ -209,19 +209,19 @@ where } } } - SolutionSize::Valid(total) + Max(Some(total)) } } -impl OptimizationProblem for MaximumMatching +impl ObjectiveProblem for MaximumMatching where G: Graph + crate::variant::VariantParam, W: WeightElement + crate::variant::VariantParam, { type Objective = W::Sum; - fn direction(&self) -> Direction { - Direction::Maximize + fn direction(&self) -> ExtremumSense { + ExtremumSense::Maximize } } @@ -238,7 +238,7 @@ pub(crate) fn canonical_model_example_specs() -> Vec SatisfactionProblem for MinMaxMulticenter +impl WitnessProblem for MinMaxMulticenter where G: Graph + crate::variant::VariantParam, W: WeightElement + crate::variant::VariantParam, diff --git a/src/models/graph/minimum_cut_into_bounded_sets.rs b/src/models/graph/minimum_cut_into_bounded_sets.rs index 3327e3ba3..9afb02840 100644 --- a/src/models/graph/minimum_cut_into_bounded_sets.rs +++ b/src/models/graph/minimum_cut_into_bounded_sets.rs @@ -6,7 +6,7 @@ use crate::registry::{FieldInfo, ProblemSchemaEntry, VariantDimension}; use crate::topology::{Graph, SimpleGraph}; -use crate::traits::{Problem, SatisfactionProblem}; +use crate::traits::{Problem, WitnessProblem}; use crate::types::WeightElement; use num_traits::Zero; use serde::{Deserialize, Serialize}; @@ -208,7 +208,7 @@ where } } -impl SatisfactionProblem for MinimumCutIntoBoundedSets +impl WitnessProblem for MinimumCutIntoBoundedSets where G: Graph + crate::variant::VariantParam, W: WeightElement + crate::variant::VariantParam, diff --git a/src/models/graph/minimum_dominating_set.rs b/src/models/graph/minimum_dominating_set.rs index d09df3a20..38aa2fd83 100644 --- a/src/models/graph/minimum_dominating_set.rs +++ b/src/models/graph/minimum_dominating_set.rs @@ -5,8 +5,8 @@ use crate::registry::{FieldInfo, ProblemSchemaEntry, VariantDimension}; use crate::topology::{Graph, SimpleGraph}; -use crate::traits::{OptimizationProblem, Problem}; -use crate::types::{Direction, SolutionSize, WeightElement}; +use crate::traits::{ObjectiveProblem, Problem}; +use crate::types::{ExtremumSense, Min, WeightElement}; use num_traits::Zero; use serde::{Deserialize, Serialize}; use std::collections::HashSet; @@ -48,7 +48,7 @@ inventory::submit! { /// let problem = MinimumDominatingSet::new(graph, vec![1; 4]); /// /// let solver = BruteForce::new(); -/// let solutions = solver.find_all_best(&problem); +/// let solutions = solver.find_all_witnesses(&problem); /// /// // Minimum dominating set is just the center vertex /// assert!(solutions.contains(&vec![1, 0, 0, 0])); @@ -139,7 +139,7 @@ where W: WeightElement + crate::variant::VariantParam, { const NAME: &'static str = "MinimumDominatingSet"; - type Value = SolutionSize; + type Value = Min; fn variant() -> Vec<(&'static str, &'static str)> { crate::variant_params![G, W] @@ -149,9 +149,9 @@ where vec![2; self.graph.num_vertices()] } - fn evaluate(&self, config: &[usize]) -> SolutionSize { + fn evaluate(&self, config: &[usize]) -> Min { if !self.is_dominating(config) { - return SolutionSize::Invalid; + return Min(None); } let mut total = W::Sum::zero(); for (i, &selected) in config.iter().enumerate() { @@ -159,19 +159,19 @@ where total += self.weights[i].to_sum(); } } - SolutionSize::Valid(total) + Min(Some(total)) } } -impl OptimizationProblem for MinimumDominatingSet +impl ObjectiveProblem for MinimumDominatingSet where G: Graph + crate::variant::VariantParam, W: WeightElement + crate::variant::VariantParam, { type Objective = W::Sum; - fn direction(&self) -> Direction { - Direction::Minimize + fn direction(&self) -> ExtremumSense { + ExtremumSense::Minimize } } @@ -188,7 +188,7 @@ pub(crate) fn canonical_model_example_specs() -> Vec; + type Value = Min; fn variant() -> Vec<(&'static str, &'static str)> { crate::variant_params![] @@ -174,9 +174,9 @@ impl Problem for MinimumDummyActivitiesPert { vec![2; self.graph.num_arcs()] } - fn evaluate(&self, config: &[usize]) -> SolutionSize { + fn evaluate(&self, config: &[usize]) -> Min { let Some(candidate) = self.build_candidate_network(config) else { - return SolutionSize::Invalid; + return Min(None); }; let source_reachability = reachability_matrix(&self.graph); @@ -189,22 +189,22 @@ impl Problem for MinimumDummyActivitiesPert { || event_reachability[candidate.finish_events[source]] [candidate.start_events[target]]; if source_reachability[source][target] != pert_reachable { - return SolutionSize::Invalid; + return Min(None); } } } - SolutionSize::Valid( + Min(Some( i32::try_from(candidate.num_dummy_arcs).expect("dummy activity count must fit in i32"), - ) + )) } } -impl OptimizationProblem for MinimumDummyActivitiesPert { +impl ObjectiveProblem for MinimumDummyActivitiesPert { type Objective = i32; - fn direction(&self) -> Direction { - Direction::Minimize + fn direction(&self) -> ExtremumSense { + ExtremumSense::Minimize } } @@ -221,7 +221,7 @@ pub(crate) fn canonical_model_example_specs() -> Vec(), 1); @@ -126,7 +126,7 @@ where W: WeightElement + crate::variant::VariantParam, { const NAME: &'static str = "MinimumFeedbackArcSet"; - type Value = SolutionSize; + type Value = Min; fn variant() -> Vec<(&'static str, &'static str)> { crate::variant_params![W] @@ -136,9 +136,9 @@ where vec![2; self.graph.num_arcs()] } - fn evaluate(&self, config: &[usize]) -> SolutionSize { + fn evaluate(&self, config: &[usize]) -> Min { if !is_valid_fas(&self.graph, config) { - return SolutionSize::Invalid; + return Min(None); } let mut total = W::Sum::zero(); for (i, &selected) in config.iter().enumerate() { @@ -146,18 +146,18 @@ where total += self.weights[i].to_sum(); } } - SolutionSize::Valid(total) + Min(Some(total)) } } -impl OptimizationProblem for MinimumFeedbackArcSet +impl ObjectiveProblem for MinimumFeedbackArcSet where W: WeightElement + crate::variant::VariantParam, { type Objective = W::Sum; - fn direction(&self) -> Direction { - Direction::Minimize + fn direction(&self) -> ExtremumSense { + ExtremumSense::Minimize } } @@ -190,7 +190,7 @@ pub(crate) fn canonical_model_example_specs() -> Vec; + type Value = Min; fn variant() -> Vec<(&'static str, &'static str)> { crate::variant_params![W] @@ -132,15 +132,15 @@ where vec![2; self.graph.num_vertices()] } - fn evaluate(&self, config: &[usize]) -> SolutionSize { + fn evaluate(&self, config: &[usize]) -> Min { if config.len() != self.graph.num_vertices() { - return SolutionSize::Invalid; + return Min(None); } // keep[v] = true if vertex v is NOT selected for removal let keep: Vec = config.iter().map(|&c| c == 0).collect(); let subgraph = self.graph.induced_subgraph(&keep); if !subgraph.is_dag() { - return SolutionSize::Invalid; + return Min(None); } let mut total = W::Sum::zero(); for (i, &selected) in config.iter().enumerate() { @@ -148,18 +148,18 @@ where total += self.weights[i].to_sum(); } } - SolutionSize::Valid(total) + Min(Some(total)) } } -impl OptimizationProblem for MinimumFeedbackVertexSet +impl ObjectiveProblem for MinimumFeedbackVertexSet where W: WeightElement + crate::variant::VariantParam, { type Objective = W::Sum; - fn direction(&self) -> Direction { - Direction::Minimize + fn direction(&self) -> ExtremumSense { + ExtremumSense::Minimize } } @@ -180,7 +180,7 @@ pub(crate) fn canonical_model_example_specs() -> Vec; + type Value = Min; fn variant() -> Vec<(&'static str, &'static str)> { crate::variant_params![G, W] @@ -171,9 +171,9 @@ where vec![2; self.graph.num_edges()] } - fn evaluate(&self, config: &[usize]) -> SolutionSize { + fn evaluate(&self, config: &[usize]) -> Min { if !terminals_separated(&self.graph, &self.terminals, config) { - return SolutionSize::Invalid; + return Min(None); } let mut total = W::Sum::zero(); for (idx, &selected) in config.iter().enumerate() { @@ -183,19 +183,19 @@ where } } } - SolutionSize::Valid(total) + Min(Some(total)) } } -impl OptimizationProblem for MinimumMultiwayCut +impl ObjectiveProblem for MinimumMultiwayCut where G: Graph + crate::variant::VariantParam, W: WeightElement + crate::variant::VariantParam, { type Objective = W::Sum; - fn direction(&self) -> Direction { - Direction::Minimize + fn direction(&self) -> ExtremumSense { + ExtremumSense::Minimize } } @@ -213,7 +213,7 @@ pub(crate) fn canonical_model_example_specs() -> Vec; + type Value = Min; fn variant() -> Vec<(&'static str, &'static str)> { crate::variant_params![G, W] @@ -224,17 +224,17 @@ where vec![2; self.graph.num_vertices()] } - fn evaluate(&self, config: &[usize]) -> SolutionSize { + fn evaluate(&self, config: &[usize]) -> Min { // Check exactly K centers are selected let num_selected: usize = config.iter().sum(); if num_selected != self.k { - return SolutionSize::Invalid; + return Min(None); } // Compute shortest distances to nearest center let distances = match self.shortest_distances(config) { Some(d) => d, - None => return SolutionSize::Invalid, + None => return Min(None), }; // Compute total weighted distance: Σ w(v) * d(v) @@ -243,19 +243,19 @@ where total += self.vertex_weights[v].to_sum() * dist.clone(); } - SolutionSize::Valid(total) + Min(Some(total)) } } -impl OptimizationProblem for MinimumSumMulticenter +impl ObjectiveProblem for MinimumSumMulticenter where G: Graph + crate::variant::VariantParam, W: WeightElement + crate::variant::VariantParam, { type Objective = W::Sum; - fn direction(&self) -> Direction { - Direction::Minimize + fn direction(&self) -> ExtremumSense { + ExtremumSense::Minimize } } @@ -286,7 +286,7 @@ pub(crate) fn canonical_model_example_specs() -> Vec; + type Value = Min; fn variant() -> Vec<(&'static str, &'static str)> { crate::variant_params![G, W] @@ -124,9 +124,9 @@ where vec![2; self.graph.num_vertices()] } - fn evaluate(&self, config: &[usize]) -> SolutionSize { + fn evaluate(&self, config: &[usize]) -> Min { if !is_vertex_cover_config(&self.graph, config) { - return SolutionSize::Invalid; + return Min(None); } let mut total = W::Sum::zero(); for (i, &selected) in config.iter().enumerate() { @@ -134,19 +134,19 @@ where total += self.weights[i].to_sum(); } } - SolutionSize::Valid(total) + Min(Some(total)) } } -impl OptimizationProblem for MinimumVertexCover +impl ObjectiveProblem for MinimumVertexCover where G: Graph + crate::variant::VariantParam, W: WeightElement + crate::variant::VariantParam, { type Objective = W::Sum; - fn direction(&self) -> Direction { - Direction::Minimize + fn direction(&self) -> ExtremumSense { + ExtremumSense::Minimize } } @@ -175,7 +175,7 @@ pub(crate) fn canonical_model_example_specs() -> Vec SatisfactionProblem for MixedChinesePostman where +impl WitnessProblem for MixedChinesePostman where W: WeightElement + crate::variant::VariantParam { } diff --git a/src/models/graph/multiple_choice_branching.rs b/src/models/graph/multiple_choice_branching.rs index d8748ac5d..82c15b7ff 100644 --- a/src/models/graph/multiple_choice_branching.rs +++ b/src/models/graph/multiple_choice_branching.rs @@ -6,7 +6,7 @@ use crate::registry::{FieldInfo, ProblemSchemaEntry, VariantDimension}; use crate::topology::DirectedGraph; -use crate::traits::{Problem, SatisfactionProblem}; +use crate::traits::{Problem, WitnessProblem}; use crate::types::WeightElement; use num_traits::Zero; use serde::de::Error as _; @@ -197,7 +197,7 @@ where } } -impl SatisfactionProblem for MultipleChoiceBranching where +impl WitnessProblem for MultipleChoiceBranching where W: WeightElement + crate::variant::VariantParam { } diff --git a/src/models/graph/multiple_copy_file_allocation.rs b/src/models/graph/multiple_copy_file_allocation.rs index 5e9abce25..0f755a245 100644 --- a/src/models/graph/multiple_copy_file_allocation.rs +++ b/src/models/graph/multiple_copy_file_allocation.rs @@ -5,7 +5,7 @@ use crate::registry::{FieldInfo, ProblemSchemaEntry, ProblemSizeFieldEntry}; use crate::topology::{Graph, SimpleGraph}; -use crate::traits::{Problem, SatisfactionProblem}; +use crate::traits::{Problem, WitnessProblem}; use serde::{Deserialize, Serialize}; use std::collections::VecDeque; @@ -194,7 +194,7 @@ impl Problem for MultipleCopyFileAllocation { } } -impl SatisfactionProblem for MultipleCopyFileAllocation {} +impl WitnessProblem for MultipleCopyFileAllocation {} #[cfg(feature = "example-db")] pub(crate) fn canonical_model_example_specs() -> Vec { diff --git a/src/models/graph/optimal_linear_arrangement.rs b/src/models/graph/optimal_linear_arrangement.rs index df85dbfff..451455e87 100644 --- a/src/models/graph/optimal_linear_arrangement.rs +++ b/src/models/graph/optimal_linear_arrangement.rs @@ -6,7 +6,7 @@ use crate::registry::{FieldInfo, ProblemSchemaEntry, VariantDimension}; use crate::topology::{Graph, SimpleGraph}; -use crate::traits::{Problem, SatisfactionProblem}; +use crate::traits::{Problem, WitnessProblem}; use serde::{Deserialize, Serialize}; inventory::submit! { @@ -57,7 +57,7 @@ inventory::submit! { /// let problem = OptimalLinearArrangement::new(graph, 3); /// /// let solver = BruteForce::new(); -/// let solution = solver.find_satisfying(&problem); +/// let solution = solver.find_witness(&problem); /// assert!(solution.is_some()); /// ``` #[derive(Debug, Clone, Serialize, Deserialize)] @@ -161,7 +161,7 @@ where } } -impl SatisfactionProblem for OptimalLinearArrangement {} +impl WitnessProblem for OptimalLinearArrangement {} crate::declare_variants! { default sat OptimalLinearArrangement => "2^num_vertices", 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 a7bc31e7e..f84e886f8 100644 --- a/src/models/graph/partition_into_paths_of_length_2.rs +++ b/src/models/graph/partition_into_paths_of_length_2.rs @@ -8,7 +8,7 @@ use crate::registry::{FieldInfo, ProblemSchemaEntry, VariantDimension}; use crate::topology::{Graph, SimpleGraph}; -use crate::traits::{Problem, SatisfactionProblem}; +use crate::traits::{Problem, WitnessProblem}; use crate::variant::VariantParam; use serde::{Deserialize, Serialize}; @@ -54,7 +54,7 @@ inventory::submit! { /// let problem = PartitionIntoPathsOfLength2::new(graph); /// /// let solver = BruteForce::new(); -/// let solution = solver.find_satisfying(&problem); +/// let solution = solver.find_witness(&problem); /// assert!(solution.is_some()); /// ``` #[derive(Debug, Clone, Serialize, Deserialize)] @@ -164,7 +164,7 @@ where } } -impl SatisfactionProblem for PartitionIntoPathsOfLength2 {} +impl WitnessProblem for PartitionIntoPathsOfLength2 {} crate::declare_variants! { default sat PartitionIntoPathsOfLength2 => "3^num_vertices", diff --git a/src/models/graph/partition_into_triangles.rs b/src/models/graph/partition_into_triangles.rs index 89972a750..8fc7e0694 100644 --- a/src/models/graph/partition_into_triangles.rs +++ b/src/models/graph/partition_into_triangles.rs @@ -5,7 +5,7 @@ use crate::registry::{FieldInfo, ProblemSchemaEntry, VariantDimension}; use crate::topology::{Graph, SimpleGraph}; -use crate::traits::{Problem, SatisfactionProblem}; +use crate::traits::{Problem, WitnessProblem}; use crate::variant::VariantParam; use serde::{Deserialize, Serialize}; @@ -46,7 +46,7 @@ inventory::submit! { /// let problem = PartitionIntoTriangles::new(graph); /// /// let solver = BruteForce::new(); -/// let solution = solver.find_satisfying(&problem); +/// let solution = solver.find_witness(&problem); /// assert!(solution.is_some()); /// ``` #[derive(Debug, Clone, Serialize, Deserialize)] @@ -154,7 +154,7 @@ where } } -impl SatisfactionProblem for PartitionIntoTriangles {} +impl WitnessProblem for PartitionIntoTriangles {} crate::declare_variants! { default sat PartitionIntoTriangles => "2^num_vertices", diff --git a/src/models/graph/path_constrained_network_flow.rs b/src/models/graph/path_constrained_network_flow.rs index d193a6320..087490ce8 100644 --- a/src/models/graph/path_constrained_network_flow.rs +++ b/src/models/graph/path_constrained_network_flow.rs @@ -8,7 +8,7 @@ use crate::registry::{FieldInfo, ProblemSchemaEntry}; use crate::topology::DirectedGraph; -use crate::traits::{Problem, SatisfactionProblem}; +use crate::traits::{Problem, WitnessProblem}; use serde::{Deserialize, Serialize}; use std::collections::HashSet; @@ -234,7 +234,7 @@ impl Problem for PathConstrainedNetworkFlow { } } -impl SatisfactionProblem for PathConstrainedNetworkFlow {} +impl WitnessProblem for PathConstrainedNetworkFlow {} crate::declare_variants! { default sat PathConstrainedNetworkFlow => "(max_capacity + 1)^num_paths", diff --git a/src/models/graph/rooted_tree_arrangement.rs b/src/models/graph/rooted_tree_arrangement.rs index b68cbb0d9..dbf86c687 100644 --- a/src/models/graph/rooted_tree_arrangement.rs +++ b/src/models/graph/rooted_tree_arrangement.rs @@ -6,7 +6,7 @@ use crate::registry::{FieldInfo, ProblemSchemaEntry, VariantDimension}; use crate::topology::{Graph, SimpleGraph}; -use crate::traits::{Problem, SatisfactionProblem}; +use crate::traits::{Problem, WitnessProblem}; use crate::variant::VariantParam; use serde::{Deserialize, Serialize}; @@ -116,7 +116,7 @@ where } } -impl SatisfactionProblem for RootedTreeArrangement {} +impl WitnessProblem for RootedTreeArrangement {} fn analyze_parent_array(parent: &[usize]) -> Option { let n = parent.len(); diff --git a/src/models/graph/rural_postman.rs b/src/models/graph/rural_postman.rs index 0cdf26fe6..c3d256168 100644 --- a/src/models/graph/rural_postman.rs +++ b/src/models/graph/rural_postman.rs @@ -6,7 +6,7 @@ use crate::registry::{FieldInfo, ProblemSchemaEntry, VariantDimension}; use crate::topology::{Graph, SimpleGraph}; -use crate::traits::{Problem, SatisfactionProblem}; +use crate::traits::{Problem, WitnessProblem}; use crate::types::WeightElement; use num_traits::Zero; use serde::{Deserialize, Serialize}; @@ -267,7 +267,7 @@ where } } -impl SatisfactionProblem for RuralPostman +impl WitnessProblem for RuralPostman where G: Graph + crate::variant::VariantParam, W: WeightElement + crate::variant::VariantParam, diff --git a/src/models/graph/shortest_weight_constrained_path.rs b/src/models/graph/shortest_weight_constrained_path.rs index f528a788b..36d373bda 100644 --- a/src/models/graph/shortest_weight_constrained_path.rs +++ b/src/models/graph/shortest_weight_constrained_path.rs @@ -6,7 +6,7 @@ use crate::registry::{FieldInfo, ProblemSchemaEntry, VariantDimension}; use crate::topology::{Graph, SimpleGraph}; -use crate::traits::{Problem, SatisfactionProblem}; +use crate::traits::{Problem, WitnessProblem}; use crate::types::WeightElement; use num_traits::Zero; use serde::{Deserialize, Serialize}; @@ -323,7 +323,7 @@ where } } -impl SatisfactionProblem for ShortestWeightConstrainedPath +impl WitnessProblem for ShortestWeightConstrainedPath where G: Graph + crate::variant::VariantParam, N: WeightElement + crate::variant::VariantParam, diff --git a/src/models/graph/spin_glass.rs b/src/models/graph/spin_glass.rs index 1949f8c7a..f16d4f158 100644 --- a/src/models/graph/spin_glass.rs +++ b/src/models/graph/spin_glass.rs @@ -4,8 +4,8 @@ use crate::registry::{FieldInfo, ProblemSchemaEntry, VariantDimension}; use crate::topology::{Graph, SimpleGraph}; -use crate::traits::{OptimizationProblem, Problem}; -use crate::types::{Direction, SolutionSize, WeightElement}; +use crate::traits::{ObjectiveProblem, Problem}; +use crate::types::{ExtremumSense, Min, WeightElement}; use serde::{Deserialize, Serialize}; inventory::submit! { @@ -56,7 +56,7 @@ inventory::submit! { /// let problem = SpinGlass::::new(2, vec![((0, 1), 1.0)], vec![0.0, 0.0]); /// /// let solver = BruteForce::new(); -/// let solutions = solver.find_all_best(&problem); +/// let solutions = solver.find_all_witnesses(&problem); /// /// // Ground state has opposite spins /// for sol in &solutions { @@ -220,15 +220,15 @@ where + From, { const NAME: &'static str = "SpinGlass"; - type Value = SolutionSize; + type Value = Min; fn dims(&self) -> Vec { vec![2; self.graph.num_vertices()] } - fn evaluate(&self, config: &[usize]) -> SolutionSize { + fn evaluate(&self, config: &[usize]) -> Min { let spins = Self::config_to_spins(config); - SolutionSize::Valid(self.compute_energy(&spins).to_sum()) + Min(Some(self.compute_energy(&spins).to_sum())) } fn variant() -> Vec<(&'static str, &'static str)> { @@ -236,7 +236,7 @@ where } } -impl OptimizationProblem for SpinGlass +impl ObjectiveProblem for SpinGlass where G: Graph + crate::variant::VariantParam, W: WeightElement @@ -251,8 +251,8 @@ where { type Objective = W::Sum; - fn direction(&self) -> Direction { - Direction::Minimize + fn direction(&self) -> ExtremumSense { + ExtremumSense::Minimize } } @@ -278,7 +278,7 @@ pub(crate) fn canonical_model_example_specs() -> Vec; + type Value = Min; fn variant() -> Vec<(&'static str, &'static str)> { crate::variant_params![G, W] @@ -231,9 +231,9 @@ where vec![2; self.graph.num_edges()] } - fn evaluate(&self, config: &[usize]) -> SolutionSize { + fn evaluate(&self, config: &[usize]) -> Min { if !is_valid_steiner_tree(&self.graph, &self.terminals, config) { - return SolutionSize::Invalid; + return Min(None); } let mut total = W::Sum::zero(); for (idx, &selected) in config.iter().enumerate() { @@ -243,19 +243,19 @@ where } } } - SolutionSize::Valid(total) + Min(Some(total)) } } -impl OptimizationProblem for SteinerTree +impl ObjectiveProblem for SteinerTree where G: Graph + crate::variant::VariantParam, W: WeightElement + crate::variant::VariantParam, { type Objective = W::Sum; - fn direction(&self) -> Direction { - Direction::Minimize + fn direction(&self) -> ExtremumSense { + ExtremumSense::Minimize } } @@ -277,7 +277,7 @@ pub(crate) fn canonical_model_example_specs() -> Vec; + type Value = Min; fn variant() -> Vec<(&'static str, &'static str)> { crate::variant_params![G, W] @@ -185,13 +185,13 @@ where vec![2; self.graph.num_edges()] } - fn evaluate(&self, config: &[usize]) -> SolutionSize { + fn evaluate(&self, config: &[usize]) -> Min { if config.len() != self.graph.num_edges() { - return SolutionSize::Invalid; + return Min(None); } let selected: Vec = config.iter().map(|&s| s == 1).collect(); if !is_steiner_tree(&self.graph, &self.terminals, &selected) { - return SolutionSize::Invalid; + return Min(None); } let mut total = W::Sum::zero(); for (idx, &sel) in config.iter().enumerate() { @@ -201,19 +201,19 @@ where } } } - SolutionSize::Valid(total) + Min(Some(total)) } } -impl OptimizationProblem for SteinerTreeInGraphs +impl ObjectiveProblem for SteinerTreeInGraphs where G: Graph + crate::variant::VariantParam, W: WeightElement + crate::variant::VariantParam, { type Objective = W::Sum; - fn direction(&self) -> Direction { - Direction::Minimize + fn direction(&self) -> ExtremumSense { + ExtremumSense::Minimize } } @@ -304,7 +304,7 @@ pub(crate) fn canonical_model_example_specs() -> Vec SatisfactionProblem for StrongConnectivityAugmentation where +impl WitnessProblem for StrongConnectivityAugmentation where W: WeightElement + crate::variant::VariantParam { } diff --git a/src/models/graph/subgraph_isomorphism.rs b/src/models/graph/subgraph_isomorphism.rs index 4e3ba2731..aeee3449d 100644 --- a/src/models/graph/subgraph_isomorphism.rs +++ b/src/models/graph/subgraph_isomorphism.rs @@ -7,7 +7,7 @@ use crate::registry::{FieldInfo, ProblemSchemaEntry}; use crate::topology::{Graph, SimpleGraph}; -use crate::traits::{Problem, SatisfactionProblem}; +use crate::traits::{Problem, WitnessProblem}; use serde::{Deserialize, Serialize}; inventory::submit! { @@ -58,7 +58,7 @@ inventory::submit! { /// assert!(problem.evaluate(&[0, 1, 2])); /// /// let solver = BruteForce::new(); -/// let solution = solver.find_satisfying(&problem); +/// let solution = solver.find_witness(&problem); /// assert!(solution.is_some()); /// ``` #[derive(Debug, Clone, Serialize, Deserialize)] @@ -177,7 +177,7 @@ impl Problem for SubgraphIsomorphism { } } -impl SatisfactionProblem for SubgraphIsomorphism {} +impl WitnessProblem for SubgraphIsomorphism {} crate::declare_variants! { default sat SubgraphIsomorphism => "num_host_vertices ^ num_pattern_vertices", diff --git a/src/models/graph/traveling_salesman.rs b/src/models/graph/traveling_salesman.rs index 21bea1c0a..5d7daf019 100644 --- a/src/models/graph/traveling_salesman.rs +++ b/src/models/graph/traveling_salesman.rs @@ -5,8 +5,8 @@ use crate::registry::{FieldInfo, ProblemSchemaEntry, VariantDimension}; use crate::topology::{Graph, SimpleGraph}; -use crate::traits::{OptimizationProblem, Problem}; -use crate::types::{Direction, SolutionSize, WeightElement}; +use crate::traits::{ObjectiveProblem, Problem}; +use crate::types::{ExtremumSense, Min, WeightElement}; use num_traits::Zero; use serde::{Deserialize, Serialize}; @@ -150,7 +150,7 @@ where W: WeightElement + crate::variant::VariantParam, { const NAME: &'static str = "TravelingSalesman"; - type Value = SolutionSize; + type Value = Min; fn variant() -> Vec<(&'static str, &'static str)> { crate::variant_params![G, W] @@ -160,9 +160,9 @@ where vec![2; self.graph.num_edges()] } - fn evaluate(&self, config: &[usize]) -> SolutionSize { + fn evaluate(&self, config: &[usize]) -> Min { if !self.is_valid_hamiltonian_cycle(config) { - return SolutionSize::Invalid; + return Min(None); } let mut total = W::Sum::zero(); for (idx, &selected) in config.iter().enumerate() { @@ -172,19 +172,19 @@ where } } } - SolutionSize::Valid(total) + Min(Some(total)) } } -impl OptimizationProblem for TravelingSalesman +impl ObjectiveProblem for TravelingSalesman where G: Graph + crate::variant::VariantParam, W: WeightElement + crate::variant::VariantParam, { type Objective = W::Sum; - fn direction(&self) -> Direction { - Direction::Minimize + fn direction(&self) -> ExtremumSense { + ExtremumSense::Minimize } } @@ -267,7 +267,7 @@ pub(crate) fn canonical_model_example_specs() -> Vec "2^num_edges", diff --git a/src/models/graph/undirected_two_commodity_integral_flow.rs b/src/models/graph/undirected_two_commodity_integral_flow.rs index 7aa631089..004b239ed 100644 --- a/src/models/graph/undirected_two_commodity_integral_flow.rs +++ b/src/models/graph/undirected_two_commodity_integral_flow.rs @@ -5,7 +5,7 @@ use crate::registry::{FieldInfo, ProblemSchemaEntry, ProblemSizeFieldEntry}; use crate::topology::{Graph, SimpleGraph}; -use crate::traits::{Problem, SatisfactionProblem}; +use crate::traits::{Problem, WitnessProblem}; use serde::{Deserialize, Serialize}; inventory::submit! { @@ -290,7 +290,7 @@ impl Problem for UndirectedTwoCommodityIntegralFlow { } } -impl SatisfactionProblem for UndirectedTwoCommodityIntegralFlow {} +impl WitnessProblem for UndirectedTwoCommodityIntegralFlow {} crate::declare_variants! { default sat UndirectedTwoCommodityIntegralFlow => "5^num_edges", diff --git a/src/models/misc/additional_key.rs b/src/models/misc/additional_key.rs index 2437cadb1..8f79257f9 100644 --- a/src/models/misc/additional_key.rs +++ b/src/models/misc/additional_key.rs @@ -8,7 +8,7 @@ //! The problem is NP-complete (Garey & Johnson, SR7). use crate::registry::{FieldInfo, ProblemSchemaEntry}; -use crate::traits::{Problem, SatisfactionProblem}; +use crate::traits::{Problem, WitnessProblem}; use serde::{Deserialize, Serialize}; inventory::submit! { @@ -56,7 +56,7 @@ inventory::submit! { /// vec![], /// ); /// let solver = BruteForce::new(); -/// let solution = solver.find_satisfying(&problem); +/// let solution = solver.find_witness(&problem); /// assert!(solution.is_some()); /// ``` #[derive(Debug, Clone, Serialize, Deserialize)] @@ -254,7 +254,7 @@ impl Problem for AdditionalKey { } } -impl SatisfactionProblem for AdditionalKey {} +impl WitnessProblem for AdditionalKey {} crate::declare_variants! { default sat AdditionalKey => "2^num_relation_attrs * num_dependencies * num_attributes", diff --git a/src/models/misc/bin_packing.rs b/src/models/misc/bin_packing.rs index 419575b50..cc5f93b4e 100644 --- a/src/models/misc/bin_packing.rs +++ b/src/models/misc/bin_packing.rs @@ -4,8 +4,8 @@ //! that minimizes the number of bins used while respecting capacity constraints. use crate::registry::{FieldInfo, ProblemSchemaEntry, VariantDimension}; -use crate::traits::{OptimizationProblem, Problem}; -use crate::types::{Direction, SolutionSize, WeightElement}; +use crate::traits::{ObjectiveProblem, Problem}; +use crate::types::{ExtremumSense, Min, WeightElement}; use serde::{Deserialize, Serialize}; inventory::submit! { @@ -48,7 +48,7 @@ inventory::submit! { /// // 4 items with sizes [3, 3, 2, 2], capacity 5 /// let problem = BinPacking::new(vec![3, 3, 2, 2], 5); /// let solver = BruteForce::new(); -/// let solution = solver.find_best(&problem); +/// let solution = solver.find_witness(&problem); /// assert!(solution.is_some()); /// ``` #[derive(Debug, Clone, Serialize, Deserialize)] @@ -87,7 +87,7 @@ where W::Sum: PartialOrd, { const NAME: &'static str = "BinPacking"; - type Value = SolutionSize; + type Value = Min; fn variant() -> Vec<(&'static str, &'static str)> { crate::variant_params![W] @@ -98,24 +98,24 @@ where vec![n; n] } - fn evaluate(&self, config: &[usize]) -> SolutionSize { + fn evaluate(&self, config: &[usize]) -> Min { if !is_valid_packing(&self.sizes, &self.capacity, config) { - return SolutionSize::Invalid; + return Min(None); } let num_bins = count_bins(config); - SolutionSize::Valid(num_bins as i32) + Min(Some(num_bins as i32)) } } -impl OptimizationProblem for BinPacking +impl ObjectiveProblem for BinPacking where W: WeightElement + crate::variant::VariantParam, W::Sum: PartialOrd, { type Objective = i32; - fn direction(&self) -> Direction { - Direction::Minimize + fn direction(&self) -> ExtremumSense { + ExtremumSense::Minimize } } @@ -165,7 +165,7 @@ pub(crate) fn canonical_model_example_specs() -> Vec::new(vec![3, 3, 4], 7)), optimal_config: vec![0, 1, 0], - optimal_value: serde_json::json!({"Valid": 2}), + optimal_value: serde_json::json!(2), }] } diff --git a/src/models/misc/boyce_codd_normal_form_violation.rs b/src/models/misc/boyce_codd_normal_form_violation.rs index f3d7a21b1..1e6594abd 100644 --- a/src/models/misc/boyce_codd_normal_form_violation.rs +++ b/src/models/misc/boyce_codd_normal_form_violation.rs @@ -6,7 +6,7 @@ //! some but not all attributes of `A' \ X` — i.e., a witness to a BCNF violation. use crate::registry::{FieldInfo, ProblemSchemaEntry}; -use crate::traits::{Problem, SatisfactionProblem}; +use crate::traits::{Problem, WitnessProblem}; use serde::{Deserialize, Serialize}; use std::collections::HashSet; @@ -213,7 +213,7 @@ impl Problem for BoyceCoddNormalFormViolation { } } -impl SatisfactionProblem for BoyceCoddNormalFormViolation {} +impl WitnessProblem for BoyceCoddNormalFormViolation {} crate::declare_variants! { default sat BoyceCoddNormalFormViolation => "2^num_target_attributes * num_target_attributes^2 * num_functional_deps", diff --git a/src/models/misc/capacity_assignment.rs b/src/models/misc/capacity_assignment.rs index 58a34a558..bbeb2ee5d 100644 --- a/src/models/misc/capacity_assignment.rs +++ b/src/models/misc/capacity_assignment.rs @@ -5,7 +5,7 @@ //! their respective budgets. use crate::registry::{FieldInfo, ProblemSchemaEntry}; -use crate::traits::{Problem, SatisfactionProblem}; +use crate::traits::{Problem, WitnessProblem}; use serde::{Deserialize, Serialize}; inventory::submit! { @@ -173,7 +173,7 @@ impl Problem for CapacityAssignment { } } -impl SatisfactionProblem for CapacityAssignment {} +impl WitnessProblem for CapacityAssignment {} crate::declare_variants! { default sat CapacityAssignment => "num_capacities ^ num_links", diff --git a/src/models/misc/conjunctive_boolean_query.rs b/src/models/misc/conjunctive_boolean_query.rs index 8154c92d5..059f89c5a 100644 --- a/src/models/misc/conjunctive_boolean_query.rs +++ b/src/models/misc/conjunctive_boolean_query.rs @@ -11,7 +11,7 @@ //! variables such that every conjunct's resolved tuple belongs to its relation. use crate::registry::{FieldInfo, ProblemSchemaEntry}; -use crate::traits::{Problem, SatisfactionProblem}; +use crate::traits::{Problem, WitnessProblem}; use serde::{Deserialize, Serialize}; inventory::submit! { @@ -76,7 +76,7 @@ pub enum QueryArg { /// ]; /// let problem = ConjunctiveBooleanQuery::new(6, relations, 1, conjuncts); /// let solver = BruteForce::new(); -/// let solution = solver.find_satisfying(&problem); +/// let solution = solver.find_witness(&problem); /// assert!(solution.is_some()); /// ``` #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] @@ -221,7 +221,7 @@ impl Problem for ConjunctiveBooleanQuery { } } -impl SatisfactionProblem for ConjunctiveBooleanQuery {} +impl WitnessProblem for ConjunctiveBooleanQuery {} crate::declare_variants! { default sat ConjunctiveBooleanQuery => "domain_size ^ num_variables", diff --git a/src/models/misc/conjunctive_query_foldability.rs b/src/models/misc/conjunctive_query_foldability.rs index 493f41188..604312da5 100644 --- a/src/models/misc/conjunctive_query_foldability.rs +++ b/src/models/misc/conjunctive_query_foldability.rs @@ -5,7 +5,7 @@ //! that transforms Q1 into Q2. NP-complete (Chandra & Merlin, 1977). use crate::registry::{FieldInfo, ProblemSchemaEntry}; -use crate::traits::{Problem, SatisfactionProblem}; +use crate::traits::{Problem, WitnessProblem}; use serde::{Deserialize, Serialize}; use std::collections::HashSet; @@ -81,7 +81,7 @@ pub enum Term { /// ], /// ); /// let solver = BruteForce::new(); -/// let solution = solver.find_satisfying(&problem); +/// let solution = solver.find_witness(&problem); /// assert!(solution.is_some()); /// ``` #[derive(Debug, Clone, Serialize, Deserialize)] @@ -313,7 +313,7 @@ impl Problem for ConjunctiveQueryFoldability { } } -impl SatisfactionProblem for ConjunctiveQueryFoldability {} +impl WitnessProblem for ConjunctiveQueryFoldability {} crate::declare_variants! { default sat ConjunctiveQueryFoldability => "(num_distinguished + num_undistinguished + domain_size)^num_undistinguished * num_conjuncts_q1", diff --git a/src/models/misc/consistency_of_database_frequency_tables.rs b/src/models/misc/consistency_of_database_frequency_tables.rs index 98aba0fd0..dc160aea3 100644 --- a/src/models/misc/consistency_of_database_frequency_tables.rs +++ b/src/models/misc/consistency_of_database_frequency_tables.rs @@ -7,7 +7,7 @@ //! frequency table and every known value. use crate::registry::{FieldInfo, ProblemSchemaEntry}; -use crate::traits::{Problem, SatisfactionProblem}; +use crate::traits::{Problem, WitnessProblem}; use serde::{Deserialize, Serialize}; use std::collections::BTreeSet; @@ -333,7 +333,7 @@ impl Problem for ConsistencyOfDatabaseFrequencyTables { } } -impl SatisfactionProblem for ConsistencyOfDatabaseFrequencyTables {} +impl WitnessProblem for ConsistencyOfDatabaseFrequencyTables {} crate::declare_variants! { default sat ConsistencyOfDatabaseFrequencyTables => "domain_size_product^num_objects", diff --git a/src/models/misc/ensemble_computation.rs b/src/models/misc/ensemble_computation.rs index fa937a514..cdf57eb80 100644 --- a/src/models/misc/ensemble_computation.rs +++ b/src/models/misc/ensemble_computation.rs @@ -1,7 +1,7 @@ //! Ensemble Computation problem implementation. use crate::registry::{FieldInfo, ProblemSchemaEntry}; -use crate::traits::{Problem, SatisfactionProblem}; +use crate::traits::{Problem, WitnessProblem}; use serde::{Deserialize, Serialize}; inventory::submit! { @@ -194,7 +194,7 @@ impl Problem for EnsembleComputation { } } -impl SatisfactionProblem for EnsembleComputation {} +impl WitnessProblem for EnsembleComputation {} crate::declare_variants! { default sat EnsembleComputation => "(universe_size + budget)^(2 * budget)", diff --git a/src/models/misc/expected_retrieval_cost.rs b/src/models/misc/expected_retrieval_cost.rs index b08e3b461..17b35dabb 100644 --- a/src/models/misc/expected_retrieval_cost.rs +++ b/src/models/misc/expected_retrieval_cost.rs @@ -5,7 +5,7 @@ //! prescribed bound. use crate::registry::{FieldInfo, ProblemSchemaEntry, ProblemSizeFieldEntry}; -use crate::traits::{Problem, SatisfactionProblem}; +use crate::traits::{Problem, WitnessProblem}; use serde::{Deserialize, Serialize}; const FLOAT_TOLERANCE: f64 = 1e-9; @@ -141,7 +141,7 @@ impl Problem for ExpectedRetrievalCost { } } -impl SatisfactionProblem for ExpectedRetrievalCost {} +impl WitnessProblem for ExpectedRetrievalCost {} fn latency_distance(num_sectors: usize, source: usize, target: usize) -> usize { if source < target { diff --git a/src/models/misc/factoring.rs b/src/models/misc/factoring.rs index 526b87892..fef48b531 100644 --- a/src/models/misc/factoring.rs +++ b/src/models/misc/factoring.rs @@ -4,8 +4,8 @@ //! Given a number N, find two factors (a, b) such that a * b = N. use crate::registry::{FieldInfo, ProblemSchemaEntry}; -use crate::traits::{OptimizationProblem, Problem}; -use crate::types::{Direction, SolutionSize}; +use crate::traits::{ObjectiveProblem, Problem}; +use crate::types::{ExtremumSense, Min}; use serde::{Deserialize, Serialize}; inventory::submit! { @@ -39,7 +39,7 @@ inventory::submit! { /// let problem = Factoring::new(2, 2, 6); /// /// let solver = BruteForce::new(); -/// let solutions = solver.find_all_best(&problem); +/// let solutions = solver.find_all_witnesses(&problem); /// /// // Should find: 2*3=6 or 3*2=6 /// for sol in &solutions { @@ -134,13 +134,13 @@ pub(crate) fn is_factoring(target: u64, a: u64, b: u64) -> bool { impl Problem for Factoring { const NAME: &'static str = "Factoring"; - type Value = SolutionSize; + type Value = Min; fn dims(&self) -> Vec { vec![2; self.m + self.n] } - fn evaluate(&self, config: &[usize]) -> SolutionSize { + fn evaluate(&self, config: &[usize]) -> Min { let (a, b) = self.read_factors(config); let product = a * b; // Distance from target (0 means exact match) @@ -149,7 +149,7 @@ impl Problem for Factoring { } else { (self.target - product) as i32 }; - SolutionSize::Valid(distance) + Min(Some(distance)) } fn variant() -> Vec<(&'static str, &'static str)> { @@ -157,11 +157,11 @@ impl Problem for Factoring { } } -impl OptimizationProblem for Factoring { +impl ObjectiveProblem for Factoring { type Objective = i32; - fn direction(&self) -> Direction { - Direction::Minimize + fn direction(&self) -> ExtremumSense { + ExtremumSense::Minimize } } @@ -175,7 +175,7 @@ pub(crate) fn canonical_model_example_specs() -> Vec "factorial(num_jobs)", diff --git a/src/models/misc/knapsack.rs b/src/models/misc/knapsack.rs index 114df707a..e6c1a7374 100644 --- a/src/models/misc/knapsack.rs +++ b/src/models/misc/knapsack.rs @@ -4,8 +4,8 @@ //! total value while respecting a weight capacity constraint. use crate::registry::{FieldInfo, ProblemSchemaEntry}; -use crate::traits::{OptimizationProblem, Problem}; -use crate::types::{Direction, SolutionSize}; +use crate::traits::{ObjectiveProblem, Problem}; +use crate::types::{ExtremumSense, Max}; use serde::{Deserialize, Serialize}; inventory::submit! { @@ -43,7 +43,7 @@ inventory::submit! { /// /// let problem = Knapsack::new(vec![2, 3, 4, 5], vec![3, 4, 5, 7], 7); /// let solver = BruteForce::new(); -/// let solution = solver.find_best(&problem); +/// let solution = solver.find_witness(&problem); /// assert!(solution.is_some()); /// ``` #[derive(Debug, Clone, Serialize, Deserialize)] @@ -119,7 +119,7 @@ impl Knapsack { impl Problem for Knapsack { const NAME: &'static str = "Knapsack"; - type Value = SolutionSize; + type Value = Max; fn variant() -> Vec<(&'static str, &'static str)> { crate::variant_params![] @@ -129,12 +129,12 @@ impl Problem for Knapsack { vec![2; self.num_items()] } - fn evaluate(&self, config: &[usize]) -> SolutionSize { + fn evaluate(&self, config: &[usize]) -> Max { if config.len() != self.num_items() { - return SolutionSize::Invalid; + return Max(None); } if config.iter().any(|&v| v >= 2) { - return SolutionSize::Invalid; + return Max(None); } let total_weight: i64 = config .iter() @@ -143,7 +143,7 @@ impl Problem for Knapsack { .map(|(i, _)| self.weights[i]) .sum(); if total_weight > self.capacity { - return SolutionSize::Invalid; + return Max(None); } let total_value: i64 = config .iter() @@ -151,15 +151,15 @@ impl Problem for Knapsack { .filter(|(_, &x)| x == 1) .map(|(i, _)| self.values[i]) .sum(); - SolutionSize::Valid(total_value) + Max(Some(total_value)) } } -impl OptimizationProblem for Knapsack { +impl ObjectiveProblem for Knapsack { type Objective = i64; - fn direction(&self) -> Direction { - Direction::Maximize + fn direction(&self) -> ExtremumSense { + ExtremumSense::Maximize } } @@ -211,7 +211,7 @@ pub(crate) fn canonical_model_example_specs() -> Vec "alphabet_size ^ bound", diff --git a/src/models/misc/minimum_tardiness_sequencing.rs b/src/models/misc/minimum_tardiness_sequencing.rs index 7cc5e3086..0f7a734f2 100644 --- a/src/models/misc/minimum_tardiness_sequencing.rs +++ b/src/models/misc/minimum_tardiness_sequencing.rs @@ -6,8 +6,8 @@ //! Corresponds to scheduling notation `1|prec, pj=1|sum Uj`. use crate::registry::{FieldInfo, ProblemSchemaEntry}; -use crate::traits::{OptimizationProblem, Problem}; -use crate::types::{Direction, SolutionSize}; +use crate::traits::{ObjectiveProblem, Problem}; +use crate::types::{ExtremumSense, Min}; use serde::{Deserialize, Serialize}; inventory::submit! { @@ -52,7 +52,7 @@ inventory::submit! { /// vec![(0, 2)], // task 0 must precede task 2 /// ); /// let solver = BruteForce::new(); -/// let solution = solver.find_best(&problem); +/// let solution = solver.find_witness(&problem); /// assert!(solution.is_some()); /// ``` #[derive(Debug, Clone, Serialize, Deserialize)] @@ -125,7 +125,7 @@ impl MinimumTardinessSequencing { impl Problem for MinimumTardinessSequencing { const NAME: &'static str = "MinimumTardinessSequencing"; - type Value = SolutionSize; + type Value = Min; fn variant() -> Vec<(&'static str, &'static str)> { crate::variant_params![] @@ -136,10 +136,10 @@ impl Problem for MinimumTardinessSequencing { (0..n).rev().map(|i| i + 1).collect() } - fn evaluate(&self, config: &[usize]) -> SolutionSize { + fn evaluate(&self, config: &[usize]) -> Min { let n = self.num_tasks; if config.len() != n { - return SolutionSize::Invalid; + return Min(None); } // Decode Lehmer code into a permutation. @@ -148,7 +148,7 @@ impl Problem for MinimumTardinessSequencing { let mut schedule = Vec::with_capacity(n); for &c in config.iter() { if c >= available.len() { - return SolutionSize::Invalid; + return Min(None); } schedule.push(available.remove(c)); } @@ -163,7 +163,7 @@ impl Problem for MinimumTardinessSequencing { // Check precedence constraints: for each (pred, succ), sigma(pred) < sigma(succ) for &(pred, succ) in &self.precedences { if sigma[pred] >= sigma[succ] { - return SolutionSize::Invalid; + return Min(None); } } @@ -174,15 +174,15 @@ impl Problem for MinimumTardinessSequencing { .filter(|&(t, &pos)| pos + 1 > self.deadlines[t]) .count(); - SolutionSize::Valid(tardy_count) + Min(Some(tardy_count)) } } -impl OptimizationProblem for MinimumTardinessSequencing { +impl ObjectiveProblem for MinimumTardinessSequencing { type Objective = usize; - fn direction(&self) -> Direction { - Direction::Minimize + fn direction(&self) -> ExtremumSense { + ExtremumSense::Minimize } } @@ -203,7 +203,7 @@ pub(crate) fn canonical_model_example_specs() -> Vec "2^num_tasks", diff --git a/src/models/misc/paintshop.rs b/src/models/misc/paintshop.rs index 2c06fc79a..e576e4de3 100644 --- a/src/models/misc/paintshop.rs +++ b/src/models/misc/paintshop.rs @@ -6,8 +6,8 @@ //! The goal is to minimize color switches between adjacent positions. use crate::registry::{FieldInfo, ProblemSchemaEntry}; -use crate::traits::{OptimizationProblem, Problem}; -use crate::types::{Direction, SolutionSize}; +use crate::traits::{ObjectiveProblem, Problem}; +use crate::types::{ExtremumSense, Min}; use serde::{Deserialize, Serialize}; use std::collections::{HashMap, HashSet}; @@ -40,7 +40,7 @@ inventory::submit! { /// let problem = PaintShop::new(vec!["a", "b", "a", "c", "c", "b"]); /// /// let solver = BruteForce::new(); -/// let solutions = solver.find_all_best(&problem); +/// let solutions = solver.find_all_witnesses(&problem); /// /// // The minimum number of color switches /// for sol in &solutions { @@ -168,15 +168,15 @@ pub(crate) fn count_paint_switches(coloring: &[usize]) -> usize { impl Problem for PaintShop { const NAME: &'static str = "PaintShop"; - type Value = SolutionSize; + type Value = Min; fn dims(&self) -> Vec { vec![2; self.num_cars] } - fn evaluate(&self, config: &[usize]) -> SolutionSize { + fn evaluate(&self, config: &[usize]) -> Min { // All configurations are valid (no hard constraints). - SolutionSize::Valid(self.count_switches(config) as i32) + Min(Some(self.count_switches(config) as i32)) } fn variant() -> Vec<(&'static str, &'static str)> { @@ -184,11 +184,11 @@ impl Problem for PaintShop { } } -impl OptimizationProblem for PaintShop { +impl ObjectiveProblem for PaintShop { type Objective = i32; - fn direction(&self) -> Direction { - Direction::Minimize + fn direction(&self) -> ExtremumSense { + ExtremumSense::Minimize } } @@ -202,7 +202,7 @@ pub(crate) fn canonical_model_example_specs() -> Vec; + type Value = Max; fn variant() -> Vec<(&'static str, &'static str)> { crate::variant_params![] @@ -233,16 +233,16 @@ impl Problem for PartiallyOrderedKnapsack { vec![2; self.num_items()] } - fn evaluate(&self, config: &[usize]) -> SolutionSize { + fn evaluate(&self, config: &[usize]) -> Max { if config.len() != self.num_items() { - return SolutionSize::Invalid; + return Max(None); } if config.iter().any(|&v| v >= 2) { - return SolutionSize::Invalid; + return Max(None); } // Check downward-closure (precedence constraints) if !self.is_downward_closed(config) { - return SolutionSize::Invalid; + return Max(None); } // Check capacity constraint let total_weight: i64 = config @@ -252,7 +252,7 @@ impl Problem for PartiallyOrderedKnapsack { .map(|(i, _)| self.weights[i]) .sum(); if total_weight > self.capacity { - return SolutionSize::Invalid; + return Max(None); } // Compute total value let total_value: i64 = config @@ -261,15 +261,15 @@ impl Problem for PartiallyOrderedKnapsack { .filter(|(_, &x)| x == 1) .map(|(i, _)| self.values[i]) .sum(); - SolutionSize::Valid(total_value) + Max(Some(total_value)) } } -impl OptimizationProblem for PartiallyOrderedKnapsack { +impl ObjectiveProblem for PartiallyOrderedKnapsack { type Objective = i64; - fn direction(&self) -> Direction { - Direction::Maximize + fn direction(&self) -> ExtremumSense { + ExtremumSense::Maximize } } @@ -288,7 +288,7 @@ pub(crate) fn canonical_model_example_specs() -> Vec "2^(num_elements / 2)", diff --git a/src/models/misc/precedence_constrained_scheduling.rs b/src/models/misc/precedence_constrained_scheduling.rs index ce9f9180a..a8db36703 100644 --- a/src/models/misc/precedence_constrained_scheduling.rs +++ b/src/models/misc/precedence_constrained_scheduling.rs @@ -5,7 +5,7 @@ //! respecting precedences. NP-complete via reduction from 3SAT (Ullman, 1975). use crate::registry::{FieldInfo, ProblemSchemaEntry}; -use crate::traits::{Problem, SatisfactionProblem}; +use crate::traits::{Problem, WitnessProblem}; use serde::{Deserialize, Serialize}; inventory::submit! { @@ -47,7 +47,7 @@ inventory::submit! { /// // 4 tasks, 2 processors, deadline 3, with t0 < t2 and t1 < t3 /// let problem = PrecedenceConstrainedScheduling::new(4, 2, 3, vec![(0, 2), (1, 3)]); /// let solver = BruteForce::new(); -/// let solution = solver.find_satisfying(&problem); +/// let solution = solver.find_witness(&problem); /// assert!(solution.is_some()); /// ``` #[derive(Debug, Clone, Serialize, Deserialize)] @@ -154,7 +154,7 @@ impl Problem for PrecedenceConstrainedScheduling { } } -impl SatisfactionProblem for PrecedenceConstrainedScheduling {} +impl WitnessProblem for PrecedenceConstrainedScheduling {} crate::declare_variants! { default sat PrecedenceConstrainedScheduling => "2^num_tasks", diff --git a/src/models/misc/rectilinear_picture_compression.rs b/src/models/misc/rectilinear_picture_compression.rs index c023a9bbf..6d202a93a 100644 --- a/src/models/misc/rectilinear_picture_compression.rs +++ b/src/models/misc/rectilinear_picture_compression.rs @@ -7,7 +7,7 @@ //! and every covered entry must be 1. use crate::registry::{FieldInfo, ProblemSchemaEntry}; -use crate::traits::{Problem, SatisfactionProblem}; +use crate::traits::{Problem, WitnessProblem}; use serde::de::Deserializer; use serde::{Deserialize, Serialize}; @@ -55,7 +55,7 @@ inventory::submit! { /// ]; /// let problem = RectilinearPictureCompression::new(matrix, 2); /// let solver = BruteForce::new(); -/// let solution = solver.find_satisfying(&problem); +/// let solution = solver.find_witness(&problem); /// assert!(solution.is_some()); /// ``` #[derive(Debug, Clone, Serialize)] @@ -286,7 +286,7 @@ impl Problem for RectilinearPictureCompression { } } -impl SatisfactionProblem for RectilinearPictureCompression {} +impl WitnessProblem for RectilinearPictureCompression {} crate::declare_variants! { default sat RectilinearPictureCompression => "2^(num_rows * num_cols)", diff --git a/src/models/misc/resource_constrained_scheduling.rs b/src/models/misc/resource_constrained_scheduling.rs index 26e42ec71..1c8608cac 100644 --- a/src/models/misc/resource_constrained_scheduling.rs +++ b/src/models/misc/resource_constrained_scheduling.rs @@ -5,7 +5,7 @@ //! processor capacity limit and resource usage constraints per time slot. use crate::registry::{FieldInfo, ProblemSchemaEntry}; -use crate::traits::{Problem, SatisfactionProblem}; +use crate::traits::{Problem, WitnessProblem}; use serde::{Deserialize, Serialize}; inventory::submit! { @@ -53,7 +53,7 @@ inventory::submit! { /// 2, /// ); /// let solver = BruteForce::new(); -/// let solution = solver.find_satisfying(&problem); +/// let solution = solver.find_witness(&problem); /// assert!(solution.is_some()); /// ``` #[derive(Debug, Clone, Serialize, Deserialize)] @@ -194,7 +194,7 @@ impl Problem for ResourceConstrainedScheduling { } } -impl SatisfactionProblem for ResourceConstrainedScheduling {} +impl WitnessProblem for ResourceConstrainedScheduling {} crate::declare_variants! { default sat ResourceConstrainedScheduling => "deadline ^ num_tasks", diff --git a/src/models/misc/scheduling_with_individual_deadlines.rs b/src/models/misc/scheduling_with_individual_deadlines.rs index 8e283ecb1..2f39e48e1 100644 --- a/src/models/misc/scheduling_with_individual_deadlines.rs +++ b/src/models/misc/scheduling_with_individual_deadlines.rs @@ -5,7 +5,7 @@ //! every task finishes by its own deadline. use crate::registry::{FieldInfo, ProblemSchemaEntry}; -use crate::traits::{Problem, SatisfactionProblem}; +use crate::traits::{Problem, WitnessProblem}; use serde::{Deserialize, Serialize}; use std::collections::BTreeMap; @@ -142,7 +142,7 @@ impl Problem for SchedulingWithIndividualDeadlines { } } -impl SatisfactionProblem for SchedulingWithIndividualDeadlines {} +impl WitnessProblem for SchedulingWithIndividualDeadlines {} crate::declare_variants! { default sat SchedulingWithIndividualDeadlines => "max_deadline^num_tasks", 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 e4c61335c..0fd0294e0 100644 --- a/src/models/misc/sequencing_to_minimize_maximum_cumulative_cost.rs +++ b/src/models/misc/sequencing_to_minimize_maximum_cumulative_cost.rs @@ -5,7 +5,7 @@ //! cost never exceeds a given bound. use crate::registry::{FieldInfo, ProblemSchemaEntry}; -use crate::traits::{Problem, SatisfactionProblem}; +use crate::traits::{Problem, WitnessProblem}; use serde::de::Error as _; use serde::{Deserialize, Serialize}; @@ -190,7 +190,7 @@ impl Problem for SequencingToMinimizeMaximumCumulativeCost { } } -impl SatisfactionProblem for SequencingToMinimizeMaximumCumulativeCost {} +impl WitnessProblem for SequencingToMinimizeMaximumCumulativeCost {} crate::declare_variants! { default sat SequencingToMinimizeMaximumCumulativeCost => "factorial(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 6e72740bf..12c76c514 100644 --- a/src/models/misc/sequencing_to_minimize_weighted_completion_time.rs +++ b/src/models/misc/sequencing_to_minimize_weighted_completion_time.rs @@ -6,8 +6,8 @@ //! weighted completion time. use crate::registry::{FieldInfo, ProblemSchemaEntry}; -use crate::traits::{OptimizationProblem, Problem}; -use crate::types::{Direction, SolutionSize}; +use crate::traits::{ObjectiveProblem, Problem}; +use crate::types::{ExtremumSense, Min}; use serde::{Deserialize, Serialize}; inventory::submit! { @@ -146,7 +146,7 @@ impl SequencingToMinimizeWeightedCompletionTime { Some(schedule) } - fn weighted_completion_time(&self, schedule: &[usize]) -> SolutionSize { + fn weighted_completion_time(&self, schedule: &[usize]) -> Min { let n = self.num_tasks(); let mut positions = vec![0usize; n]; let mut completion_times = vec![0u64; n]; @@ -162,7 +162,7 @@ impl SequencingToMinimizeWeightedCompletionTime { for &(pred, succ) in &self.precedences { if positions[pred] >= positions[succ] { - return SolutionSize::Invalid; + return Min(None); } } @@ -174,7 +174,7 @@ impl SequencingToMinimizeWeightedCompletionTime { acc.checked_add(weighted_completion) }) .expect("weighted completion time overflowed u64"); - SolutionSize::Valid(total) + Min(Some(total)) } } @@ -207,7 +207,7 @@ impl<'de> Deserialize<'de> for SequencingToMinimizeWeightedCompletionTime { impl Problem for SequencingToMinimizeWeightedCompletionTime { const NAME: &'static str = "SequencingToMinimizeWeightedCompletionTime"; - type Value = SolutionSize; + type Value = Min; fn variant() -> Vec<(&'static str, &'static str)> { crate::variant_params![] @@ -218,19 +218,19 @@ impl Problem for SequencingToMinimizeWeightedCompletionTime { (0..n).rev().map(|i| i + 1).collect() } - fn evaluate(&self, config: &[usize]) -> SolutionSize { + fn evaluate(&self, config: &[usize]) -> Min { let Some(schedule) = self.decode_schedule(config) else { - return SolutionSize::Invalid; + return Min(None); }; self.weighted_completion_time(&schedule) } } -impl OptimizationProblem for SequencingToMinimizeWeightedCompletionTime { +impl ObjectiveProblem for SequencingToMinimizeWeightedCompletionTime { type Objective = u64; - fn direction(&self) -> Direction { - Direction::Minimize + fn direction(&self) -> ExtremumSense { + ExtremumSense::Minimize } } @@ -248,7 +248,7 @@ pub(crate) fn canonical_model_example_specs() -> Vec "factorial(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 90d020c05..27d243291 100644 --- a/src/models/misc/sequencing_with_release_times_and_deadlines.rs +++ b/src/models/misc/sequencing_with_release_times_and_deadlines.rs @@ -6,7 +6,7 @@ //! Strongly NP-complete (Garey & Johnson, A5 SS1). use crate::registry::{FieldInfo, ProblemSchemaEntry}; -use crate::traits::{Problem, SatisfactionProblem}; +use crate::traits::{Problem, WitnessProblem}; use serde::{Deserialize, Serialize}; inventory::submit! { @@ -52,7 +52,7 @@ inventory::submit! { /// vec![3, 3, 4], /// ); /// let solver = BruteForce::new(); -/// let solution = solver.find_satisfying(&problem); +/// let solution = solver.find_witness(&problem); /// assert!(solution.is_some()); /// ``` #[derive(Debug, Clone, Serialize, Deserialize)] @@ -148,7 +148,7 @@ impl Problem for SequencingWithReleaseTimesAndDeadlines { } } -impl SatisfactionProblem for SequencingWithReleaseTimesAndDeadlines {} +impl WitnessProblem for SequencingWithReleaseTimesAndDeadlines {} crate::declare_variants! { default sat SequencingWithReleaseTimesAndDeadlines => "2^num_tasks * num_tasks", diff --git a/src/models/misc/sequencing_within_intervals.rs b/src/models/misc/sequencing_within_intervals.rs index d4ae4df18..6306b910a 100644 --- a/src/models/misc/sequencing_within_intervals.rs +++ b/src/models/misc/sequencing_within_intervals.rs @@ -5,7 +5,7 @@ //! task runs entirely within its allowed time window. use crate::registry::{FieldInfo, ProblemSchemaEntry}; -use crate::traits::{Problem, SatisfactionProblem}; +use crate::traits::{Problem, WitnessProblem}; use serde::{Deserialize, Serialize}; inventory::submit! { @@ -50,7 +50,7 @@ inventory::submit! { /// // 3 tasks: release_times = [0, 2, 4], deadlines = [3, 5, 7], lengths = [2, 2, 2] /// let problem = SequencingWithinIntervals::new(vec![0, 2, 4], vec![3, 5, 7], vec![2, 2, 2]); /// let solver = BruteForce::new(); -/// let solution = solver.find_satisfying(&problem); +/// let solution = solver.find_witness(&problem); /// assert!(solution.is_some()); /// ``` #[derive(Debug, Clone, Serialize, Deserialize)] @@ -169,7 +169,7 @@ impl Problem for SequencingWithinIntervals { } } -impl SatisfactionProblem for SequencingWithinIntervals {} +impl WitnessProblem for SequencingWithinIntervals {} crate::declare_variants! { default sat SequencingWithinIntervals => "2^num_tasks", diff --git a/src/models/misc/shortest_common_supersequence.rs b/src/models/misc/shortest_common_supersequence.rs index 4a67b33a0..bb8337849 100644 --- a/src/models/misc/shortest_common_supersequence.rs +++ b/src/models/misc/shortest_common_supersequence.rs @@ -11,7 +11,7 @@ //! to the standard `|w| ≤ B` formulation. This problem is NP-hard (Maier, 1978). use crate::registry::{FieldInfo, ProblemSchemaEntry}; -use crate::traits::{Problem, SatisfactionProblem}; +use crate::traits::{Problem, WitnessProblem}; use serde::{Deserialize, Serialize}; inventory::submit! { @@ -52,7 +52,7 @@ inventory::submit! { /// // Alphabet {0, 1}, strings [0,1] and [1,0], bound 3 /// let problem = ShortestCommonSupersequence::new(2, vec![vec![0, 1], vec![1, 0]], 3); /// let solver = BruteForce::new(); -/// let solution = solver.find_satisfying(&problem); +/// let solution = solver.find_witness(&problem); /// assert!(solution.is_some()); /// ``` #[derive(Debug, Clone, Serialize, Deserialize)] @@ -146,7 +146,7 @@ impl Problem for ShortestCommonSupersequence { } } -impl SatisfactionProblem for ShortestCommonSupersequence {} +impl WitnessProblem for ShortestCommonSupersequence {} crate::declare_variants! { default sat ShortestCommonSupersequence => "alphabet_size ^ bound", diff --git a/src/models/misc/stacker_crane.rs b/src/models/misc/stacker_crane.rs index 297a465aa..974067dae 100644 --- a/src/models/misc/stacker_crane.rs +++ b/src/models/misc/stacker_crane.rs @@ -5,7 +5,7 @@ //! traverses every required arc in some order and stays within the bound. use crate::registry::{FieldInfo, ProblemSchemaEntry}; -use crate::traits::{Problem, SatisfactionProblem}; +use crate::traits::{Problem, WitnessProblem}; use serde::{Deserialize, Serialize}; use std::cmp::Reverse; use std::collections::BinaryHeap; @@ -274,7 +274,7 @@ impl Problem for StackerCrane { } } -impl SatisfactionProblem for StackerCrane {} +impl WitnessProblem for StackerCrane {} crate::declare_variants! { default sat StackerCrane => "num_vertices^2 * 2^num_arcs", diff --git a/src/models/misc/staff_scheduling.rs b/src/models/misc/staff_scheduling.rs index 5f392d6dd..1a19e4523 100644 --- a/src/models/misc/staff_scheduling.rs +++ b/src/models/misc/staff_scheduling.rs @@ -5,7 +5,7 @@ //! all requirements are met without exceeding the budget. use crate::registry::{FieldInfo, ProblemSchemaEntry}; -use crate::traits::{Problem, SatisfactionProblem}; +use crate::traits::{Problem, WitnessProblem}; use serde::{Deserialize, Serialize}; inventory::submit! { @@ -170,7 +170,7 @@ impl Problem for StaffScheduling { } } -impl SatisfactionProblem for StaffScheduling {} +impl WitnessProblem for StaffScheduling {} crate::declare_variants! { default sat StaffScheduling => "(num_workers + 1)^num_schedules", diff --git a/src/models/misc/string_to_string_correction.rs b/src/models/misc/string_to_string_correction.rs index d4f9de1a9..03e7fce94 100644 --- a/src/models/misc/string_to_string_correction.rs +++ b/src/models/misc/string_to_string_correction.rs @@ -15,7 +15,7 @@ //! This problem is NP-complete (Wagner, 1975). use crate::registry::{FieldInfo, ProblemSchemaEntry}; -use crate::traits::{Problem, SatisfactionProblem}; +use crate::traits::{Problem, WitnessProblem}; use serde::{Deserialize, Serialize}; inventory::submit! { @@ -66,7 +66,7 @@ inventory::submit! { /// // source = [0,1,2,3,1,0], target = [0,1,3,2,1], bound = 2 /// let problem = StringToStringCorrection::new(4, vec![0,1,2,3,1,0], vec![0,1,3,2,1], 2); /// let solver = BruteForce::new(); -/// let solution = solver.find_satisfying(&problem); +/// let solution = solver.find_witness(&problem); /// assert!(solution.is_some()); /// ``` #[derive(Debug, Clone, Serialize, Deserialize)] @@ -188,7 +188,7 @@ impl Problem for StringToStringCorrection { } } -impl SatisfactionProblem for StringToStringCorrection {} +impl WitnessProblem for StringToStringCorrection {} crate::declare_variants! { default sat StringToStringCorrection => "(2 * source_length + 1) ^ bound", diff --git a/src/models/misc/subset_sum.rs b/src/models/misc/subset_sum.rs index 26e38c3df..03fe0f457 100644 --- a/src/models/misc/subset_sum.rs +++ b/src/models/misc/subset_sum.rs @@ -8,7 +8,7 @@ //! reductions can construct large instances without fixed-width overflow. use crate::registry::{FieldInfo, ProblemSchemaEntry}; -use crate::traits::{Problem, SatisfactionProblem}; +use crate::traits::{Problem, WitnessProblem}; use num_bigint::{BigUint, ToBigUint}; use num_traits::Zero; use serde::{Deserialize, Serialize}; @@ -46,7 +46,7 @@ inventory::submit! { /// /// let problem = SubsetSum::new(vec![3u32, 7, 1, 8, 2, 4], 11u32); /// let solver = BruteForce::new(); -/// let solution = solver.find_satisfying(&problem); +/// let solution = solver.find_witness(&problem); /// assert!(solution.is_some()); /// ``` #[derive(Debug, Clone, Serialize, Deserialize)] @@ -135,7 +135,7 @@ impl Problem for SubsetSum { } } -impl SatisfactionProblem for SubsetSum {} +impl WitnessProblem for SubsetSum {} crate::declare_variants! { default sat SubsetSum => "2^(num_elements / 2)", diff --git a/src/models/misc/sum_of_squares_partition.rs b/src/models/misc/sum_of_squares_partition.rs index 697db9fd3..31a7000b0 100644 --- a/src/models/misc/sum_of_squares_partition.rs +++ b/src/models/misc/sum_of_squares_partition.rs @@ -6,7 +6,7 @@ //! NP-complete in the strong sense (Garey & Johnson, SP19). use crate::registry::{FieldInfo, ProblemSchemaEntry}; -use crate::traits::{Problem, SatisfactionProblem}; +use crate::traits::{Problem, WitnessProblem}; use serde::de::Error; use serde::{Deserialize, Deserializer, Serialize}; @@ -50,7 +50,7 @@ inventory::submit! { /// // 6 elements with sizes [5, 3, 8, 2, 7, 1], K=3 groups, bound J=240 /// let problem = SumOfSquaresPartition::new(vec![5, 3, 8, 2, 7, 1], 3, 240); /// let solver = BruteForce::new(); -/// let solution = solver.find_satisfying(&problem); +/// let solution = solver.find_witness(&problem); /// assert!(solution.is_some()); /// ``` #[derive(Debug, Clone, Serialize)] @@ -182,7 +182,7 @@ impl Problem for SumOfSquaresPartition { } } -impl SatisfactionProblem for SumOfSquaresPartition {} +impl WitnessProblem for SumOfSquaresPartition {} crate::declare_variants! { default sat SumOfSquaresPartition => "num_groups^num_elements", diff --git a/src/models/misc/timetable_design.rs b/src/models/misc/timetable_design.rs index b1be88713..c0776172f 100644 --- a/src/models/misc/timetable_design.rs +++ b/src/models/misc/timetable_design.rs @@ -5,7 +5,7 @@ //! requirements. use crate::registry::{FieldInfo, ProblemSchemaEntry}; -use crate::traits::{Problem, SatisfactionProblem}; +use crate::traits::{Problem, WitnessProblem}; use serde::{Deserialize, Serialize}; inventory::submit! { @@ -351,7 +351,7 @@ impl Problem for TimetableDesign { } } -impl SatisfactionProblem for TimetableDesign {} +impl WitnessProblem for TimetableDesign {} crate::declare_variants! { default sat TimetableDesign => "2^(num_craftsmen * num_tasks * num_periods)", diff --git a/src/models/set/comparative_containment.rs b/src/models/set/comparative_containment.rs index 865f183f9..e04cb3a7b 100644 --- a/src/models/set/comparative_containment.rs +++ b/src/models/set/comparative_containment.rs @@ -5,7 +5,7 @@ //! in the first family is at least its containment weight in the second. use crate::registry::{FieldInfo, ProblemSchemaEntry, VariantDimension}; -use crate::traits::{Problem, SatisfactionProblem}; +use crate::traits::{Problem, WitnessProblem}; use crate::types::{One, WeightElement}; use num_traits::Zero; use serde::{Deserialize, Serialize}; @@ -192,7 +192,7 @@ where } } -impl SatisfactionProblem for ComparativeContainment where +impl WitnessProblem for ComparativeContainment where W: WeightElement + crate::variant::VariantParam { } diff --git a/src/models/set/consecutive_sets.rs b/src/models/set/consecutive_sets.rs index b978551e1..d95796d08 100644 --- a/src/models/set/consecutive_sets.rs +++ b/src/models/set/consecutive_sets.rs @@ -6,7 +6,7 @@ //! contiguous block in some order) within the string. use crate::registry::{FieldInfo, ProblemSchemaEntry}; -use crate::traits::{Problem, SatisfactionProblem}; +use crate::traits::{Problem, WitnessProblem}; use serde::{Deserialize, Serialize}; use std::collections::HashSet; @@ -55,7 +55,7 @@ inventory::submit! { /// ); /// /// let solver = BruteForce::new(); -/// let solution = solver.find_satisfying(&problem); +/// let solution = solver.find_witness(&problem); /// /// // w = [0, 4, 2, 5, 1, 3] is a valid solution /// assert!(solution.is_some()); @@ -219,7 +219,7 @@ impl Problem for ConsecutiveSets { } } -impl SatisfactionProblem for ConsecutiveSets {} +impl WitnessProblem for ConsecutiveSets {} crate::declare_variants! { default sat ConsecutiveSets => "alphabet_size^bound_k * num_subsets", diff --git a/src/models/set/exact_cover_by_3_sets.rs b/src/models/set/exact_cover_by_3_sets.rs index 0d543cbcd..109644482 100644 --- a/src/models/set/exact_cover_by_3_sets.rs +++ b/src/models/set/exact_cover_by_3_sets.rs @@ -5,7 +5,7 @@ //! q disjoint triples covering every element exactly once. use crate::registry::{FieldInfo, ProblemSchemaEntry}; -use crate::traits::{Problem, SatisfactionProblem}; +use crate::traits::{Problem, WitnessProblem}; use serde::{Deserialize, Serialize}; use std::collections::HashSet; @@ -47,7 +47,7 @@ inventory::submit! { /// ); /// /// let solver = BruteForce::new(); -/// let solutions = solver.find_all_satisfying(&problem); +/// let solutions = solver.find_all_witnesses(&problem); /// /// // S0 and S1 form an exact cover /// assert_eq!(solutions.len(), 1); @@ -186,7 +186,7 @@ impl Problem for ExactCoverBy3Sets { } } -impl SatisfactionProblem for ExactCoverBy3Sets {} +impl WitnessProblem for ExactCoverBy3Sets {} crate::declare_variants! { default sat ExactCoverBy3Sets => "2^universe_size", diff --git a/src/models/set/maximum_set_packing.rs b/src/models/set/maximum_set_packing.rs index 80f0db1db..ee68ebd19 100644 --- a/src/models/set/maximum_set_packing.rs +++ b/src/models/set/maximum_set_packing.rs @@ -4,8 +4,8 @@ //! pairwise disjoint sets. use crate::registry::{FieldInfo, ProblemSchemaEntry, VariantDimension}; -use crate::traits::{OptimizationProblem, Problem}; -use crate::types::{Direction, One, SolutionSize, WeightElement}; +use crate::traits::{ObjectiveProblem, Problem}; +use crate::types::{ExtremumSense, Max, One, WeightElement}; use num_traits::Zero; use serde::{Deserialize, Serialize}; use std::collections::HashSet; @@ -46,7 +46,7 @@ inventory::submit! { /// ]); /// /// let solver = BruteForce::new(); -/// let solutions = solver.find_all_best(&problem); +/// let solutions = solver.find_all_witnesses(&problem); /// /// // Verify solutions are pairwise disjoint /// for sol in solutions { @@ -141,15 +141,15 @@ where W: WeightElement + crate::variant::VariantParam, { const NAME: &'static str = "MaximumSetPacking"; - type Value = SolutionSize; + type Value = Max; fn dims(&self) -> Vec { vec![2; self.sets.len()] } - fn evaluate(&self, config: &[usize]) -> SolutionSize { + fn evaluate(&self, config: &[usize]) -> Max { if !is_valid_packing(&self.sets, config) { - return SolutionSize::Invalid; + return Max(None); } let mut total = W::Sum::zero(); for (i, &selected) in config.iter().enumerate() { @@ -157,7 +157,7 @@ where total += self.weights[i].to_sum(); } } - SolutionSize::Valid(total) + Max(Some(total)) } fn variant() -> Vec<(&'static str, &'static str)> { @@ -165,14 +165,14 @@ where } } -impl OptimizationProblem for MaximumSetPacking +impl ObjectiveProblem for MaximumSetPacking where W: WeightElement + crate::variant::VariantParam, { type Objective = W::Sum; - fn direction(&self) -> Direction { - Direction::Maximize + fn direction(&self) -> ExtremumSense { + ExtremumSense::Maximize } } @@ -225,7 +225,7 @@ pub(crate) fn canonical_model_example_specs() -> Vec "2^num_attributes", diff --git a/src/models/set/minimum_hitting_set.rs b/src/models/set/minimum_hitting_set.rs index 293bb6acc..8d986319a 100644 --- a/src/models/set/minimum_hitting_set.rs +++ b/src/models/set/minimum_hitting_set.rs @@ -4,8 +4,8 @@ //! elements that intersects every set in a collection. use crate::registry::{FieldInfo, ProblemSchemaEntry, ProblemSizeFieldEntry}; -use crate::traits::{OptimizationProblem, Problem}; -use crate::types::{Direction, SolutionSize}; +use crate::traits::{ObjectiveProblem, Problem}; +use crate::types::{ExtremumSense, Min}; use serde::{Deserialize, Serialize}; inventory::submit! { @@ -117,24 +117,24 @@ impl MinimumHittingSet { impl Problem for MinimumHittingSet { const NAME: &'static str = "MinimumHittingSet"; - type Value = SolutionSize; + type Value = Min; fn dims(&self) -> Vec { vec![2; self.universe_size] } - fn evaluate(&self, config: &[usize]) -> SolutionSize { + fn evaluate(&self, config: &[usize]) -> Min { let Some(selected) = self.selected_elements(config) else { - return SolutionSize::Invalid; + return Min(None); }; if self.sets.iter().all(|set| { set.iter() .any(|element| selected.binary_search(element).is_ok()) }) { - SolutionSize::Valid(selected.len()) + Min(Some(selected.len())) } else { - SolutionSize::Invalid + Min(None) } } @@ -143,11 +143,11 @@ impl Problem for MinimumHittingSet { } } -impl OptimizationProblem for MinimumHittingSet { +impl ObjectiveProblem for MinimumHittingSet { type Objective = usize; - fn direction(&self) -> Direction { - Direction::Minimize + fn direction(&self) -> ExtremumSense { + ExtremumSense::Minimize } } @@ -172,7 +172,7 @@ pub(crate) fn canonical_model_example_specs() -> Vec; + type Value = Min; fn dims(&self) -> Vec { vec![2; self.sets.len()] } - fn evaluate(&self, config: &[usize]) -> SolutionSize { + fn evaluate(&self, config: &[usize]) -> Min { let covered = self.covered_elements(config); let is_valid = covered.len() == self.universe_size && (0..self.universe_size).all(|e| covered.contains(&e)); if !is_valid { - return SolutionSize::Invalid; + return Min(None); } let mut total = W::Sum::zero(); for (i, &selected) in config.iter().enumerate() { @@ -162,7 +162,7 @@ where total += self.weights[i].to_sum(); } } - SolutionSize::Valid(total) + Min(Some(total)) } fn variant() -> Vec<(&'static str, &'static str)> { @@ -170,14 +170,14 @@ where } } -impl OptimizationProblem for MinimumSetCovering +impl ObjectiveProblem for MinimumSetCovering where W: WeightElement + crate::variant::VariantParam, { type Objective = W::Sum; - fn direction(&self) -> Direction { - Direction::Minimize + fn direction(&self) -> ExtremumSense { + ExtremumSense::Minimize } } @@ -211,7 +211,7 @@ pub(crate) fn canonical_model_example_specs() -> Vec. use crate::registry::{FieldInfo, ProblemSchemaEntry}; -use crate::traits::{Problem, SatisfactionProblem}; +use crate::traits::{Problem, WitnessProblem}; use serde::{Deserialize, Serialize}; inventory::submit! { @@ -57,7 +57,7 @@ inventory::submit! { /// assert!(problem.evaluate(&[0, 0, 1, 1, 0, 0])); /// /// let solver = BruteForce::new(); -/// let solution = solver.find_satisfying(&problem); +/// let solution = solver.find_witness(&problem); /// assert!(solution.is_some()); /// ``` #[derive(Debug, Clone, Serialize, Deserialize)] @@ -202,7 +202,7 @@ impl Problem for PrimeAttributeName { } } -impl SatisfactionProblem for PrimeAttributeName {} +impl WitnessProblem for PrimeAttributeName {} crate::declare_variants! { default sat PrimeAttributeName => "2^num_attributes * num_dependencies * num_attributes", diff --git a/src/models/set/rooted_tree_storage_assignment.rs b/src/models/set/rooted_tree_storage_assignment.rs index 26a0028c4..c13b5ba98 100644 --- a/src/models/set/rooted_tree_storage_assignment.rs +++ b/src/models/set/rooted_tree_storage_assignment.rs @@ -1,7 +1,7 @@ //! Rooted Tree Storage Assignment problem implementation. use crate::registry::{FieldInfo, ProblemSchemaEntry}; -use crate::traits::{Problem, SatisfactionProblem}; +use crate::traits::{Problem, WitnessProblem}; use serde::{Deserialize, Serialize}; use std::collections::HashSet; @@ -214,7 +214,7 @@ impl Problem for RootedTreeStorageAssignment { } } -impl SatisfactionProblem for RootedTreeStorageAssignment {} +impl WitnessProblem for RootedTreeStorageAssignment {} crate::declare_variants! { default sat RootedTreeStorageAssignment => "universe_size^universe_size", diff --git a/src/models/set/set_basis.rs b/src/models/set/set_basis.rs index 9746893e6..76f9e2457 100644 --- a/src/models/set/set_basis.rs +++ b/src/models/set/set_basis.rs @@ -5,7 +5,7 @@ //! can be reconstructed as a union of some subcollection of the basis. use crate::registry::{FieldInfo, ProblemSchemaEntry}; -use crate::traits::{Problem, SatisfactionProblem}; +use crate::traits::{Problem, WitnessProblem}; use serde::{Deserialize, Serialize}; inventory::submit! { @@ -168,7 +168,7 @@ impl Problem for SetBasis { } } -impl SatisfactionProblem for SetBasis {} +impl WitnessProblem for SetBasis {} crate::declare_variants! { default sat SetBasis => "2^(basis_size * universe_size)", diff --git a/src/models/set/two_dimensional_consecutive_sets.rs b/src/models/set/two_dimensional_consecutive_sets.rs index be3dc056b..19cacf0f2 100644 --- a/src/models/set/two_dimensional_consecutive_sets.rs +++ b/src/models/set/two_dimensional_consecutive_sets.rs @@ -6,7 +6,7 @@ //! are spread across consecutive groups. use crate::registry::{FieldInfo, ProblemSchemaEntry}; -use crate::traits::{Problem, SatisfactionProblem}; +use crate::traits::{Problem, WitnessProblem}; use serde::de::Error as _; use serde::{Deserialize, Serialize}; use std::collections::HashSet; @@ -201,7 +201,7 @@ impl Problem for TwoDimensionalConsecutiveSets { } } -impl SatisfactionProblem for TwoDimensionalConsecutiveSets {} +impl WitnessProblem for TwoDimensionalConsecutiveSets {} crate::declare_variants! { default sat TwoDimensionalConsecutiveSets => "alphabet_size^alphabet_size", diff --git a/src/rules/graph.rs b/src/rules/graph.rs index 26548c882..303cbb698 100644 --- a/src/rules/graph.rs +++ b/src/rules/graph.rs @@ -237,9 +237,9 @@ pub struct NeighborInfo { pub hops: usize, } -/// Direction for graph traversal. +/// Traversal mode for graph exploration. #[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub enum TraversalDirection { +pub enum TraversalFlow { /// Follow outgoing edges (what can this reduce to?). Outgoing, /// Follow incoming edges (what can reduce to this?). @@ -458,6 +458,7 @@ impl ReductionGraph { /// Find the cheapest path between two specific problem variants while /// requiring a specific edge capability. + #[allow(clippy::too_many_arguments)] pub fn find_cheapest_path_mode( &self, source: &str, @@ -936,7 +937,7 @@ impl ReductionGraph { name: &str, variant: &BTreeMap, max_hops: usize, - direction: TraversalDirection, + direction: TraversalFlow, ) -> Vec { use std::collections::VecDeque; @@ -955,11 +956,11 @@ impl ReductionGraph { continue; } - let directions: Vec = match direction { - TraversalDirection::Outgoing => vec![petgraph::Direction::Outgoing], - TraversalDirection::Incoming => vec![petgraph::Direction::Incoming], - TraversalDirection::Both => { - vec![petgraph::Direction::Outgoing, petgraph::Direction::Incoming] + let directions = match direction { + TraversalFlow::Outgoing => vec![petgraph::Outgoing], + TraversalFlow::Incoming => vec![petgraph::Incoming], + TraversalFlow::Both => { + vec![petgraph::Outgoing, petgraph::Incoming] } }; @@ -991,7 +992,7 @@ impl ReductionGraph { name: &str, variant: &BTreeMap, max_hops: usize, - direction: TraversalDirection, + direction: TraversalFlow, ) -> Vec { use std::collections::VecDeque; @@ -1013,11 +1014,11 @@ impl ReductionGraph { continue; } - let directions: Vec = match direction { - TraversalDirection::Outgoing => vec![petgraph::Direction::Outgoing], - TraversalDirection::Incoming => vec![petgraph::Direction::Incoming], - TraversalDirection::Both => { - vec![petgraph::Direction::Outgoing, petgraph::Direction::Incoming] + let directions = match direction { + TraversalFlow::Outgoing => vec![petgraph::Outgoing], + TraversalFlow::Incoming => vec![petgraph::Incoming], + TraversalFlow::Both => { + vec![petgraph::Outgoing, petgraph::Incoming] } }; diff --git a/src/rules/mod.rs b/src/rules/mod.rs index 041d04599..a7a0c3dde 100644 --- a/src/rules/mod.rs +++ b/src/rules/mod.rs @@ -98,7 +98,7 @@ pub(crate) mod travelingsalesman_ilp; pub use graph::{ AggregateReductionChain, NeighborInfo, NeighborTree, ReductionChain, ReductionEdgeInfo, - ReductionGraph, ReductionMode, ReductionPath, ReductionStep, TraversalDirection, + ReductionGraph, ReductionMode, ReductionPath, ReductionStep, TraversalFlow, }; pub use traits::{ AggregateReductionResult, ReduceTo, ReduceToAggregate, ReductionAutoCast, ReductionResult, diff --git a/src/rules/test_helpers.rs b/src/rules/test_helpers.rs index 9136f735f..f9aa3e10d 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, Solver}; -use crate::traits::{OptimizationProblem, Problem, SatisfactionProblem}; +use crate::solvers::BruteForce; +use crate::traits::{ObjectiveProblem, Problem, WitnessProblem}; +use crate::types::Aggregate; use std::collections::HashSet; fn verify_optimization_round_trip( @@ -10,9 +11,8 @@ fn verify_optimization_round_trip( target_solution_kind: &str, context: &str, ) where - Source: OptimizationProblem + 'static, - ::Objective: std::fmt::Debug + PartialEq, - ::Value: std::fmt::Debug + PartialEq, + Source: ObjectiveProblem + 'static, + ::Value: Aggregate + std::fmt::Debug + PartialEq, Extract: Fn(&[usize]) -> Vec, { assert!( @@ -22,7 +22,7 @@ fn verify_optimization_round_trip( let solver = BruteForce::new(); let reference_solutions: HashSet> = - solver.find_all_best(source).into_iter().collect(); + solver.find_all_witnesses(source).into_iter().collect(); assert!( !reference_solutions.is_empty(), "{context}: direct source solver found no optimal solutions" @@ -48,11 +48,6 @@ fn verify_optimization_round_trip( ); for source_solution in &extracted { let extracted_metric = source.evaluate(source_solution); - assert!( - extracted_metric.is_valid(), - "{context}: extracted source solution is infeasible: {:?}", - source_solution - ); assert_eq!( extracted_metric, reference_metric, "{context}: extracted source objective does not match direct solve" @@ -67,7 +62,8 @@ fn verify_satisfaction_round_trip( target_solution_kind: &str, context: &str, ) where - Source: SatisfactionProblem + 'static, + Source: WitnessProblem + 'static, + ::Value: Aggregate + std::fmt::Debug, Extract: Fn(&[usize]) -> Vec, { assert!( @@ -82,9 +78,11 @@ fn verify_satisfaction_round_trip( !extracted.is_empty(), "{context}: no extracted source solutions" ); + let total = ::solve(&BruteForce::new(), source); for source_solution in &extracted { + let value = source.evaluate(source_solution); assert!( - source.evaluate(source_solution), + ::contributes_to_witnesses(&value, &total), "{context}: extracted source solution is not satisfying: {:?}", source_solution ); @@ -97,12 +95,12 @@ pub(crate) fn assert_optimization_round_trip_from_optimization_target( context: &str, ) where R: ReductionResult, - R::Source: OptimizationProblem + 'static, - R::Target: OptimizationProblem + 'static, - ::Objective: std::fmt::Debug + PartialEq, - ::Value: std::fmt::Debug + PartialEq, + R::Source: ObjectiveProblem + 'static, + R::Target: ObjectiveProblem + 'static, + ::Value: Aggregate + std::fmt::Debug + PartialEq, + ::Value: Aggregate, { - let target_solutions = BruteForce::new().find_all_best(reduction.target_problem()); + let target_solutions = BruteForce::new().find_all_witnesses(reduction.target_problem()); verify_optimization_round_trip( source, target_solutions, @@ -118,12 +116,12 @@ pub(crate) fn assert_optimization_round_trip_from_satisfaction_target( context: &str, ) where R: ReductionResult, - R::Source: OptimizationProblem + 'static, - R::Target: SatisfactionProblem + 'static, - ::Objective: std::fmt::Debug + PartialEq, - ::Value: std::fmt::Debug + PartialEq, + R::Source: ObjectiveProblem + 'static, + R::Target: WitnessProblem + 'static, + ::Value: Aggregate + std::fmt::Debug + PartialEq, + ::Value: Aggregate, { - let target_solutions = BruteForce::new().find_all_satisfying(reduction.target_problem()); + let target_solutions = BruteForce::new().find_all_witnesses(reduction.target_problem()); verify_optimization_round_trip( source, target_solutions, @@ -138,12 +136,12 @@ pub(crate) fn assert_optimization_round_trip_chain( chain: &ReductionChain, context: &str, ) where - Source: OptimizationProblem + 'static, - Target: OptimizationProblem + 'static, - ::Objective: std::fmt::Debug + PartialEq, - ::Value: std::fmt::Debug + PartialEq, + Source: ObjectiveProblem + 'static, + Target: ObjectiveProblem + 'static, + ::Value: Aggregate + std::fmt::Debug + PartialEq, + ::Value: Aggregate, { - let target_solutions = BruteForce::new().find_all_best(chain.target_problem::()); + let target_solutions = BruteForce::new().find_all_witnesses(chain.target_problem::()); verify_optimization_round_trip( source, target_solutions, @@ -159,10 +157,12 @@ pub(crate) fn assert_satisfaction_round_trip_from_optimization_target( context: &str, ) where R: ReductionResult, - R::Source: SatisfactionProblem + 'static, - R::Target: OptimizationProblem + 'static, + R::Source: WitnessProblem + 'static, + R::Target: ObjectiveProblem + 'static, + ::Value: Aggregate + std::fmt::Debug, + ::Value: Aggregate, { - let target_solutions = BruteForce::new().find_all_best(reduction.target_problem()); + let target_solutions = BruteForce::new().find_all_witnesses(reduction.target_problem()); verify_satisfaction_round_trip( source, target_solutions, @@ -178,10 +178,12 @@ pub(crate) fn assert_satisfaction_round_trip_from_satisfaction_target( context: &str, ) where R: ReductionResult, - R::Source: SatisfactionProblem + 'static, - R::Target: SatisfactionProblem + 'static, + R::Source: WitnessProblem + 'static, + R::Target: WitnessProblem + 'static, + ::Value: Aggregate + std::fmt::Debug, + ::Value: Aggregate, { - let target_solutions = BruteForce::new().find_all_satisfying(reduction.target_problem()); + let target_solutions = BruteForce::new().find_all_witnesses(reduction.target_problem()); verify_satisfaction_round_trip( source, target_solutions, @@ -193,16 +195,18 @@ pub(crate) fn assert_satisfaction_round_trip_from_satisfaction_target( pub(crate) fn solve_optimization_problem

(problem: &P) -> Option> where - P: OptimizationProblem + 'static, + P: ObjectiveProblem + 'static, + P::Value: Aggregate, { - BruteForce::new().find_best(problem) + BruteForce::new().find_witness(problem) } pub(crate) fn solve_satisfaction_problem

(problem: &P) -> Option> where - P: SatisfactionProblem + 'static, + P: WitnessProblem + 'static, + P::Value: Aggregate, { - BruteForce::new().find_satisfying(problem) + BruteForce::new().find_witness(problem) } #[cfg(test)] @@ -214,15 +218,15 @@ mod tests { assert_satisfaction_round_trip_from_satisfaction_target, }; use crate::rules::ReductionResult; - use crate::traits::{OptimizationProblem, Problem, SatisfactionProblem}; - use crate::types::{Direction, SolutionSize}; + use crate::traits::{ObjectiveProblem, Problem, WitnessProblem}; + use crate::types::{ExtremumSense, Max}; #[derive(Clone)] - struct ToyOptimizationProblem; + struct ToyObjectiveProblem; - impl Problem for ToyOptimizationProblem { - const NAME: &'static str = "ToyOptimizationProblem"; - type Value = SolutionSize; + impl Problem for ToyObjectiveProblem { + const NAME: &'static str = "ToyObjectiveProblem"; + type Value = Max; fn dims(&self) -> Vec { vec![2, 2] @@ -230,8 +234,8 @@ mod tests { fn evaluate(&self, config: &[usize]) -> Self::Value { match config { - [1, 0] | [0, 1] => SolutionSize::Valid(1), - _ => SolutionSize::Invalid, + [1, 0] | [0, 1] => Max(Some(1)), + _ => Max(None), } } @@ -240,19 +244,19 @@ mod tests { } } - impl OptimizationProblem for ToyOptimizationProblem { + impl ObjectiveProblem for ToyObjectiveProblem { type Objective = i32; - fn direction(&self) -> Direction { - Direction::Maximize + fn direction(&self) -> ExtremumSense { + ExtremumSense::Maximize } } #[derive(Clone)] - struct ToySatisfactionProblem; + struct ToyWitnessProblem; - impl Problem for ToySatisfactionProblem { - const NAME: &'static str = "ToySatisfactionProblem"; + impl Problem for ToyWitnessProblem { + const NAME: &'static str = "ToyWitnessProblem"; type Value = bool; fn dims(&self) -> Vec { @@ -268,15 +272,15 @@ mod tests { } } - impl SatisfactionProblem for ToySatisfactionProblem {} + impl WitnessProblem for ToyWitnessProblem {} struct OptToOptReduction { - target: ToyOptimizationProblem, + target: ToyObjectiveProblem, } impl ReductionResult for OptToOptReduction { - type Source = ToyOptimizationProblem; - type Target = ToyOptimizationProblem; + type Source = ToyObjectiveProblem; + type Target = ToyObjectiveProblem; fn target_problem(&self) -> &Self::Target { &self.target @@ -288,12 +292,12 @@ mod tests { } struct OptToSatReduction { - target: ToySatisfactionProblem, + target: ToyWitnessProblem, } impl ReductionResult for OptToSatReduction { - type Source = ToyOptimizationProblem; - type Target = ToySatisfactionProblem; + type Source = ToyObjectiveProblem; + type Target = ToyWitnessProblem; fn target_problem(&self) -> &Self::Target { &self.target @@ -305,12 +309,12 @@ mod tests { } struct SatToOptReduction { - target: ToyOptimizationProblem, + target: ToyObjectiveProblem, } impl ReductionResult for SatToOptReduction { - type Source = ToySatisfactionProblem; - type Target = ToyOptimizationProblem; + type Source = ToyWitnessProblem; + type Target = ToyObjectiveProblem; fn target_problem(&self) -> &Self::Target { &self.target @@ -322,12 +326,12 @@ mod tests { } struct SatToSatReduction { - target: ToySatisfactionProblem, + target: ToyWitnessProblem, } impl ReductionResult for SatToSatReduction { - type Source = ToySatisfactionProblem; - type Target = ToySatisfactionProblem; + type Source = ToyWitnessProblem; + type Target = ToyWitnessProblem; fn target_problem(&self) -> &Self::Target { &self.target @@ -340,19 +344,19 @@ mod tests { #[test] fn test_optimization_round_trip_wrappers_accept_identity_reductions() { - let source = ToyOptimizationProblem; + let source = ToyObjectiveProblem; assert_optimization_round_trip_from_optimization_target( &source, &OptToOptReduction { - target: ToyOptimizationProblem, + target: ToyObjectiveProblem, }, "opt->opt", ); assert_optimization_round_trip_from_satisfaction_target( &source, &OptToSatReduction { - target: ToySatisfactionProblem, + target: ToyWitnessProblem, }, "opt->sat", ); @@ -360,19 +364,19 @@ mod tests { #[test] fn test_satisfaction_round_trip_wrappers_accept_identity_reductions() { - let source = ToySatisfactionProblem; + let source = ToyWitnessProblem; assert_satisfaction_round_trip_from_optimization_target( &source, &SatToOptReduction { - target: ToyOptimizationProblem, + target: ToyObjectiveProblem, }, "sat->opt", ); assert_satisfaction_round_trip_from_satisfaction_target( &source, &SatToSatReduction { - target: ToySatisfactionProblem, + target: ToyWitnessProblem, }, "sat->sat", ); diff --git a/src/rules/traits.rs b/src/rules/traits.rs index 2a1df4281..a46dc19ec 100644 --- a/src/rules/traits.rs +++ b/src/rules/traits.rs @@ -51,7 +51,7 @@ pub trait ReductionResult { /// /// // Solve and extract solutions /// let solver = BruteForce::new(); -/// let solutions = solver.find_all_best(is_problem); +/// let solutions = solver.find_all_witnesses(is_problem); /// let sat_solutions: Vec<_> = solutions.iter() /// .map(|s| reduction.extract_solution(s)) /// .collect(); @@ -129,7 +129,9 @@ impl ReductionResult for ReductionAutoCast { } } -impl> AggregateReductionResult for ReductionAutoCast { +impl> AggregateReductionResult + for ReductionAutoCast +{ type Source = S; type Target = T; diff --git a/src/solvers/brute_force.rs b/src/solvers/brute_force.rs index 47c7d578b..73b33e742 100644 --- a/src/solvers/brute_force.rs +++ b/src/solvers/brute_force.rs @@ -2,7 +2,7 @@ use crate::config::DimsIterator; use crate::solvers::Solver; -use crate::traits::{OptimizationProblem, Problem, SatisfactionProblem}; +use crate::traits::{ObjectiveProblem, Problem, WitnessProblem}; use crate::types::Aggregate; /// A brute force solver that enumerates all possible configurations. @@ -20,62 +20,39 @@ impl BruteForce { } /// Temporary compatibility helper for optimization problems. - pub fn find_all_best(&self, problem: &P) -> Vec> { - let iter = DimsIterator::new(problem.dims()); - let direction = problem.direction(); - let mut best_solutions: Vec> = vec![]; - let mut best_metric: Option< - crate::types::SolutionSize<

::Objective>, - > = None; - - for config in iter { - let metric = problem.evaluate(&config); - - if !metric.is_valid() { - continue; - } - - let dominated = match &best_metric { - None => false, - Some(current_best) => current_best.is_better(&metric, direction), - }; - - if dominated { - continue; - } - - let dominates = match &best_metric { - None => true, - Some(current_best) => metric.is_better(current_best, direction), - }; - - if dominates { - best_metric = Some(metric); - best_solutions.clear(); - best_solutions.push(config); - } else if best_metric.is_some() { - best_solutions.push(config); - } - } - - best_solutions + pub fn find_all_best

(&self, problem: &P) -> Vec> + where + P: ObjectiveProblem, + P::Value: Aggregate, + { + self.find_all_witnesses(problem) } /// Temporary compatibility helper for optimization problems. - pub fn find_best(&self, problem: &P) -> Option> { - self.find_all_best(problem).into_iter().next() + pub fn find_best

(&self, problem: &P) -> Option> + where + P: ObjectiveProblem, + P::Value: Aggregate, + { + self.find_witness(problem) } /// Temporary compatibility helper for satisfaction problems. - pub fn find_all_satisfying(&self, problem: &P) -> Vec> { - DimsIterator::new(problem.dims()) - .filter(|config| problem.evaluate(config)) - .collect() + pub fn find_all_satisfying

(&self, problem: &P) -> Vec> + where + P: WitnessProblem, + P::Value: Aggregate, + { + self.find_all_witnesses(problem) } /// Temporary compatibility helper for satisfaction problems. - pub fn find_satisfying(&self, problem: &P) -> Option> { - DimsIterator::new(problem.dims()).find(|config| problem.evaluate(config)) + pub fn find_satisfying

(&self, problem: &P) -> Option> + where + P: WitnessProblem, + P::Value: Aggregate, + { + self.find_witness(problem) } /// Find one witness configuration when the aggregate value admits them. @@ -141,12 +118,20 @@ impl Solver for BruteForce { .fold(P::Value::identity(), P::Value::combine) } - fn find_best(&self, problem: &P) -> Option> { - BruteForce::find_best(self, problem) + fn find_best

(&self, problem: &P) -> Option> + where + P: ObjectiveProblem, + P::Value: Aggregate, + { + BruteForce::find_witness(self, problem) } - fn find_satisfying(&self, problem: &P) -> Option> { - BruteForce::find_satisfying(self, problem) + fn find_satisfying

(&self, problem: &P) -> Option> + where + P: WitnessProblem, + P::Value: Aggregate, + { + BruteForce::find_witness(self, problem) } } diff --git a/src/solvers/ilp/solver.rs b/src/solvers/ilp/solver.rs index 995d08370..0f12d5562 100644 --- a/src/solvers/ilp/solver.rs +++ b/src/solvers/ilp/solver.rs @@ -270,7 +270,8 @@ impl ILPSolver { let graph = crate::rules::ReductionGraph::new(); - let Some(path) = self.best_path_to_ilp(&graph, name, variant, ReductionMode::Witness) else { + let Some(path) = self.best_path_to_ilp(&graph, name, variant, ReductionMode::Witness) + else { if self .best_path_to_ilp(&graph, name, variant, ReductionMode::Aggregate) .is_some() @@ -290,11 +291,11 @@ impl ILPSolver { name: name.to_string(), } })?; - let ilp_solution = - self.solve_dyn(chain.target_problem_any()) - .ok_or_else(|| SolveViaReductionError::NoSolution { - name: name.to_string(), - })?; + let ilp_solution = self.solve_dyn(chain.target_problem_any()).ok_or_else(|| { + SolveViaReductionError::NoSolution { + name: name.to_string(), + } + })?; Ok(chain.extract_solution(&ilp_solution)) } diff --git a/src/solvers/mod.rs b/src/solvers/mod.rs index 4fea37889..326872aad 100644 --- a/src/solvers/mod.rs +++ b/src/solvers/mod.rs @@ -10,7 +10,7 @@ pub use brute_force::BruteForce; #[cfg(feature = "ilp-solver")] pub use ilp::ILPSolver; -use crate::traits::{OptimizationProblem, Problem, SatisfactionProblem}; +use crate::traits::{ObjectiveProblem, Problem, WitnessProblem}; /// Trait for problem solvers. pub trait Solver { @@ -21,8 +21,14 @@ pub trait Solver { P::Value: crate::types::Aggregate; /// Temporary compatibility helper for optimization problems. - fn find_best(&self, problem: &P) -> Option>; + fn find_best

(&self, problem: &P) -> Option> + where + P: ObjectiveProblem, + P::Value: crate::types::Aggregate; /// Temporary compatibility helper for satisfaction problems. - fn find_satisfying(&self, problem: &P) -> Option>; + fn find_satisfying

(&self, problem: &P) -> Option> + where + P: WitnessProblem, + P::Value: crate::types::Aggregate; } diff --git a/src/topology/directed_graph.rs b/src/topology/directed_graph.rs index d8491206a..84fe30027 100644 --- a/src/topology/directed_graph.rs +++ b/src/topology/directed_graph.rs @@ -115,7 +115,7 @@ impl DirectedGraph { /// These are all vertices `u` such that there is an arc `u → v`. pub fn predecessors(&self, v: usize) -> Vec { self.inner - .neighbors_directed(NodeIndex::new(v), petgraph::Direction::Incoming) + .neighbors_directed(NodeIndex::new(v), petgraph::Incoming) .map(|n| n.index()) .collect() } diff --git a/src/traits.rs b/src/traits.rs index e8a39a096..57bf1c5fe 100644 --- a/src/traits.rs +++ b/src/traits.rs @@ -34,15 +34,15 @@ pub trait Problem: Clone { } /// Temporary compatibility trait for optimization problems during the aggregate migration. -pub trait OptimizationProblem: Problem> { +pub trait ObjectiveProblem: Problem { /// The inner objective value type (e.g., `i32`, `f64`). type Objective: PartialOrd + Clone; /// Whether to maximize or minimize the metric. - fn direction(&self) -> crate::types::Direction; + fn direction(&self) -> crate::types::ExtremumSense; } /// Temporary compatibility trait for satisfaction problems during the aggregate migration. -pub trait SatisfactionProblem: Problem {} +pub trait WitnessProblem: Problem {} /// Marker trait for explicitly declared problem variants. /// diff --git a/src/types.rs b/src/types.rs index 8b3772120..c5e249c64 100644 --- a/src/types.rs +++ b/src/types.rs @@ -227,6 +227,20 @@ impl fmt::Display for Max { } } +impl Max { + pub fn is_valid(&self) -> bool { + self.0.is_some() + } + + pub fn size(&self) -> Option<&V> { + self.0.as_ref() + } + + pub fn unwrap(self) -> V { + self.0.expect("called unwrap on invalid Max value") + } +} + /// Minimum aggregate over feasible values. #[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)] pub struct Min(pub Option); @@ -270,6 +284,20 @@ impl fmt::Display for Min { } } +impl Min { + pub fn is_valid(&self) -> bool { + self.0.is_some() + } + + pub fn size(&self) -> Option<&V> { + self.0.as_ref() + } + + pub fn unwrap(self) -> V { + self.0.expect("called unwrap on invalid Min value") + } +} + /// Sum aggregate for value-only problems. #[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)] pub struct Sum(pub W); @@ -314,6 +342,24 @@ impl Aggregate for Or { } } +impl Aggregate for bool { + fn identity() -> Self { + false + } + + fn combine(self, other: Self) -> Self { + self || other + } + + fn supports_witnesses() -> bool { + true + } + + fn contributes_to_witnesses(config_value: &Self, total: &Self) -> bool { + *config_value && *total + } +} + impl fmt::Display for Or { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { write!(f, "Or({})", self.0) @@ -340,98 +386,109 @@ impl fmt::Display for And { } } -/// Result of evaluating a constrained optimization problem. -/// -/// For optimization problems with constraints (like MaximumIndependentSet), -/// configurations may be infeasible. This enum explicitly represents validity. -/// -/// # Example -/// -/// ``` -/// use problemreductions::types::SolutionSize; -/// -/// let valid = SolutionSize::Valid(42); -/// assert!(valid.is_valid()); -/// assert_eq!(valid.size(), Some(&42)); -/// -/// let invalid: SolutionSize = SolutionSize::Invalid; -/// assert!(!invalid.is_valid()); -/// ``` -#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default, Serialize, Deserialize)] -pub enum SolutionSize { - /// A valid (feasible) solution with the given objective value. - Valid(T), - /// An invalid (infeasible) solution that violates constraints. - #[default] - Invalid, -} - -impl SolutionSize { - /// Returns true if this is a valid solution. - pub fn is_valid(&self) -> bool { - matches!(self, SolutionSize::Valid(_)) - } +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)] +pub enum ExtremumSense { + Maximize, + Minimize, +} + +#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)] +pub struct Extremum { + pub sense: ExtremumSense, + pub value: Option, +} - /// Returns the size if valid, None if invalid. - pub fn size(&self) -> Option<&T> { - match self { - SolutionSize::Valid(t) => Some(t), - SolutionSize::Invalid => None, +impl Extremum { + pub fn maximize(value: Option) -> Self { + Self { + sense: ExtremumSense::Maximize, + value, } } - /// Unwraps the size, panicking if invalid. - pub fn unwrap(self) -> T { - match self { - SolutionSize::Valid(t) => t, - SolutionSize::Invalid => panic!("called unwrap on Invalid SolutionSize"), + pub fn minimize(value: Option) -> Self { + Self { + sense: ExtremumSense::Minimize, + value, } } - /// Maps the inner value if valid. - pub fn map U>(self, f: F) -> SolutionSize { - match self { - SolutionSize::Valid(t) => SolutionSize::Valid(f(t)), - SolutionSize::Invalid => SolutionSize::Invalid, - } + pub fn is_valid(&self) -> bool { + self.value.is_some() + } + + pub fn size(&self) -> Option<&V> { + self.value.as_ref() + } + + pub fn unwrap(self) -> V { + self.value.expect("called unwrap on invalid Extremum value") } } -impl SolutionSize { - /// Returns true if self is a better solution than other for the given direction. - /// - /// - For maximization: larger values are better - /// - For minimization: smaller values are better - /// - Valid solutions are always better than invalid ones - /// - Two invalid solutions are equally bad (neither is better) - /// - /// # Panics - /// - /// Panics if comparing two valid values that are not comparable (e.g., NaN for f64). - pub fn is_better(&self, other: &Self, direction: Direction) -> bool { - match (self, other) { - (SolutionSize::Valid(a), SolutionSize::Valid(b)) => { - use std::cmp::Ordering; - let ord = a.partial_cmp(b).expect("cannot compare values (NaN?)"); - match direction { - Direction::Maximize => ord == Ordering::Greater, - Direction::Minimize => ord == Ordering::Less, +impl Aggregate for Extremum { + fn identity() -> Self { + Self::maximize(None) + } + + fn combine(self, other: Self) -> Self { + use std::cmp::Ordering; + + match (self.value, other.value) { + (None, rhs) => Self { + sense: other.sense, + value: rhs, + }, + (lhs, None) => Self { + sense: self.sense, + value: lhs, + }, + (Some(lhs), Some(rhs)) => { + assert_eq!( + self.sense, other.sense, + "cannot combine Extremum values with different senses" + ); + let ord = lhs.partial_cmp(&rhs).expect("cannot compare values (NaN?)"); + let keep_self = match self.sense { + ExtremumSense::Maximize => matches!(ord, Ordering::Equal | Ordering::Greater), + ExtremumSense::Minimize => matches!(ord, Ordering::Equal | Ordering::Less), + }; + if keep_self { + Self { + sense: self.sense, + value: Some(lhs), + } + } else { + Self { + sense: other.sense, + value: Some(rhs), + } } } - (SolutionSize::Valid(_), SolutionSize::Invalid) => true, - (SolutionSize::Invalid, SolutionSize::Valid(_)) => false, - (SolutionSize::Invalid, SolutionSize::Invalid) => false, } } + + fn supports_witnesses() -> bool { + true + } + + fn contributes_to_witnesses(config_value: &Self, total: &Self) -> bool { + matches!( + (config_value.value.as_ref(), total.value.as_ref()), + (Some(value), Some(best)) if config_value.sense == total.sense && value == best + ) + } } -/// Optimization direction. -#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)] -pub enum Direction { - /// Maximize the objective value. - Maximize, - /// Minimize the objective value. - Minimize, +impl fmt::Display for Extremum { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match (&self.sense, &self.value) { + (ExtremumSense::Maximize, Some(value)) => write!(f, "Max({value})"), + (ExtremumSense::Maximize, None) => write!(f, "Max(None)"), + (ExtremumSense::Minimize, Some(value)) => write!(f, "Min({value})"), + (ExtremumSense::Minimize, None) => write!(f, "Min(None)"), + } + } } /// Problem size metadata (varies by problem type). diff --git a/src/unit_tests/example_db.rs b/src/unit_tests/example_db.rs index 2abe80d6c..6a3036fdb 100644 --- a/src/unit_tests/example_db.rs +++ b/src/unit_tests/example_db.rs @@ -173,7 +173,7 @@ fn test_find_model_example_minimum_dummy_activities_pert() { assert_eq!(example.problem, "MinimumDummyActivitiesPert"); assert_eq!(example.variant, problem.variant); assert!(example.instance.is_object()); - assert_eq!(example.optimal_value, serde_json::json!({"Valid": 2})); + assert_eq!(example.optimal_value, serde_json::json!(2)); assert!( !example.optimal_config.is_empty(), "canonical example should include an optimal merge selection" diff --git a/src/unit_tests/export.rs b/src/unit_tests/export.rs index 27776a8cf..c1c9a578f 100644 --- a/src/unit_tests/export.rs +++ b/src/unit_tests/export.rs @@ -118,7 +118,7 @@ fn test_write_example_db_uses_one_line_per_example_entry() { // Add richer data so the one-line-per-entry format is meaningful db.models[0].instance = serde_json::json!({"n": 5, "edges": [[0, 1], [1, 2]]}); db.models[0].optimal_config = vec![1, 0, 1]; - db.models[0].optimal_value = serde_json::json!({"Valid": 2}); + db.models[0].optimal_value = serde_json::json!(2); db.rules[0].source.instance = serde_json::json!({"n": 3, "edges": [[0, 1], [1, 2]]}); db.rules[0].target.instance = serde_json::json!({"m": 4, "weights": [1, 2, 3, 4]}); db.rules[0].solutions = vec![SolutionPair { @@ -229,11 +229,11 @@ fn model_example_new() { variant_to_map(vec![("graph", "SimpleGraph"), ("weight", "i32")]), serde_json::json!({"num_vertices": 3, "edges": [[0, 1], [1, 2]]}), vec![1, 0, 1], - serde_json::json!({"Valid": 2}), + serde_json::json!(2), ); assert_eq!(example.problem, "MaximumIndependentSet"); assert_eq!(example.optimal_config, vec![1, 0, 1]); - assert_eq!(example.optimal_value, serde_json::json!({"Valid": 2})); + assert_eq!(example.optimal_value, serde_json::json!(2)); assert!(example.instance.is_object()); } @@ -286,7 +286,7 @@ fn write_model_example_to_creates_json_file() { variant: variant_to_map(vec![("graph", "SimpleGraph")]), instance: serde_json::json!({"n": 3}), optimal_config: vec![1, 0, 1], - optimal_value: serde_json::json!({"Valid": 2}), + optimal_value: serde_json::json!(2), }; write_model_example_to(&dir, "test_model", &example); let path = dir.join("test_model.json"); diff --git a/src/unit_tests/graph_models.rs b/src/unit_tests/graph_models.rs index 363d10cc4..07b9a266f 100644 --- a/src/unit_tests/graph_models.rs +++ b/src/unit_tests/graph_models.rs @@ -9,8 +9,8 @@ use crate::models::graph::minimum_vertex_cover::is_vertex_cover; use crate::models::graph::{KColoring, MaximumIndependentSet, MinimumVertexCover}; use crate::prelude::*; use crate::topology::{Graph, SimpleGraph}; -use crate::traits::{OptimizationProblem, Problem}; -use crate::types::{Direction, SolutionSize}; +use crate::traits::{ObjectiveProblem, Problem}; +use crate::types::{ExtremumSense, Max, Min}; use crate::variant::{K1, K2, K3, K4}; // ============================================================================= @@ -61,10 +61,10 @@ mod maximum_independent_set { MaximumIndependentSet::new(SimpleGraph::new(4, vec![(0, 1), (2, 3)]), vec![1i32; 4]); // Valid: select 0 and 2 (not adjacent) - assert_eq!(problem.evaluate(&[1, 0, 1, 0]), SolutionSize::Valid(2)); + assert_eq!(problem.evaluate(&[1, 0, 1, 0]), Max(Some(2))); // Valid: select 1 and 3 (not adjacent) - assert_eq!(problem.evaluate(&[0, 1, 0, 1]), SolutionSize::Valid(2)); + assert_eq!(problem.evaluate(&[0, 1, 0, 1]), Max(Some(2))); } #[test] @@ -73,10 +73,10 @@ mod maximum_independent_set { MaximumIndependentSet::new(SimpleGraph::new(4, vec![(0, 1), (2, 3)]), vec![1i32; 4]); // Invalid: 0 and 1 are adjacent - returns Invalid - assert_eq!(problem.evaluate(&[1, 1, 0, 0]), SolutionSize::Invalid); + assert_eq!(problem.evaluate(&[1, 1, 0, 0]), Max(None)); // Invalid: 2 and 3 are adjacent - assert_eq!(problem.evaluate(&[0, 0, 1, 1]), SolutionSize::Invalid); + assert_eq!(problem.evaluate(&[0, 0, 1, 1]), Max(None)); } #[test] @@ -84,7 +84,7 @@ mod maximum_independent_set { let problem = MaximumIndependentSet::new(SimpleGraph::new(3, vec![(0, 1), (1, 2)]), vec![1i32; 3]); // Empty selection is valid with size 0 - assert_eq!(problem.evaluate(&[0, 0, 0]), SolutionSize::Valid(0)); + assert_eq!(problem.evaluate(&[0, 0, 0]), Max(Some(0))); } #[test] @@ -93,10 +93,10 @@ mod maximum_independent_set { MaximumIndependentSet::new(SimpleGraph::new(3, vec![(0, 1)]), vec![10, 20, 30]); // Select vertex 2 (weight 30) - assert_eq!(problem.evaluate(&[0, 0, 1]), SolutionSize::Valid(30)); + assert_eq!(problem.evaluate(&[0, 0, 1]), Max(Some(30))); // Select vertices 0 and 2 (weights 10 + 30 = 40) - assert_eq!(problem.evaluate(&[1, 0, 1]), SolutionSize::Valid(40)); + assert_eq!(problem.evaluate(&[1, 0, 1]), Max(Some(40))); } #[test] @@ -108,7 +108,7 @@ mod maximum_independent_set { ); let solver = BruteForce::new(); - let solutions = solver.find_all_best(&problem); + let solutions = solver.find_all_witnesses(&problem); // All solutions should have exactly 1 vertex selected assert_eq!(solutions.len(), 3); // Three equivalent solutions for sol in &solutions { @@ -125,13 +125,13 @@ mod maximum_independent_set { ); let solver = BruteForce::new(); - let solutions = solver.find_all_best(&problem); + let solutions = solver.find_all_witnesses(&problem); // Maximum size is 2 for sol in &solutions { let size: usize = sol.iter().sum(); assert_eq!(size, 2); // Verify it's valid (evaluate returns Valid, not Invalid) - assert_eq!(problem.evaluate(sol), SolutionSize::Valid(2)); + assert_eq!(problem.evaluate(sol), Max(Some(2))); } } @@ -142,7 +142,7 @@ mod maximum_independent_set { MaximumIndependentSet::new(SimpleGraph::new(3, vec![(0, 1), (1, 2)]), vec![1, 100, 1]); let solver = BruteForce::new(); - let solutions = solver.find_all_best(&problem); + let solutions = solver.find_all_witnesses(&problem); assert_eq!(solutions.len(), 1); // Should select vertex 1 (weight 100) over vertices 0+2 (weight 2) assert_eq!(solutions[0], vec![0, 1, 0]); @@ -175,7 +175,7 @@ mod maximum_independent_set { #[test] fn test_direction() { let problem = MaximumIndependentSet::new(SimpleGraph::new(3, vec![(0, 1)]), vec![1i32; 3]); - assert_eq!(problem.direction(), Direction::Maximize); + assert_eq!(problem.direction(), ExtremumSense::Maximize); } #[test] @@ -200,7 +200,7 @@ mod maximum_independent_set { let problem = MaximumIndependentSet::new(SimpleGraph::new(3, vec![]), vec![1i32; 3]); let solver = BruteForce::new(); - let solutions = solver.find_all_best(&problem); + let solutions = solver.find_all_witnesses(&problem); assert_eq!(solutions.len(), 1); // All vertices can be selected assert_eq!(solutions[0], vec![1, 1, 1]); @@ -215,8 +215,8 @@ mod maximum_independent_set { assert!(problem.evaluate(&[1, 0, 1]).is_valid()); assert!(problem.evaluate(&[0, 1, 0]).is_valid()); // Invalid configurations return Invalid - assert_eq!(problem.evaluate(&[1, 1, 0]), SolutionSize::Invalid); - assert_eq!(problem.evaluate(&[0, 1, 1]), SolutionSize::Invalid); + assert_eq!(problem.evaluate(&[1, 1, 0]), Max(None)); + assert_eq!(problem.evaluate(&[0, 1, 1]), Max(None)); } } @@ -251,10 +251,10 @@ mod minimum_vertex_cover { MinimumVertexCover::new(SimpleGraph::new(3, vec![(0, 1), (1, 2)]), vec![1i32; 3]); // Valid: select vertex 1 (covers both edges) - assert_eq!(problem.evaluate(&[0, 1, 0]), SolutionSize::Valid(1)); + assert_eq!(problem.evaluate(&[0, 1, 0]), Min(Some(1))); // Valid: select all vertices - assert_eq!(problem.evaluate(&[1, 1, 1]), SolutionSize::Valid(3)); + assert_eq!(problem.evaluate(&[1, 1, 1]), Min(Some(3))); } #[test] @@ -263,10 +263,10 @@ mod minimum_vertex_cover { MinimumVertexCover::new(SimpleGraph::new(3, vec![(0, 1), (1, 2)]), vec![1i32; 3]); // Invalid: no vertex selected - returns Invalid for minimization - assert_eq!(problem.evaluate(&[0, 0, 0]), SolutionSize::Invalid); + assert_eq!(problem.evaluate(&[0, 0, 0]), Min(None)); // Invalid: only vertex 0 selected (edge 1-2 not covered) - assert_eq!(problem.evaluate(&[1, 0, 0]), SolutionSize::Invalid); + assert_eq!(problem.evaluate(&[1, 0, 0]), Min(None)); } #[test] @@ -276,7 +276,7 @@ mod minimum_vertex_cover { MinimumVertexCover::new(SimpleGraph::new(3, vec![(0, 1), (1, 2)]), vec![1i32; 3]); let solver = BruteForce::new(); - let solutions = solver.find_all_best(&problem); + let solutions = solver.find_all_witnesses(&problem); assert_eq!(solutions.len(), 1); assert_eq!(solutions[0], vec![0, 1, 0]); } @@ -290,7 +290,7 @@ mod minimum_vertex_cover { ); let solver = BruteForce::new(); - let solutions = solver.find_all_best(&problem); + let solutions = solver.find_all_witnesses(&problem); // There are 3 minimum covers of size 2 assert_eq!(solutions.len(), 3); for sol in &solutions { @@ -307,7 +307,7 @@ mod minimum_vertex_cover { MinimumVertexCover::new(SimpleGraph::new(3, vec![(0, 1), (1, 2)]), vec![100, 1, 100]); let solver = BruteForce::new(); - let solutions = solver.find_all_best(&problem); + let solutions = solver.find_all_witnesses(&problem); assert_eq!(solutions.len(), 1); // Should select vertex 1 (weight 1) instead of 0 and 2 (total 200) assert_eq!(solutions[0], vec![0, 1, 0]); @@ -336,7 +336,7 @@ mod minimum_vertex_cover { #[test] fn test_direction() { let problem = MinimumVertexCover::new(SimpleGraph::new(3, vec![(0, 1)]), vec![1i32; 3]); - assert_eq!(problem.direction(), Direction::Minimize); + assert_eq!(problem.direction(), ExtremumSense::Minimize); } #[test] @@ -344,7 +344,7 @@ mod minimum_vertex_cover { let problem = MinimumVertexCover::new(SimpleGraph::new(3, vec![]), vec![1i32; 3]); let solver = BruteForce::new(); - let solutions = solver.find_all_best(&problem); + let solutions = solver.find_all_witnesses(&problem); // No edges means empty cover is valid and optimal assert_eq!(solutions.len(), 1); assert_eq!(solutions[0], vec![0, 0, 0]); @@ -355,7 +355,7 @@ mod minimum_vertex_cover { let problem = MinimumVertexCover::new(SimpleGraph::new(2, vec![(0, 1)]), vec![1i32; 2]); let solver = BruteForce::new(); - let solutions = solver.find_all_best(&problem); + let solutions = solver.find_all_witnesses(&problem); // Either vertex covers the single edge assert_eq!(solutions.len(), 2); } @@ -369,8 +369,8 @@ mod minimum_vertex_cover { assert!(problem.evaluate(&[0, 1, 0]).is_valid()); assert!(problem.evaluate(&[1, 0, 1]).is_valid()); // Invalid configurations return Invalid - assert_eq!(problem.evaluate(&[1, 0, 0]), SolutionSize::Invalid); - assert_eq!(problem.evaluate(&[0, 0, 1]), SolutionSize::Invalid); + assert_eq!(problem.evaluate(&[1, 0, 0]), Min(None)); + assert_eq!(problem.evaluate(&[0, 0, 1]), Min(None)); } #[test] @@ -383,7 +383,7 @@ mod minimum_vertex_cover { let solver = BruteForce::new(); - let is_solutions = solver.find_all_best(&is_problem); + let is_solutions = solver.find_all_witnesses(&is_problem); for is_sol in &is_solutions { // Complement should be a valid vertex cover let vc_config: Vec = is_sol.iter().map(|&x| 1 - x).collect(); @@ -490,7 +490,7 @@ mod kcoloring { let problem = KColoring::::new(SimpleGraph::new(4, vec![(0, 1), (1, 2), (2, 3)])); let solver = BruteForce::new(); - let solutions = solver.find_all_satisfying(&problem); + let solutions = solver.find_all_witnesses(&problem); // All solutions should be valid for sol in &solutions { assert!(problem.evaluate(sol)); @@ -503,7 +503,7 @@ mod kcoloring { let problem = KColoring::::new(SimpleGraph::new(3, vec![(0, 1), (1, 2), (0, 2)])); let solver = BruteForce::new(); - let solutions = solver.find_all_satisfying(&problem); + let solutions = solver.find_all_witnesses(&problem); for sol in &solutions { assert!(problem.evaluate(sol)); // All three vertices have different colors @@ -520,7 +520,7 @@ mod kcoloring { let solver = BruteForce::new(); // No satisfying assignments - let solution = solver.find_satisfying(&problem); + let solution = solver.find_witness(&problem); assert!(solution.is_none()); } @@ -547,7 +547,7 @@ mod kcoloring { let problem = KColoring::::new(SimpleGraph::new(3, vec![])); let solver = BruteForce::new(); - let solutions = solver.find_all_satisfying(&problem); + let solutions = solver.find_all_witnesses(&problem); // Any coloring is valid when there are no edges assert!(problem.evaluate(&solutions[0])); } @@ -561,7 +561,7 @@ mod kcoloring { )); let solver = BruteForce::new(); - let solutions = solver.find_all_satisfying(&problem); + let solutions = solver.find_all_witnesses(&problem); for sol in &solutions { assert!(problem.evaluate(sol)); } diff --git a/src/unit_tests/models/algebraic/bmf.rs b/src/unit_tests/models/algebraic/bmf.rs index dac5a34be..8b7dc13d0 100644 --- a/src/unit_tests/models/algebraic/bmf.rs +++ b/src/unit_tests/models/algebraic/bmf.rs @@ -1,7 +1,7 @@ use super::*; -use crate::solvers::{BruteForce, Solver}; -use crate::traits::{OptimizationProblem, Problem}; -use crate::types::{Direction, SolutionSize}; +use crate::solvers::BruteForce; +use crate::traits::{ObjectiveProblem, Problem}; +use crate::types::{ExtremumSense, Min}; include!("../../jl_helpers.rs"); @@ -80,11 +80,11 @@ fn test_evaluate() { // Exact factorization -> distance 0 let config = vec![1, 0, 0, 1, 1, 0, 0, 1]; - assert_eq!(Problem::evaluate(&problem, &config), SolutionSize::Valid(0)); + assert_eq!(Problem::evaluate(&problem, &config), Min(Some(0))); // Non-exact -> distance 2 let config = vec![0, 0, 0, 0, 0, 0, 0, 0]; - assert_eq!(Problem::evaluate(&problem, &config), SolutionSize::Valid(2)); + assert_eq!(Problem::evaluate(&problem, &config), Min(Some(2))); } #[test] @@ -94,10 +94,10 @@ fn test_brute_force_ones() { let problem = BMF::new(matrix, 1); let solver = BruteForce::new(); - let solutions = solver.find_all_best(&problem); + let solutions = solver.find_all_witnesses(&problem); for sol in &solutions { // Exact factorization has distance 0 - assert_eq!(Problem::evaluate(&problem, sol), SolutionSize::Valid(0)); + assert_eq!(Problem::evaluate(&problem, sol), Min(Some(0))); } } @@ -108,7 +108,7 @@ fn test_brute_force_identity() { let problem = BMF::new(matrix, 2); let solver = BruteForce::new(); - let solutions = solver.find_all_best(&problem); + let solutions = solver.find_all_witnesses(&problem); // Should find exact factorization for sol in &solutions { assert!(problem.is_exact(sol)); @@ -122,7 +122,7 @@ fn test_brute_force_insufficient_rank() { let problem = BMF::new(matrix, 1); let solver = BruteForce::new(); - let solutions = solver.find_all_best(&problem); + let solutions = solver.find_all_witnesses(&problem); // Best approximation has distance > 0 let best_distance = problem.hamming_distance(&solutions[0]); // With rank 1, best we can do is distance 1 (all ones or all zeros except one) @@ -151,7 +151,7 @@ fn test_matrix_hamming_distance_function() { fn test_direction() { let matrix = vec![vec![true]]; let problem = BMF::new(matrix, 1); - assert_eq!(problem.direction(), Direction::Minimize); + assert_eq!(problem.direction(), ExtremumSense::Minimize); } #[test] @@ -160,7 +160,7 @@ fn test_empty_matrix() { let problem = BMF::new(matrix, 1); assert_eq!(problem.num_variables(), 0); // Empty matrix has distance 0 - assert_eq!(Problem::evaluate(&problem, &[]), SolutionSize::Valid(0)); + assert_eq!(Problem::evaluate(&problem, &[]), Min(Some(0))); } #[test] @@ -173,8 +173,8 @@ fn test_is_exact() { #[test] fn test_bmf_problem() { - use crate::traits::{OptimizationProblem, Problem}; - use crate::types::Direction; + use crate::traits::{ObjectiveProblem, Problem}; + use crate::types::ExtremumSense; // 2x2 identity matrix with rank 2 let matrix = vec![vec![true, false], vec![false, true]]; @@ -187,24 +187,24 @@ fn test_bmf_problem() { // Config: [1,0,0,1, 1,0,0,1] assert_eq!( Problem::evaluate(&problem, &[1, 0, 0, 1, 1, 0, 0, 1]), - SolutionSize::Valid(0) + Min(Some(0)) ); // All zeros -> product is all zeros, distance = 2 assert_eq!( Problem::evaluate(&problem, &[0, 0, 0, 0, 0, 0, 0, 0]), - SolutionSize::Valid(2) + Min(Some(2)) ); - // Direction is minimize - assert_eq!(problem.direction(), Direction::Minimize); + // ExtremumSense is minimize + assert_eq!(problem.direction(), ExtremumSense::Minimize); // Test with 1x1 matrix let matrix = vec![vec![true]]; let problem = BMF::new(matrix, 1); assert_eq!(problem.dims(), vec![2; 2]); // B(1*1) + C(1*1) - assert_eq!(Problem::evaluate(&problem, &[1, 1]), SolutionSize::Valid(0)); // Exact - assert_eq!(Problem::evaluate(&problem, &[0, 0]), SolutionSize::Valid(1)); // Distance 1 + assert_eq!(Problem::evaluate(&problem, &[1, 1]), Min(Some(0))); // Exact + assert_eq!(Problem::evaluate(&problem, &[0, 0]), Min(Some(1))); // Distance 1 } #[test] @@ -232,12 +232,12 @@ fn test_jl_parity_evaluation() { // BMF always returns Valid(hamming_distance) assert_eq!( result, - SolutionSize::Valid(jl_size), + Min(Some(jl_size)), "BMF: size mismatch for config {:?}", config ); } - let best = BruteForce::new().find_all_best(&problem); + let best = BruteForce::new().find_all_witnesses(&problem); let jl_best = jl_parse_configs_set(&instance["best_solutions"]); let rust_best: HashSet> = best.into_iter().collect(); assert_eq!(rust_best, jl_best, "BMF best solutions mismatch"); @@ -269,9 +269,9 @@ fn test_bmf_paper_example() { // C: c00=1,c01=1,c02=0, c10=0,c11=1,c12=1 let config = vec![1, 0, 1, 1, 0, 1, 1, 1, 0, 0, 1, 1]; let result = Problem::evaluate(&problem, &config); - assert_eq!(result, SolutionSize::Valid(0)); // exact factorization + assert_eq!(result, Min(Some(0))); // exact factorization let solver = BruteForce::new(); - let best = solver.find_best(&problem).unwrap(); - assert_eq!(Problem::evaluate(&problem, &best), SolutionSize::Valid(0)); + let best = solver.find_witness(&problem).unwrap(); + assert_eq!(Problem::evaluate(&problem, &best), Min(Some(0))); } diff --git a/src/unit_tests/models/algebraic/closest_vector_problem.rs b/src/unit_tests/models/algebraic/closest_vector_problem.rs index d4b2cd5ff..5025ec06d 100644 --- a/src/unit_tests/models/algebraic/closest_vector_problem.rs +++ b/src/unit_tests/models/algebraic/closest_vector_problem.rs @@ -1,7 +1,7 @@ use super::*; -use crate::solvers::{BruteForce, Solver}; -use crate::traits::{OptimizationProblem, Problem}; -use crate::types::{Direction, SolutionSize}; +use crate::solvers::BruteForce; +use crate::traits::{ObjectiveProblem, Problem}; +use crate::types::{ExtremumSense, Min}; #[test] fn test_cvp_creation() { @@ -35,7 +35,7 @@ fn test_cvp_evaluate() { // config offset: x_i - lower = 1 - (-2) = 3 let config_111 = vec![3, 3, 3]; // maps to x=(1,1,1) let result = Problem::evaluate(&cvp, &config_111); - assert_eq!(result, SolutionSize::Valid(1.0)); + assert_eq!(result, Min(Some(1.0))); } #[test] @@ -44,7 +44,7 @@ fn test_cvp_direction() { let target = vec![0.5, 0.5]; let bounds = vec![VarBounds::bounded(0, 2), VarBounds::bounded(0, 2)]; let cvp = ClosestVectorProblem::new(basis, target, bounds); - assert_eq!(cvp.direction(), Direction::Minimize); + assert_eq!(cvp.direction(), ExtremumSense::Minimize); } #[test] @@ -103,14 +103,14 @@ fn test_cvp_brute_force() { let cvp = ClosestVectorProblem::new(basis, target, bounds); let solver = BruteForce::new(); - let solution = solver.find_best(&cvp).expect("should find a solution"); + let solution = solver.find_witness(&cvp).expect("should find a solution"); let values: Vec = solution .iter() .enumerate() .map(|(i, &c)| cvp.bounds()[i].lower.unwrap() + c as i64) .collect(); assert_eq!(values, vec![1, 1, 1]); - assert_eq!(Problem::evaluate(&cvp, &solution), SolutionSize::Valid(1.0)); + assert_eq!(Problem::evaluate(&cvp, &solution), Min(Some(1.0))); } #[test] @@ -145,7 +145,7 @@ fn test_cvp_f64_basis() { let cvp = ClosestVectorProblem::new(basis, target, bounds); let solver = BruteForce::new(); - let solution = solver.find_best(&cvp).expect("should find a solution"); + let solution = solver.find_witness(&cvp).expect("should find a solution"); let values: Vec = solution .iter() .enumerate() @@ -169,7 +169,7 @@ fn test_cvp_2d_identity() { let cvp = ClosestVectorProblem::new(basis, target, bounds); let solver = BruteForce::new(); - let solution = solver.find_best(&cvp).expect("should find a solution"); + let solution = solver.find_witness(&cvp).expect("should find a solution"); let values: Vec = solution .iter() .enumerate() @@ -189,7 +189,7 @@ fn test_cvp_evaluate_exact_solution() { // x=(2,2), Bx=(2,2), distance=0 let config = vec![2, 2]; // offset from lower=0 let result = Problem::evaluate(&cvp, &config); - assert_eq!(result, SolutionSize::Valid(0.0)); + assert_eq!(result, Min(Some(0.0))); } #[test] @@ -228,7 +228,7 @@ fn test_cvp_paper_example() { assert!((dist - 0.29_f64.sqrt()).abs() < 1e-10); let solver = BruteForce::new(); - let best = solver.find_best(&cvp).unwrap(); + let best = solver.find_witness(&cvp).unwrap(); let best_dist = Problem::evaluate(&cvp, &best).unwrap(); assert!((best_dist - 0.29_f64.sqrt()).abs() < 1e-10); } diff --git a/src/unit_tests/models/algebraic/consecutive_block_minimization.rs b/src/unit_tests/models/algebraic/consecutive_block_minimization.rs index 7cb9e5d2f..60fbba417 100644 --- a/src/unit_tests/models/algebraic/consecutive_block_minimization.rs +++ b/src/unit_tests/models/algebraic/consecutive_block_minimization.rs @@ -60,7 +60,7 @@ fn test_consecutive_block_minimization_brute_force() { 2, ); let solver = BruteForce::new(); - let mut solutions = solver.find_all_satisfying(&problem); + let mut solutions = solver.find_all_witnesses(&problem); solutions.sort(); let mut expected = vec![vec![0, 2, 1], vec![1, 2, 0]]; expected.sort(); diff --git a/src/unit_tests/models/algebraic/consecutive_ones_submatrix.rs b/src/unit_tests/models/algebraic/consecutive_ones_submatrix.rs index d245c3fcb..e1efe8ca5 100644 --- a/src/unit_tests/models/algebraic/consecutive_ones_submatrix.rs +++ b/src/unit_tests/models/algebraic/consecutive_ones_submatrix.rs @@ -1,5 +1,5 @@ use super::*; -use crate::solvers::{BruteForce, Solver}; +use crate::solvers::BruteForce; use crate::traits::Problem; /// Tucker matrix (3×4) — the classic C1P obstruction. @@ -70,7 +70,7 @@ fn test_consecutive_ones_submatrix_brute_force() { let problem = ConsecutiveOnesSubmatrix::new(tucker_matrix(), 3); let solver = BruteForce::new(); let solution = solver - .find_satisfying(&problem) + .find_witness(&problem) .expect("should find a solution"); assert!(problem.evaluate(&solution)); } @@ -79,7 +79,7 @@ fn test_consecutive_ones_submatrix_brute_force() { fn test_consecutive_ones_submatrix_brute_force_all() { let problem = ConsecutiveOnesSubmatrix::new(tucker_matrix(), 3); let solver = BruteForce::new(); - let solutions = solver.find_all_satisfying(&problem); + let solutions = solver.find_all_witnesses(&problem); assert!(!solutions.is_empty()); for sol in &solutions { assert!(problem.evaluate(sol)); @@ -91,7 +91,7 @@ fn test_consecutive_ones_submatrix_unsatisfiable() { // Tucker matrix with K=4: no permutation of all 4 columns gives C1P let problem = ConsecutiveOnesSubmatrix::new(tucker_matrix(), 4); let solver = BruteForce::new(); - assert!(solver.find_satisfying(&problem).is_none()); + assert!(solver.find_witness(&problem).is_none()); } #[test] @@ -104,9 +104,7 @@ fn test_consecutive_ones_submatrix_trivial_c1p() { ]; let problem = ConsecutiveOnesSubmatrix::new(matrix, 3); let solver = BruteForce::new(); - let solution = solver - .find_satisfying(&problem) - .expect("full matrix has C1P"); + let solution = solver.find_witness(&problem).expect("full matrix has C1P"); assert!(problem.evaluate(&solution)); } @@ -116,7 +114,7 @@ fn test_consecutive_ones_submatrix_single_column() { let matrix = vec![vec![true, false, true], vec![false, true, false]]; let problem = ConsecutiveOnesSubmatrix::new(matrix, 1); let solver = BruteForce::new(); - let solutions = solver.find_all_satisfying(&problem); + let solutions = solver.find_all_witnesses(&problem); assert_eq!(solutions.len(), 3); // each column works individually } @@ -130,7 +128,7 @@ fn test_consecutive_ones_submatrix_empty_rows() { ]; let problem = ConsecutiveOnesSubmatrix::new(matrix, 2); let solver = BruteForce::new(); - let solutions = solver.find_all_satisfying(&problem); + let solutions = solver.find_all_witnesses(&problem); assert!(!solutions.is_empty()); for sol in &solutions { assert!(problem.evaluate(sol)); @@ -166,7 +164,7 @@ fn test_consecutive_ones_submatrix_paper_example() { assert!(problem.evaluate(&[1, 1, 0, 1])); let solver = BruteForce::new(); - let solutions = solver.find_all_satisfying(&problem); + let solutions = solver.find_all_witnesses(&problem); // All solutions must be valid for sol in &solutions { assert!(problem.evaluate(sol)); diff --git a/src/unit_tests/models/algebraic/ilp.rs b/src/unit_tests/models/algebraic/ilp.rs index 91f60cc27..2c6efec52 100644 --- a/src/unit_tests/models/algebraic/ilp.rs +++ b/src/unit_tests/models/algebraic/ilp.rs @@ -1,7 +1,7 @@ use super::*; use crate::solvers::BruteForce; -use crate::traits::{OptimizationProblem, Problem}; -use crate::types::{Direction, SolutionSize}; +use crate::traits::{ObjectiveProblem, Problem}; +use crate::types::{Extremum, ExtremumSense}; // ============================================================ // Comparison tests @@ -99,11 +99,11 @@ fn test_linear_constraint_out_of_bounds() { #[test] fn test_objective_sense_direction_conversions() { - // Test that ObjectiveSense and Direction can be converted + // Test that ObjectiveSense and ExtremumSense can be converted let max_sense = ObjectiveSense::Maximize; let min_sense = ObjectiveSense::Minimize; - // Direction values match ObjectiveSense semantics + // ExtremumSense values match ObjectiveSense semantics assert_eq!(max_sense, ObjectiveSense::Maximize); assert_eq!(min_sense, ObjectiveSense::Minimize); } @@ -194,8 +194,8 @@ fn test_ilp_direction() { let max_ilp = ILP::::new(2, vec![], vec![], ObjectiveSense::Maximize); let min_ilp = ILP::::new(2, vec![], vec![], ObjectiveSense::Minimize); - assert_eq!(max_ilp.direction(), Direction::Maximize); - assert_eq!(min_ilp.direction(), Direction::Minimize); + assert_eq!(max_ilp.direction(), ExtremumSense::Maximize); + assert_eq!(min_ilp.direction(), ExtremumSense::Minimize); } #[test] @@ -209,10 +209,16 @@ fn test_ilp_evaluate_valid() { ); // Config [0, 1] means x0=0, x1=1 => obj = 2, valid - assert_eq!(Problem::evaluate(&ilp, &[0, 1]), SolutionSize::Valid(2.0)); + assert_eq!( + Problem::evaluate(&ilp, &[0, 1]), + Extremum::maximize(Some(2.0)) + ); // Config [1, 0] means x0=1, x1=0 => obj = 1, valid - assert_eq!(Problem::evaluate(&ilp, &[1, 0]), SolutionSize::Valid(1.0)); + assert_eq!( + Problem::evaluate(&ilp, &[1, 0]), + Extremum::maximize(Some(1.0)) + ); } #[test] @@ -226,7 +232,7 @@ fn test_ilp_evaluate_invalid() { ); // Config [1, 1] means x0=1, x1=1 => invalid (1+1 > 1), returns Invalid - assert_eq!(Problem::evaluate(&ilp, &[1, 1]), SolutionSize::Invalid); + assert_eq!(Problem::evaluate(&ilp, &[1, 1]), Extremum::maximize(None)); } #[test] @@ -240,7 +246,7 @@ fn test_ilp_brute_force_maximization() { ); let solver = BruteForce::new(); - let solutions = solver.find_all_best(&ilp); + let solutions = solver.find_all_witnesses(&ilp); // Optimal: x1=1, x0=0 => objective = 2 assert_eq!(solutions.len(), 1); @@ -258,12 +264,12 @@ fn test_ilp_brute_force_minimization() { ); let solver = BruteForce::new(); - let solutions = solver.find_all_best(&ilp); + let solutions = solver.find_all_witnesses(&ilp); // Optimal: x0=1,x1=0 or x0=0,x1=1 => objective = 1 assert_eq!(solutions.len(), 2); for sol in &solutions { - assert_eq!(Problem::evaluate(&ilp, sol), SolutionSize::Valid(1.0)); + assert_eq!(Problem::evaluate(&ilp, sol), Extremum::minimize(Some(1.0))); } } @@ -281,7 +287,7 @@ fn test_ilp_brute_force_no_feasible() { ); let solver = BruteForce::new(); - let solutions = solver.find_all_best(&ilp); + let solutions = solver.find_all_witnesses(&ilp); // All solutions are infeasible - BruteForce should return empty list assert!( @@ -291,7 +297,7 @@ fn test_ilp_brute_force_no_feasible() { // Verify all configs are indeed infeasible for config in &[[0], [1]] { - assert_eq!(Problem::evaluate(&ilp, config), SolutionSize::Invalid); + assert_eq!(Problem::evaluate(&ilp, config), Extremum::minimize(None)); let values = ilp.config_to_values(config); assert!(!ilp.is_feasible(&values)); } @@ -308,7 +314,7 @@ fn test_ilp_unconstrained() { ); let solver = BruteForce::new(); - let solutions = solver.find_all_best(&ilp); + let solutions = solver.find_all_witnesses(&ilp); // Optimal: both = 1 assert_eq!(solutions.len(), 1); @@ -326,7 +332,7 @@ fn test_ilp_equality_constraint() { ); let solver = BruteForce::new(); - let solutions = solver.find_all_best(&ilp); + let solutions = solver.find_all_witnesses(&ilp); // Optimal: x0=0, x1=1 => objective = 0 assert_eq!(solutions.len(), 1); @@ -350,7 +356,7 @@ fn test_ilp_multiple_constraints() { ); let solver = BruteForce::new(); - let solutions = solver.find_all_best(&ilp); + let solutions = solver.find_all_witnesses(&ilp); // Optimal: x0=1, x1=0, x2=1 => objective = 2 assert_eq!(solutions.len(), 1); @@ -379,15 +385,24 @@ fn test_ilp_problem() { assert_eq!(ilp.dims(), vec![2, 2]); // [0, 0] -> feasible, obj = 0 - assert_eq!(Problem::evaluate(&ilp, &[0, 0]), SolutionSize::Valid(0.0)); + assert_eq!( + Problem::evaluate(&ilp, &[0, 0]), + Extremum::maximize(Some(0.0)) + ); // [0, 1] -> feasible, obj = 2 - assert_eq!(Problem::evaluate(&ilp, &[0, 1]), SolutionSize::Valid(2.0)); + assert_eq!( + Problem::evaluate(&ilp, &[0, 1]), + Extremum::maximize(Some(2.0)) + ); // [1, 0] -> feasible, obj = 1 - assert_eq!(Problem::evaluate(&ilp, &[1, 0]), SolutionSize::Valid(1.0)); + assert_eq!( + Problem::evaluate(&ilp, &[1, 0]), + Extremum::maximize(Some(1.0)) + ); // [1, 1] -> infeasible - assert_eq!(Problem::evaluate(&ilp, &[1, 1]), SolutionSize::Invalid); + assert_eq!(Problem::evaluate(&ilp, &[1, 1]), Extremum::maximize(None)); - assert_eq!(ilp.direction(), Direction::Maximize); + assert_eq!(ilp.direction(), ExtremumSense::Maximize); } #[test] @@ -399,9 +414,15 @@ fn test_ilp_problem_minimize() { vec![(0, 1.0), (1, 1.0)], ObjectiveSense::Minimize, ); - assert_eq!(Problem::evaluate(&ilp, &[0, 0]), SolutionSize::Valid(0.0)); - assert_eq!(Problem::evaluate(&ilp, &[1, 1]), SolutionSize::Valid(2.0)); - assert_eq!(ilp.direction(), Direction::Minimize); + assert_eq!( + Problem::evaluate(&ilp, &[0, 0]), + Extremum::minimize(Some(0.0)) + ); + assert_eq!( + Problem::evaluate(&ilp, &[1, 1]), + Extremum::minimize(Some(2.0)) + ); + assert_eq!(ilp.direction(), ExtremumSense::Minimize); } #[test] @@ -443,7 +464,7 @@ fn test_ilp_paper_example() { // Verify optimal solution x* = (3, 2) → config [3, 2] let result = Problem::evaluate(&ilp, &[3, 2]); - assert_eq!(result, SolutionSize::Valid(-27.0)); + assert_eq!(result, Extremum::minimize(Some(-27.0))); // Verify feasibility: 3+2=5≤5, 4*3+7*2=26≤28 assert!(ilp.is_feasible(&[3, 2])); @@ -453,5 +474,5 @@ fn test_ilp_paper_example() { // Verify suboptimal feasible point: -5*0 - 6*4 = -24 > -27 let result2 = Problem::evaluate(&ilp, &[0, 4]); - assert_eq!(result2, SolutionSize::Valid(-24.0)); + assert_eq!(result2, Extremum::minimize(Some(-24.0))); } diff --git a/src/unit_tests/models/algebraic/quadratic_assignment.rs b/src/unit_tests/models/algebraic/quadratic_assignment.rs index 703321938..58c7bd596 100644 --- a/src/unit_tests/models/algebraic/quadratic_assignment.rs +++ b/src/unit_tests/models/algebraic/quadratic_assignment.rs @@ -1,7 +1,7 @@ use super::*; -use crate::solvers::{BruteForce, Solver}; -use crate::traits::{OptimizationProblem, Problem}; -use crate::types::{Direction, SolutionSize}; +use crate::solvers::BruteForce; +use crate::traits::{ObjectiveProblem, Problem}; +use crate::types::{ExtremumSense, Min}; /// Create a 4x4 test instance matching issue #300's example. /// @@ -51,10 +51,7 @@ fn test_quadratic_assignment_evaluate_identity() { // cost = sum_{i != j} C[i][j] * D[i][j] // = 5*4 + 2*1 + 0*1 + 5*4 + 0*3 + 3*4 + 2*1 + 0*3 + 4*4 + 0*1 + 3*4 + 4*4 // = 20 + 2 + 0 + 20 + 0 + 12 + 2 + 0 + 16 + 0 + 12 + 16 = 100 - assert_eq!( - Problem::evaluate(&qap, &[0, 1, 2, 3]), - SolutionSize::Valid(100) - ); + assert_eq!(Problem::evaluate(&qap, &[0, 1, 2, 3]), Min(Some(100))); } #[test] @@ -67,38 +64,26 @@ fn test_quadratic_assignment_evaluate_swap() { // i=2,j=0: 2*D[1][0]=2*4=8 i=2,j=1: 0*D[1][2]=0*3=0 i=2,j=3: 4*D[1][3]=4*4=16 // i=3,j=0: 0*D[3][0]=0 i=3,j=1: 3*D[3][2]=3*4=12 i=3,j=2: 4*D[3][1]=4*4=16 // Total = 5+8+0+5+0+12+8+0+16+0+12+16 = 82 - assert_eq!( - Problem::evaluate(&qap, &[0, 2, 1, 3]), - SolutionSize::Valid(82) - ); + assert_eq!(Problem::evaluate(&qap, &[0, 2, 1, 3]), Min(Some(82))); } #[test] fn test_quadratic_assignment_evaluate_invalid() { let qap = make_test_instance(); // Duplicate location 0 — not injective, should be Invalid. - assert_eq!( - Problem::evaluate(&qap, &[0, 0, 1, 2]), - SolutionSize::Invalid - ); + assert_eq!(Problem::evaluate(&qap, &[0, 0, 1, 2]), Min(None)); // Out-of-range location index. - assert_eq!( - Problem::evaluate(&qap, &[0, 1, 2, 99]), - SolutionSize::Invalid - ); + assert_eq!(Problem::evaluate(&qap, &[0, 1, 2, 99]), Min(None)); // Wrong config length — too short. - assert_eq!(Problem::evaluate(&qap, &[0, 1, 2]), SolutionSize::Invalid); + assert_eq!(Problem::evaluate(&qap, &[0, 1, 2]), Min(None)); // Wrong config length — too long. - assert_eq!( - Problem::evaluate(&qap, &[0, 1, 2, 3, 0]), - SolutionSize::Invalid - ); + assert_eq!(Problem::evaluate(&qap, &[0, 1, 2, 3, 0]), Min(None)); } #[test] fn test_quadratic_assignment_direction() { let qap = make_test_instance(); - assert_eq!(qap.direction(), Direction::Minimize); + assert_eq!(qap.direction(), ExtremumSense::Minimize); } #[test] @@ -125,13 +110,13 @@ fn test_quadratic_assignment_rectangular() { assert_eq!(qap.num_locations(), 3); assert_eq!(qap.dims(), 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, &[0, 1]), SolutionSize::Valid(6)); + assert_eq!(Problem::evaluate(&qap, &[0, 1]), Min(Some(6))); // Assignment f=(0,2): cost = 3*D[0][2] + 3*D[2][0] = 3*4 + 3*4 = 24 - assert_eq!(Problem::evaluate(&qap, &[0, 2]), SolutionSize::Valid(24)); + assert_eq!(Problem::evaluate(&qap, &[0, 2]), Min(Some(24))); // BruteForce should find optimal let solver = BruteForce::new(); - let best = solver.find_best(&qap).unwrap(); - assert_eq!(Problem::evaluate(&qap, &best), SolutionSize::Valid(6)); + let best = solver.find_witness(&qap).unwrap(); + assert_eq!(Problem::evaluate(&qap, &best), Min(Some(6))); } #[test] @@ -153,12 +138,9 @@ fn test_quadratic_assignment_too_many_facilities() { fn test_quadratic_assignment_solver() { let qap = make_test_instance(); let solver = BruteForce::new(); - let best = solver.find_best(&qap); + let best = solver.find_witness(&qap); assert!(best.is_some()); let best_config = best.unwrap(); // The brute-force solver finds the optimal assignment f* = (3, 0, 1, 2) with cost 56. - assert_eq!( - Problem::evaluate(&qap, &best_config), - SolutionSize::Valid(56) - ); + assert_eq!(Problem::evaluate(&qap, &best_config), Min(Some(56))); } diff --git a/src/unit_tests/models/algebraic/qubo.rs b/src/unit_tests/models/algebraic/qubo.rs index d33f5b958..c73febbf8 100644 --- a/src/unit_tests/models/algebraic/qubo.rs +++ b/src/unit_tests/models/algebraic/qubo.rs @@ -1,7 +1,7 @@ use super::*; -use crate::solvers::{BruteForce, Solver}; -use crate::traits::{OptimizationProblem, Problem}; -use crate::types::{Direction, SolutionSize}; +use crate::solvers::BruteForce; +use crate::traits::{ObjectiveProblem, Problem}; +use crate::types::{ExtremumSense, Min}; include!("../../jl_helpers.rs"); #[test] @@ -24,7 +24,7 @@ fn test_qubo_new() { #[test] fn test_direction() { let problem = QUBO::::from_matrix(vec![vec![1.0]]); - assert_eq!(problem.direction(), Direction::Minimize); + assert_eq!(problem.direction(), ExtremumSense::Minimize); } #[test] @@ -49,7 +49,7 @@ fn test_matrix_access() { fn test_empty_qubo() { let problem = QUBO::::from_matrix(vec![]); assert_eq!(problem.num_vars(), 0); - assert_eq!(Problem::evaluate(&problem, &[]), SolutionSize::Valid(0.0)); + assert_eq!(Problem::evaluate(&problem, &[]), Min(Some(0.0))); } #[test] @@ -94,7 +94,7 @@ fn test_jl_parity_evaluation() { let problem = QUBO::from_matrix(rust_matrix); for eval in instance["evaluations"].as_array().unwrap() { let config = jl_parse_config(&eval["config"]); - let result: SolutionSize = Problem::evaluate(&problem, &config); + let result = Problem::evaluate(&problem, &config); let jl_size = eval["size"].as_f64().unwrap(); assert!(result.is_valid(), "QUBO should always be valid"); assert!( @@ -103,7 +103,7 @@ fn test_jl_parity_evaluation() { config ); } - let best = BruteForce::new().find_all_best(&problem); + let best = BruteForce::new().find_all_witnesses(&problem); let jl_best = jl_parse_configs_set(&instance["best_solutions"]); let rust_best: HashSet> = best.into_iter().collect(); assert_eq!(rust_best, jl_best, "QUBO best solutions mismatch"); @@ -118,15 +118,9 @@ fn test_qubo_paper_example() { vec![0.0, -1.0, 2.0], vec![0.0, 0.0, -1.0], ]); - assert_eq!( - Problem::evaluate(&problem, &[1, 0, 1]), - SolutionSize::Valid(-2.0) - ); + assert_eq!(Problem::evaluate(&problem, &[1, 0, 1]), Min(Some(-2.0))); let solver = BruteForce::new(); - let best = solver.find_best(&problem).unwrap(); - assert_eq!( - Problem::evaluate(&problem, &best), - SolutionSize::Valid(-2.0) - ); + let best = solver.find_witness(&problem).unwrap(); + assert_eq!(Problem::evaluate(&problem, &best), Min(Some(-2.0))); } diff --git a/src/unit_tests/models/algebraic/sparse_matrix_compression.rs b/src/unit_tests/models/algebraic/sparse_matrix_compression.rs index a6095d630..2d3b48630 100644 --- a/src/unit_tests/models/algebraic/sparse_matrix_compression.rs +++ b/src/unit_tests/models/algebraic/sparse_matrix_compression.rs @@ -1,6 +1,6 @@ use super::*; use crate::registry::VariantEntry; -use crate::solvers::{BruteForce, Solver}; +use crate::solvers::BruteForce; use crate::traits::Problem; fn issue_example_matrix() -> Vec> { @@ -67,11 +67,11 @@ fn test_sparse_matrix_compression_bruteforce_finds_unique_solution() { let solver = BruteForce::new(); let solution = solver - .find_satisfying(&problem) + .find_witness(&problem) .expect("issue example should be satisfiable"); assert_eq!(solution, vec![1, 1, 1, 0]); - let all = solver.find_all_satisfying(&problem); + let all = solver.find_all_witnesses(&problem); assert_eq!(all, vec![vec![1, 1, 1, 0]]); } diff --git a/src/unit_tests/models/formula/circuit.rs b/src/unit_tests/models/formula/circuit.rs index 81a9e1899..137829ba9 100644 --- a/src/unit_tests/models/formula/circuit.rs +++ b/src/unit_tests/models/formula/circuit.rs @@ -156,7 +156,7 @@ fn test_circuit_sat_brute_force() { let problem = CircuitSAT::new(circuit); let solver = BruteForce::new(); - let solutions = solver.find_all_satisfying(&problem); + let solutions = solver.find_all_witnesses(&problem); // All satisfying: c matches x AND y // 4 valid configs: (0,0,0), (0,0,1), (0,1,0), (1,1,1) assert_eq!(solutions.len(), 4); @@ -182,7 +182,7 @@ fn test_circuit_sat_complex() { let problem = CircuitSAT::new(circuit); let solver = BruteForce::new(); - let solutions = solver.find_all_satisfying(&problem); + let solutions = solver.find_all_witnesses(&problem); // All valid solutions satisfy both assignments for sol in &solutions { assert!(problem.evaluate(sol)); @@ -290,6 +290,6 @@ fn test_circuit_sat_paper_example() { // All 4 consistent configs are satisfying (CircuitSAT checks consistency) let solver = BruteForce::new(); - let all = solver.find_all_satisfying(&problem); + let all = solver.find_all_witnesses(&problem); assert_eq!(all.len(), 4); } diff --git a/src/unit_tests/models/formula/ksat.rs b/src/unit_tests/models/formula/ksat.rs index ebaa569a8..eaad41707 100644 --- a/src/unit_tests/models/formula/ksat.rs +++ b/src/unit_tests/models/formula/ksat.rs @@ -1,5 +1,5 @@ use super::*; -use crate::solvers::{BruteForce, Solver}; +use crate::solvers::BruteForce; use crate::traits::Problem; use crate::variant::{K2, K3, KN}; include!("../../jl_helpers.rs"); @@ -60,7 +60,7 @@ fn test_3sat_brute_force() { ], ); let solver = BruteForce::new(); - let solutions = solver.find_all_satisfying(&problem); + let solutions = solver.find_all_witnesses(&problem); assert!(!solutions.is_empty()); for sol in &solutions { @@ -173,7 +173,7 @@ fn test_jl_parity_evaluation() { config ); } - let rust_best = BruteForce::new().find_all_satisfying(&problem); + let rust_best = BruteForce::new().find_all_witnesses(&problem); let jl_best = jl_parse_configs_set(&instance["best_solutions"]); let rust_best_set: HashSet> = rust_best.into_iter().collect(); assert_eq!(rust_best_set, jl_best, "KSat best solutions mismatch"); @@ -241,6 +241,6 @@ fn test_ksat_paper_example() { assert!(problem.evaluate(&[1, 0, 1])); let solver = BruteForce::new(); - let solution = solver.find_satisfying(&problem); + let solution = solver.find_witness(&problem); assert!(solution.is_some()); } diff --git a/src/unit_tests/models/formula/nae_satisfiability.rs b/src/unit_tests/models/formula/nae_satisfiability.rs index 9b38f88f5..51fcf6e7e 100644 --- a/src/unit_tests/models/formula/nae_satisfiability.rs +++ b/src/unit_tests/models/formula/nae_satisfiability.rs @@ -1,5 +1,5 @@ use super::*; -use crate::solvers::{BruteForce, Solver}; +use crate::solvers::BruteForce; use crate::traits::Problem; use std::collections::HashSet; @@ -64,7 +64,7 @@ fn test_nae_solver_counts_ten_solutions_for_issue_example() { let problem = issue_problem(); let solver = BruteForce::new(); - let solutions = solver.find_all_satisfying(&problem); + let solutions = solver.find_all_witnesses(&problem); let set: HashSet> = solutions.into_iter().collect(); assert_eq!(set.len(), 10); @@ -78,9 +78,9 @@ fn test_nae_empty_formula_is_trivially_satisfying() { let solver = BruteForce::new(); assert!(problem.evaluate(&[])); - assert_eq!(solver.find_satisfying(&problem), Some(vec![])); + assert_eq!(solver.find_witness(&problem), Some(vec![])); assert_eq!( - solver.find_all_satisfying(&problem), + solver.find_all_witnesses(&problem), vec![Vec::::new()] ); } @@ -139,5 +139,5 @@ fn test_nae_satisfiability_paper_example() { assert!(problem.evaluate(&[0, 0, 0, 1, 1])); assert!(problem.evaluate(&[1, 1, 1, 0, 0])); - assert_eq!(solver.find_all_satisfying(&problem).len(), 10); + assert_eq!(solver.find_all_witnesses(&problem).len(), 10); } diff --git a/src/unit_tests/models/formula/qbf.rs b/src/unit_tests/models/formula/qbf.rs index eccd7d28b..136cc967d 100644 --- a/src/unit_tests/models/formula/qbf.rs +++ b/src/unit_tests/models/formula/qbf.rs @@ -1,5 +1,5 @@ use super::*; -use crate::solvers::{BruteForce, Solver}; +use crate::solvers::BruteForce; use crate::traits::Problem; #[test] @@ -150,7 +150,7 @@ fn test_qbf_solver() { let solver = BruteForce::new(); // With dims()=[], there is exactly one config: []. evaluate([]) = is_true() = true - let solution = solver.find_satisfying(&problem); + let solution = solver.find_witness(&problem); assert!(solution.is_some()); let sol = solution.unwrap(); assert_eq!(sol, Vec::::new()); @@ -167,7 +167,7 @@ fn test_qbf_solver_false() { ); let solver = BruteForce::new(); - let solution = solver.find_satisfying(&problem); + let solution = solver.find_witness(&problem); assert!(solution.is_none()); } @@ -181,7 +181,7 @@ fn test_qbf_solver_all_satisfying() { ); let solver = BruteForce::new(); - let solutions = solver.find_all_satisfying(&problem); + let solutions = solver.find_all_witnesses(&problem); // Only one config exists (the empty config []), and it satisfies assert_eq!(solutions.len(), 1); assert_eq!(solutions[0], Vec::::new()); diff --git a/src/unit_tests/models/formula/sat.rs b/src/unit_tests/models/formula/sat.rs index cd78576f7..29ddad85a 100644 --- a/src/unit_tests/models/formula/sat.rs +++ b/src/unit_tests/models/formula/sat.rs @@ -1,5 +1,5 @@ use super::*; -use crate::solvers::{BruteForce, Solver}; +use crate::solvers::BruteForce; use crate::traits::Problem; include!("../../jl_helpers.rs"); @@ -97,9 +97,9 @@ fn test_empty_formula_zero_vars_solver() { let problem = Satisfiability::new(0, vec![]); let solver = BruteForce::new(); - assert_eq!(solver.find_satisfying(&problem), Some(vec![])); + assert_eq!(solver.find_witness(&problem), Some(vec![])); assert_eq!( - solver.find_all_satisfying(&problem), + solver.find_all_witnesses(&problem), vec![Vec::::new()] ); } @@ -109,8 +109,8 @@ fn test_zero_vars_unsat_solver() { let problem = Satisfiability::new(0, vec![CNFClause::new(vec![1])]); let solver = BruteForce::new(); - assert_eq!(solver.find_satisfying(&problem), None); - assert!(solver.find_all_satisfying(&problem).is_empty()); + assert_eq!(solver.find_witness(&problem), None); + assert!(solver.find_all_witnesses(&problem).is_empty()); } #[test] @@ -119,7 +119,7 @@ fn test_single_literal_clauses() { let problem = Satisfiability::new(2, vec![CNFClause::new(vec![1]), CNFClause::new(vec![-2])]); let solver = BruteForce::new(); - let solutions = solver.find_all_satisfying(&problem); + let solutions = solver.find_all_witnesses(&problem); assert_eq!(solutions.len(), 1); assert_eq!(solutions[0], vec![1, 0]); // x1=T, x2=F } @@ -186,7 +186,7 @@ fn test_jl_parity_evaluation() { config ); } - let rust_best = BruteForce::new().find_all_satisfying(&problem); + let rust_best = BruteForce::new().find_all_witnesses(&problem); let rust_best_set: HashSet> = rust_best.into_iter().collect(); if !rust_best_set.is_empty() { let jl_best = jl_parse_configs_set(&instance["best_solutions"]); @@ -223,6 +223,6 @@ fn test_sat_paper_example() { assert!(problem.evaluate(&[1, 0, 1])); let solver = BruteForce::new(); - let solution = solver.find_satisfying(&problem); + let solution = solver.find_witness(&problem); assert!(solution.is_some()); } diff --git a/src/unit_tests/models/graph/acyclic_partition.rs b/src/unit_tests/models/graph/acyclic_partition.rs index c15582f30..70e1d7df8 100644 --- a/src/unit_tests/models/graph/acyclic_partition.rs +++ b/src/unit_tests/models/graph/acyclic_partition.rs @@ -1,6 +1,6 @@ use super::*; use crate::registry::declared_size_fields; -use crate::solvers::{BruteForce, Solver}; +use crate::solvers::BruteForce; use crate::topology::DirectedGraph; use crate::traits::Problem; use serde_json; @@ -160,7 +160,7 @@ fn test_acyclic_partition_solver_finds_issue_example() { let problem = yes_instance(); let solver = BruteForce::new(); - let solution = solver.find_satisfying(&problem); + let solution = solver.find_witness(&problem); assert!(solution.is_some()); assert!(problem.evaluate(&solution.unwrap())); } @@ -168,7 +168,7 @@ fn test_acyclic_partition_solver_finds_issue_example() { #[test] fn test_acyclic_partition_solver_has_four_canonical_solutions() { let problem = yes_instance(); - let solutions = BruteForce::new().find_all_satisfying(&problem); + let solutions = BruteForce::new().find_all_witnesses(&problem); let normalized: BTreeSet> = solutions .iter() .map(|config| canonicalize_labels(config)) @@ -187,7 +187,7 @@ fn test_acyclic_partition_solver_has_four_canonical_solutions() { #[test] fn test_acyclic_partition_no_solution_when_cost_bound_is_four() { let problem = no_cost_instance(); - assert!(BruteForce::new().find_satisfying(&problem).is_none()); + assert!(BruteForce::new().find_witness(&problem).is_none()); } #[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 843da8994..2faab061f 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,5 @@ use super::*; -use crate::solvers::{BruteForce, Solver}; +use crate::solvers::BruteForce; use crate::topology::BipartiteGraph; use crate::traits::Problem; @@ -102,11 +102,11 @@ fn test_balanced_complete_bipartite_subgraph_solver_yes_instance() { let problem = BalancedCompleteBipartiteSubgraph::new(issue_instance_2_graph(), 3); let solver = BruteForce::new(); - let solution = solver.find_satisfying(&problem); + let solution = solver.find_witness(&problem); assert!(solution.is_some()); assert!(problem.evaluate(&solution.unwrap())); - let all = solver.find_all_satisfying(&problem); + let all = solver.find_all_witnesses(&problem); assert_eq!(all, vec![issue_instance_2_witness()]); } @@ -115,7 +115,7 @@ fn test_balanced_complete_bipartite_subgraph_solver_no_instance() { let problem = BalancedCompleteBipartiteSubgraph::new(issue_instance_1_graph(), 3); let solver = BruteForce::new(); - assert!(solver.find_satisfying(&problem).is_none()); + assert!(solver.find_witness(&problem).is_none()); } #[test] @@ -155,5 +155,5 @@ fn test_balanced_complete_bipartite_subgraph_paper_example() { let solver = BruteForce::new(); assert!(problem.evaluate(&witness)); - assert_eq!(solver.find_all_satisfying(&problem), vec![witness]); + assert_eq!(solver.find_all_witnesses(&problem), vec![witness]); } diff --git a/src/unit_tests/models/graph/biclique_cover.rs b/src/unit_tests/models/graph/biclique_cover.rs index 1d48479af..a088a0bb8 100644 --- a/src/unit_tests/models/graph/biclique_cover.rs +++ b/src/unit_tests/models/graph/biclique_cover.rs @@ -1,8 +1,8 @@ use super::*; -use crate::solvers::{BruteForce, Solver}; +use crate::solvers::BruteForce; use crate::topology::BipartiteGraph; -use crate::traits::{OptimizationProblem, Problem}; -use crate::types::{Direction, SolutionSize}; +use crate::traits::{ObjectiveProblem, Problem}; +use crate::types::{ExtremumSense, Min}; include!("../../jl_helpers.rs"); @@ -74,10 +74,10 @@ fn test_evaluate() { let problem = BicliqueCover::new(graph, 1); // Valid cover with size 2 - assert_eq!(problem.evaluate(&[1, 0, 1, 0]), SolutionSize::Valid(2)); + assert_eq!(problem.evaluate(&[1, 0, 1, 0]), Min(Some(2))); // Invalid cover returns Invalid - assert_eq!(problem.evaluate(&[1, 0, 0, 0]), SolutionSize::Invalid); + assert_eq!(problem.evaluate(&[1, 0, 0, 0]), Min(None)); } #[test] @@ -87,7 +87,7 @@ fn test_brute_force_simple() { let problem = BicliqueCover::new(graph, 1); let solver = BruteForce::new(); - let solutions = solver.find_all_best(&problem); + let solutions = solver.find_all_witnesses(&problem); for sol in &solutions { assert!(problem.is_valid_cover(sol)); // Minimum size is 2 (one left, one right vertex) @@ -103,7 +103,7 @@ fn test_brute_force_two_bicliques() { let problem = BicliqueCover::new(graph, 2); let solver = BruteForce::new(); - let solutions = solver.find_all_best(&problem); + let solutions = solver.find_all_witnesses(&problem); for sol in &solutions { assert!(problem.is_valid_cover(sol)); } @@ -139,7 +139,7 @@ fn test_is_biclique_cover_function() { fn test_direction() { let graph = BipartiteGraph::new(1, 1, vec![(0, 0)]); let problem = BicliqueCover::new(graph, 1); - assert_eq!(problem.direction(), Direction::Minimize); + assert_eq!(problem.direction(), ExtremumSense::Minimize); } #[test] @@ -147,13 +147,13 @@ fn test_empty_edges() { let graph = BipartiteGraph::new(2, 2, vec![]); let problem = BicliqueCover::new(graph, 1); // No edges to cover -> valid with size 0 - assert_eq!(problem.evaluate(&[0, 0, 0, 0]), SolutionSize::Valid(0)); + assert_eq!(problem.evaluate(&[0, 0, 0, 0]), Min(Some(0))); } #[test] fn test_biclique_problem() { - use crate::traits::{OptimizationProblem, Problem}; - use crate::types::Direction; + use crate::traits::{ObjectiveProblem, Problem}; + use crate::types::ExtremumSense; // Single edge (0,0) in local coords with k=1, 2 left + 2 right vertices let graph = BipartiteGraph::new(2, 2, vec![(0, 0)]); @@ -164,27 +164,24 @@ fn test_biclique_problem() { // Valid cover: vertex 0 and vertex 2 in biclique 0 // Config: [v0_b0=1, v1_b0=0, v2_b0=1, v3_b0=0] - assert_eq!(problem.evaluate(&[1, 0, 1, 0]), SolutionSize::Valid(2)); + assert_eq!(problem.evaluate(&[1, 0, 1, 0]), Min(Some(2))); // Invalid cover: only vertex 0, edge (0,2) not covered - assert_eq!(problem.evaluate(&[1, 0, 0, 0]), SolutionSize::Invalid); + assert_eq!(problem.evaluate(&[1, 0, 0, 0]), Min(None)); // Valid cover with all vertices -> size 4 - assert_eq!(problem.evaluate(&[1, 1, 1, 1]), SolutionSize::Valid(4)); + assert_eq!(problem.evaluate(&[1, 1, 1, 1]), Min(Some(4))); // Empty config: no vertices in biclique, edge not covered - assert_eq!(problem.evaluate(&[0, 0, 0, 0]), SolutionSize::Invalid); + assert_eq!(problem.evaluate(&[0, 0, 0, 0]), Min(None)); - // Direction is minimize - assert_eq!(problem.direction(), Direction::Minimize); + // ExtremumSense is minimize + assert_eq!(problem.direction(), ExtremumSense::Minimize); // Test with no edges: any config is valid let empty_graph = BipartiteGraph::new(2, 2, vec![]); let empty_problem = BicliqueCover::new(empty_graph, 1); - assert_eq!( - empty_problem.evaluate(&[0, 0, 0, 0]), - SolutionSize::Valid(0) - ); + assert_eq!(empty_problem.evaluate(&[0, 0, 0, 0]), Min(Some(0))); } #[test] @@ -213,18 +210,18 @@ fn test_jl_parity_evaluation() { if jl_valid { assert_eq!( result, - SolutionSize::Valid(jl_size), + Min(Some(jl_size)), "BicliqueCover: valid config mismatch" ); } else { assert_eq!( result, - SolutionSize::Invalid, + Min(None), "BicliqueCover: invalid config should be Invalid" ); } } - let best = BruteForce::new().find_all_best(&problem); + let best = BruteForce::new().find_all_witnesses(&problem); let jl_best = jl_parse_configs_set(&instance["best_solutions"]); let rust_best: HashSet> = best.into_iter().collect(); assert_eq!(rust_best, jl_best, "BicliqueCover best solutions mismatch"); @@ -269,7 +266,7 @@ fn test_biclique_paper_example() { assert_eq!(result.unwrap(), 6); let solver = BruteForce::new(); - let best = solver.find_best(&problem).unwrap(); + let best = solver.find_witness(&problem).unwrap(); let best_size = problem.evaluate(&best).unwrap(); assert!(best_size <= 6); } diff --git a/src/unit_tests/models/graph/biconnectivity_augmentation.rs b/src/unit_tests/models/graph/biconnectivity_augmentation.rs index c71b2f102..db4f33fc7 100644 --- a/src/unit_tests/models/graph/biconnectivity_augmentation.rs +++ b/src/unit_tests/models/graph/biconnectivity_augmentation.rs @@ -1,5 +1,5 @@ use super::*; -use crate::solvers::{BruteForce, Solver}; +use crate::solvers::BruteForce; use crate::topology::SimpleGraph; use crate::traits::Problem; use crate::types::One; @@ -90,11 +90,11 @@ fn test_biconnectivity_augmentation_solver() { let solver = BruteForce::new(); let solution = solver - .find_satisfying(&problem) + .find_witness(&problem) .expect("expected a satisfying augmentation"); assert_eq!(solution, vec![0, 0, 1]); - let all_solutions = solver.find_all_satisfying(&problem); + let all_solutions = solver.find_all_witnesses(&problem); assert_eq!(all_solutions, vec![vec![0, 0, 1]]); } @@ -103,8 +103,8 @@ fn test_biconnectivity_augmentation_no_solution() { let problem = BiconnectivityAugmentation::new(SimpleGraph::path(4), vec![(0, 2, 1)], 1); let solver = BruteForce::new(); - assert!(solver.find_satisfying(&problem).is_none()); - assert!(solver.find_all_satisfying(&problem).is_empty()); + assert!(solver.find_witness(&problem).is_none()); + assert!(solver.find_all_witnesses(&problem).is_empty()); } #[test] @@ -112,7 +112,7 @@ fn test_biconnectivity_augmentation_paper_example() { let problem = example_instance(); let solver = BruteForce::new(); let satisfying_config = vec![1, 0, 0, 1, 0, 0, 1, 0, 1]; - let satisfying_solutions = solver.find_all_satisfying(&problem); + let satisfying_solutions = solver.find_all_witnesses(&problem); assert!(problem.evaluate(&satisfying_config)); assert!(satisfying_solutions.contains(&satisfying_config)); @@ -133,7 +133,7 @@ fn test_biconnectivity_augmentation_paper_example() { 3, ); assert!(!over_budget_problem.evaluate(&satisfying_config)); - assert!(solver.find_satisfying(&over_budget_problem).is_none()); + assert!(solver.find_witness(&over_budget_problem).is_none()); } #[test] diff --git a/src/unit_tests/models/graph/bottleneck_traveling_salesman.rs b/src/unit_tests/models/graph/bottleneck_traveling_salesman.rs index a82530101..d12116566 100644 --- a/src/unit_tests/models/graph/bottleneck_traveling_salesman.rs +++ b/src/unit_tests/models/graph/bottleneck_traveling_salesman.rs @@ -1,8 +1,8 @@ use super::*; use crate::solvers::BruteForce; use crate::topology::SimpleGraph; -use crate::traits::{OptimizationProblem, Problem}; -use crate::types::{Direction, SolutionSize}; +use crate::traits::{ObjectiveProblem, Problem}; +use crate::types::{ExtremumSense, Min}; fn k5_btsp() -> BottleneckTravelingSalesman { BottleneckTravelingSalesman::new( @@ -63,11 +63,11 @@ fn test_bottleneck_traveling_salesman_evaluate_valid_and_invalid() { let valid_cycle = vec![0, 1, 1, 0, 1, 0, 1, 0, 0, 1]; assert!(problem.is_valid_solution(&valid_cycle)); - assert_eq!(problem.evaluate(&valid_cycle), SolutionSize::Valid(4)); + assert_eq!(problem.evaluate(&valid_cycle), Min(Some(4))); let degree_violation = vec![1, 1, 1, 0, 1, 0, 1, 0, 0, 1]; assert!(!problem.is_valid_solution(°ree_violation)); - assert_eq!(problem.evaluate(°ree_violation), SolutionSize::Invalid); + assert_eq!(problem.evaluate(°ree_violation), Min(None)); } #[test] @@ -79,10 +79,7 @@ fn test_bottleneck_traveling_salesman_evaluate_disconnected_subtour_invalid() { let disconnected_subtour = vec![1, 1, 1, 1, 1, 1]; assert!(!problem.is_valid_solution(&disconnected_subtour)); - assert_eq!( - problem.evaluate(&disconnected_subtour), - SolutionSize::Invalid - ); + assert_eq!(problem.evaluate(&disconnected_subtour), Min(None)); } #[test] @@ -92,17 +89,17 @@ fn test_bottleneck_traveling_salesman_direction() { vec![7, 4, 6], ); - assert_eq!(problem.direction(), Direction::Minimize); + assert_eq!(problem.direction(), ExtremumSense::Minimize); } #[test] fn test_bottleneck_traveling_salesman_bruteforce_unique_optimum() { let problem = k5_btsp(); let solver = BruteForce::new(); - let best = solver.find_all_best(&problem); + let best = solver.find_all_witnesses(&problem); assert_eq!(best, vec![vec![0, 1, 1, 0, 1, 0, 1, 0, 0, 1]]); - assert_eq!(problem.evaluate(&best[0]), SolutionSize::Valid(4)); + assert_eq!(problem.evaluate(&best[0]), Min(Some(4))); } #[test] @@ -116,7 +113,7 @@ fn test_bottleneck_traveling_salesman_serialization() { assert_eq!(restored.weights(), problem.weights()); assert_eq!( restored.evaluate(&[0, 1, 1, 0, 1, 0, 1, 0, 0, 1]), - SolutionSize::Valid(4) + Min(Some(4)) ); } @@ -126,10 +123,10 @@ fn test_bottleneck_traveling_salesman_paper_example() { let config = vec![0, 1, 1, 0, 1, 0, 1, 0, 0, 1]; assert!(problem.is_valid_solution(&config)); - assert_eq!(problem.evaluate(&config), SolutionSize::Valid(4)); + assert_eq!(problem.evaluate(&config), Min(Some(4))); let solver = BruteForce::new(); - let best = solver.find_all_best(&problem); + let best = solver.find_all_witnesses(&problem); assert_eq!(best.len(), 1); assert_eq!(best[0], config); } 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 2541b2e89..5e2573001 100644 --- a/src/unit_tests/models/graph/bounded_component_spanning_forest.rs +++ b/src/unit_tests/models/graph/bounded_component_spanning_forest.rs @@ -1,5 +1,5 @@ use super::*; -use crate::solvers::{BruteForce, Solver}; +use crate::solvers::BruteForce; use crate::topology::SimpleGraph; use crate::traits::Problem; use std::alloc::{GlobalAlloc, Layout, System}; @@ -150,12 +150,12 @@ fn test_bounded_component_spanning_forest_solver_yes_and_no_instances() { let solver = BruteForce::new(); let yes_problem = yes_instance(); - let solution = solver.find_satisfying(&yes_problem); + let solution = solver.find_witness(&yes_problem); assert!(solution.is_some()); assert!(yes_problem.evaluate(solution.as_ref().unwrap())); let no_problem = no_instance(); - assert!(solver.find_satisfying(&no_problem).is_none()); + assert!(solver.find_witness(&no_problem).is_none()); } #[test] @@ -165,7 +165,7 @@ fn test_bounded_component_spanning_forest_paper_example() { assert!(problem.evaluate(&config)); let solver = BruteForce::new(); - let all_solutions = solver.find_all_satisfying(&problem); + let all_solutions = solver.find_all_witnesses(&problem); assert!(all_solutions.iter().any(|solution| solution == &config)); } 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 57ea7952a..435f74744 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,5 +1,5 @@ use super::*; -use crate::solvers::{BruteForce, Solver}; +use crate::solvers::BruteForce; use crate::topology::DirectedGraph; use crate::traits::Problem; @@ -99,7 +99,7 @@ fn test_directed_two_commodity_integral_flow_negative_net_flow_at_sink_is_infeas fn test_directed_two_commodity_integral_flow_solver_yes() { let problem = yes_instance(); let solver = BruteForce::new(); - let solution = solver.find_satisfying(&problem); + let solution = solver.find_witness(&problem); assert!(solution.is_some()); let sol = solution.unwrap(); assert!(problem.evaluate(&sol)); @@ -109,7 +109,7 @@ fn test_directed_two_commodity_integral_flow_solver_yes() { fn test_directed_two_commodity_integral_flow_solver_no() { let problem = no_instance(); let solver = BruteForce::new(); - let solution = solver.find_satisfying(&problem); + let solution = solver.find_witness(&problem); assert!(solution.is_none()); } @@ -154,7 +154,7 @@ fn test_directed_two_commodity_integral_flow_paper_example() { assert!(problem.evaluate(&config)); // Find all satisfying solutions and verify count - let all_solutions = solver.find_all_satisfying(&problem); + let all_solutions = solver.find_all_witnesses(&problem); assert!(!all_solutions.is_empty()); // Each solution must evaluate to true @@ -193,5 +193,5 @@ fn test_directed_two_commodity_integral_flow_higher_capacity() { assert!(problem.evaluate(&config)); let solver = BruteForce::new(); - assert!(solver.find_satisfying(&problem).is_some()); + assert!(solver.find_witness(&problem).is_some()); } diff --git a/src/unit_tests/models/graph/disjoint_connecting_paths.rs b/src/unit_tests/models/graph/disjoint_connecting_paths.rs index 2602ac0c3..6c613bd07 100644 --- a/src/unit_tests/models/graph/disjoint_connecting_paths.rs +++ b/src/unit_tests/models/graph/disjoint_connecting_paths.rs @@ -1,5 +1,5 @@ use super::*; -use crate::solvers::{BruteForce, Solver}; +use crate::solvers::BruteForce; use crate::topology::SimpleGraph; use crate::traits::Problem; @@ -63,7 +63,7 @@ fn test_disjoint_connecting_paths_yes_instance() { fn test_disjoint_connecting_paths_no_instance() { let problem = issue_no_problem(); let solver = BruteForce::new(); - assert!(solver.find_satisfying(&problem).is_none()); + assert!(solver.find_witness(&problem).is_none()); } #[test] @@ -103,7 +103,7 @@ fn test_disjoint_connecting_paths_paper_example() { assert!(problem.evaluate(&config)); let solver = BruteForce::new(); - let solution = solver.find_satisfying(&problem); + let solution = solver.find_witness(&problem); assert!(solution.is_some()); assert!(problem.evaluate(&solution.unwrap())); } diff --git a/src/unit_tests/models/graph/generalized_hex.rs b/src/unit_tests/models/graph/generalized_hex.rs index 9087cd898..7b9799099 100644 --- a/src/unit_tests/models/graph/generalized_hex.rs +++ b/src/unit_tests/models/graph/generalized_hex.rs @@ -1,5 +1,5 @@ use super::*; -use crate::solvers::{BruteForce, Solver}; +use crate::solvers::BruteForce; use crate::topology::SimpleGraph; use crate::traits::Problem; @@ -64,9 +64,9 @@ fn test_generalized_hex_detects_losing_position() { fn test_generalized_hex_solver_returns_empty_config_for_win() { let problem = winning_example(); let solver = BruteForce::new(); - assert_eq!(solver.find_satisfying(&problem), Some(vec![])); + assert_eq!(solver.find_witness(&problem), Some(vec![])); assert_eq!( - solver.find_all_satisfying(&problem), + solver.find_all_witnesses(&problem), Vec::>::from([vec![]]) ); } @@ -99,7 +99,7 @@ fn test_generalized_hex_issue_example_is_losing_under_optimal_play() { fn test_generalized_hex_paper_example() { let problem = winning_example(); assert!(problem.evaluate(&[])); - assert_eq!(BruteForce::new().find_satisfying(&problem), Some(vec![])); + assert_eq!(BruteForce::new().find_witness(&problem), Some(vec![])); } #[test] diff --git a/src/unit_tests/models/graph/graph_partitioning.rs b/src/unit_tests/models/graph/graph_partitioning.rs index e674432b6..d2faca863 100644 --- a/src/unit_tests/models/graph/graph_partitioning.rs +++ b/src/unit_tests/models/graph/graph_partitioning.rs @@ -1,8 +1,8 @@ use super::*; -use crate::solvers::{BruteForce, Solver}; +use crate::solvers::BruteForce; use crate::topology::SimpleGraph; -use crate::traits::{OptimizationProblem, Problem}; -use crate::types::Direction; +use crate::traits::{ObjectiveProblem, Problem}; +use crate::types::ExtremumSense; /// Issue example: 6 vertices, edges forming two triangles connected by 3 edges. /// Optimal partition A={0,1,2}, B={3,4,5}, cut=3. @@ -36,13 +36,13 @@ fn test_graphpartitioning_basic() { // Crossing edges: (1,3), (2,3), (2,4) => cut = 3 let config = vec![0, 0, 0, 1, 1, 1]; let result = problem.evaluate(&config); - assert_eq!(result, SolutionSize::Valid(3)); + assert_eq!(result, Min(Some(3))); } #[test] fn test_graphpartitioning_direction() { let problem = issue_example(); - assert_eq!(problem.direction(), Direction::Minimize); + assert_eq!(problem.direction(), ExtremumSense::Minimize); } #[test] @@ -62,15 +62,15 @@ fn test_graphpartitioning_serialization() { fn test_graphpartitioning_solver() { let problem = issue_example(); let solver = BruteForce::new(); - let best = solver.find_best(&problem).unwrap(); + let best = solver.find_witness(&problem).unwrap(); let size = problem.evaluate(&best); - assert_eq!(size, SolutionSize::Valid(3)); + assert_eq!(size, Min(Some(3))); // All optimal solutions should have cut = 3 - let all_best = solver.find_all_best(&problem); + let all_best = solver.find_all_witnesses(&problem); assert!(!all_best.is_empty()); for sol in &all_best { - assert_eq!(problem.evaluate(sol), SolutionSize::Valid(3)); + assert_eq!(problem.evaluate(sol), Min(Some(3))); } } @@ -86,7 +86,7 @@ fn test_graphpartitioning_odd_vertices() { for c in 0..2 { assert_eq!( problem.evaluate(&[a, b, c]), - SolutionSize::Invalid, + Min(None), "Expected Invalid for odd n, config [{}, {}, {}]", a, b, @@ -104,20 +104,20 @@ fn test_graphpartitioning_unbalanced_invalid() { let problem = GraphPartitioning::new(graph); // All zeros: 0 ones, not balanced - assert_eq!(problem.evaluate(&[0, 0, 0, 0]), SolutionSize::Invalid); + assert_eq!(problem.evaluate(&[0, 0, 0, 0]), Min(None)); // All ones: 4 ones, not balanced - assert_eq!(problem.evaluate(&[1, 1, 1, 1]), SolutionSize::Invalid); + assert_eq!(problem.evaluate(&[1, 1, 1, 1]), Min(None)); // One vertex in partition 1: not balanced - assert_eq!(problem.evaluate(&[1, 0, 0, 0]), SolutionSize::Invalid); + assert_eq!(problem.evaluate(&[1, 0, 0, 0]), Min(None)); // Three vertices in partition 1: not balanced - assert_eq!(problem.evaluate(&[1, 1, 1, 0]), SolutionSize::Invalid); + assert_eq!(problem.evaluate(&[1, 1, 1, 0]), Min(None)); // Two vertices in partition 1: balanced, should be Valid // 4-cycle edges: (0,1),(1,2),(2,3),(0,3). Config [1,1,0,0] cuts (1,2) and (0,3) => cut=2 - assert_eq!(problem.evaluate(&[1, 1, 0, 0]), SolutionSize::Valid(2)); + assert_eq!(problem.evaluate(&[1, 1, 0, 0]), Min(Some(2))); } #[test] @@ -134,11 +134,11 @@ fn test_graphpartitioning_square_graph() { let problem = GraphPartitioning::new(graph); let solver = BruteForce::new(); - let all_best = solver.find_all_best(&problem); + let all_best = solver.find_all_witnesses(&problem); // Minimum bisection of a 4-cycle: cut = 2 for sol in &all_best { - assert_eq!(problem.evaluate(sol), SolutionSize::Valid(2)); + assert_eq!(problem.evaluate(sol), Min(Some(2))); } } @@ -165,5 +165,5 @@ fn test_graphpartitioning_empty_graph() { let problem = GraphPartitioning::new(graph); let config = vec![0, 0, 1, 1]; - assert_eq!(problem.evaluate(&config), SolutionSize::Valid(0)); + assert_eq!(problem.evaluate(&config), Min(Some(0))); } diff --git a/src/unit_tests/models/graph/hamiltonian_circuit.rs b/src/unit_tests/models/graph/hamiltonian_circuit.rs index 3fd70b413..42f6e8d79 100644 --- a/src/unit_tests/models/graph/hamiltonian_circuit.rs +++ b/src/unit_tests/models/graph/hamiltonian_circuit.rs @@ -1,5 +1,5 @@ use super::*; -use crate::solvers::{BruteForce, Solver}; +use crate::solvers::BruteForce; use crate::topology::SimpleGraph; use crate::traits::Problem; @@ -65,7 +65,7 @@ fn test_hamiltonian_circuit_small_graphs() { let graph = SimpleGraph::new(3, vec![(0, 1), (1, 2), (2, 0)]); let problem = HamiltonianCircuit::new(graph); let solver = BruteForce::new(); - let solutions = solver.find_all_satisfying(&problem); + let solutions = solver.find_all_witnesses(&problem); // K3 has 6 directed Hamiltonian circuits: 3 rotations x 2 directions assert_eq!(solutions.len(), 6); } @@ -77,7 +77,7 @@ fn test_hamiltonian_circuit_complete_graph_k4() { let problem = HamiltonianCircuit::new(graph); let solver = BruteForce::new(); - let solutions = solver.find_all_satisfying(&problem); + let solutions = solver.find_all_witnesses(&problem); // K4 has 3 distinct undirected Hamiltonian circuits, each yielding // 4 rotations x 2 directions = 8 directed permutations => 24 total assert_eq!(solutions.len(), 24); @@ -93,8 +93,8 @@ fn test_hamiltonian_circuit_no_solution() { let problem = HamiltonianCircuit::new(graph); let solver = BruteForce::new(); - assert!(solver.find_satisfying(&problem).is_none()); - assert!(solver.find_all_satisfying(&problem).is_empty()); + assert!(solver.find_witness(&problem).is_none()); + assert!(solver.find_all_witnesses(&problem).is_empty()); } #[test] @@ -104,7 +104,7 @@ fn test_hamiltonian_circuit_solver() { let problem = HamiltonianCircuit::new(graph); let solver = BruteForce::new(); - let solutions = solver.find_all_satisfying(&problem); + let solutions = solver.find_all_witnesses(&problem); // 4-cycle has 8 Hamiltonian circuits: 4 starting positions x 2 directions assert_eq!(solutions.len(), 8); diff --git a/src/unit_tests/models/graph/hamiltonian_path.rs b/src/unit_tests/models/graph/hamiltonian_path.rs index 493769055..55078a641 100644 --- a/src/unit_tests/models/graph/hamiltonian_path.rs +++ b/src/unit_tests/models/graph/hamiltonian_path.rs @@ -1,5 +1,5 @@ use super::*; -use crate::solvers::{BruteForce, Solver}; +use crate::solvers::BruteForce; use crate::topology::SimpleGraph; #[test] @@ -30,7 +30,7 @@ fn test_hamiltonian_path_no_solution() { vec![(0, 1), (0, 2), (0, 3), (1, 2), (1, 3), (2, 3)], )); let solver = BruteForce::new(); - let solution = solver.find_satisfying(&problem); + let solution = solver.find_witness(&problem); assert!( solution.is_none(), "Graph with isolated vertices has no Hamiltonian path" @@ -45,11 +45,11 @@ fn test_hamiltonian_path_brute_force() { let problem = HamiltonianPath::new(SimpleGraph::new(4, vec![(0, 1), (1, 2), (2, 3)])); let solver = BruteForce::new(); - let solution = solver.find_satisfying(&problem); + let solution = solver.find_witness(&problem); assert!(solution.is_some()); assert!(problem.evaluate(&solution.unwrap())); - let all = solver.find_all_satisfying(&problem); + let all = solver.find_all_witnesses(&problem); // Path graph P4 has exactly 2 Hamiltonian paths: 0-1-2-3 and 3-2-1-0 assert_eq!(all.len(), 2); for sol in &all { @@ -87,7 +87,7 @@ fn test_hamiltonian_path_complete_graph() { vec![(0, 1), (0, 2), (0, 3), (1, 2), (1, 3), (2, 3)], )); let solver = BruteForce::new(); - let all = solver.find_all_satisfying(&problem); + let all = solver.find_all_witnesses(&problem); // K4 has 4! = 24 Hamiltonian paths (all permutations) assert_eq!(all.len(), 24); } @@ -151,7 +151,7 @@ fn test_hamiltonianpath_paper_example() { // Verify with brute force let solver = BruteForce::new(); - let all = solver.find_all_satisfying(&problem); + let all = solver.find_all_witnesses(&problem); assert!(!all.is_empty()); for sol in &all { assert!(problem.evaluate(sol)); @@ -166,6 +166,6 @@ fn test_single_vertex() { let problem = HamiltonianPath::new(SimpleGraph::new(1, vec![])); assert!(problem.evaluate(&[0])); let solver = BruteForce::new(); - let all = solver.find_all_satisfying(&problem); + let all = solver.find_all_witnesses(&problem); assert_eq!(all.len(), 1); } diff --git a/src/unit_tests/models/graph/integral_flow_bundles.rs b/src/unit_tests/models/graph/integral_flow_bundles.rs index 67a6595e5..0e95b3c24 100644 --- a/src/unit_tests/models/graph/integral_flow_bundles.rs +++ b/src/unit_tests/models/graph/integral_flow_bundles.rs @@ -74,7 +74,7 @@ fn test_integral_flow_bundles_rejects_bad_bundle_sum_or_conservation() { fn test_integral_flow_bundles_solver_and_paper_example() { let problem = yes_instance(); let solver = BruteForce::new(); - let all = solver.find_all_satisfying(&problem); + let all = solver.find_all_witnesses(&problem); assert!(!all.is_empty()); assert!(all.contains(&satisfying_config())); assert!(problem.evaluate(&satisfying_config())); 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 e9cfb245e..ae200f3eb 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,5 @@ use super::*; -use crate::solvers::{BruteForce, Solver}; +use crate::solvers::BruteForce; use crate::topology::DirectedGraph; use crate::traits::Problem; @@ -83,7 +83,7 @@ fn test_integral_flow_homologous_arcs_wrong_config_length_is_invalid() { fn test_integral_flow_homologous_arcs_solver_yes() { let problem = yes_instance(); let solver = BruteForce::new(); - let solution = solver.find_satisfying(&problem); + let solution = solver.find_witness(&problem); assert!(solution.is_some()); assert!(problem.evaluate(&solution.unwrap())); } @@ -92,7 +92,7 @@ fn test_integral_flow_homologous_arcs_solver_yes() { fn test_integral_flow_homologous_arcs_solver_no() { let problem = no_instance(); let solver = BruteForce::new(); - assert!(solver.find_satisfying(&problem).is_none()); + assert!(solver.find_witness(&problem).is_none()); } #[test] @@ -126,7 +126,7 @@ fn test_integral_flow_homologous_arcs_non_unit_capacity() { assert!(problem.evaluate(&[3, 3])); assert!(!problem.evaluate(&[2, 3])); // homologous violation let solver = BruteForce::new(); - let solutions = solver.find_all_satisfying(&problem); + let solutions = solver.find_all_witnesses(&problem); assert_eq!(solutions.len(), 2); // [2,2] and [3,3] } @@ -138,7 +138,7 @@ fn test_integral_flow_homologous_arcs_paper_example() { assert!(problem.evaluate(&config)); - let solutions = solver.find_all_satisfying(&problem); + let solutions = solver.find_all_witnesses(&problem); assert!(!solutions.is_empty()); assert!(solutions.iter().all(|solution| problem.evaluate(solution))); } 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 d2dc7a65a..a4afd16af 100644 --- a/src/unit_tests/models/graph/integral_flow_with_multipliers.rs +++ b/src/unit_tests/models/graph/integral_flow_with_multipliers.rs @@ -1,6 +1,6 @@ use super::*; use crate::registry::declared_size_fields; -use crate::solvers::{BruteForce, Solver}; +use crate::solvers::BruteForce; use crate::topology::DirectedGraph; use crate::traits::Problem; use std::collections::HashSet; @@ -64,7 +64,7 @@ fn test_integral_flow_with_multipliers_evaluate_yes_instance() { #[test] fn test_integral_flow_with_multipliers_evaluate_no_instance() { let solver = BruteForce::new(); - assert!(solver.find_satisfying(&no_instance()).is_none()); + assert!(solver.find_witness(&no_instance()).is_none()); } #[test] @@ -103,7 +103,7 @@ fn test_integral_flow_with_multipliers_serialization_round_trip() { fn test_integral_flow_with_multipliers_solver_yes_instance() { let problem = yes_instance(); let solver = BruteForce::new(); - let solution = solver.find_satisfying(&problem).unwrap(); + let solution = solver.find_witness(&problem).unwrap(); assert!(problem.evaluate(&solution)); } @@ -144,6 +144,6 @@ fn test_integral_flow_with_multipliers_paper_example() { assert_eq!([config[6], config[8], config[10]], [2, 4, 6]); assert_eq!(config[6] + config[8] + config[10], 12); - let all_solutions = solver.find_all_satisfying(&problem); + let all_solutions = solver.find_all_witnesses(&problem); assert!(all_solutions.iter().any(|solution| solution == &config)); } diff --git a/src/unit_tests/models/graph/isomorphic_spanning_tree.rs b/src/unit_tests/models/graph/isomorphic_spanning_tree.rs index 3a871f223..b16d36f62 100644 --- a/src/unit_tests/models/graph/isomorphic_spanning_tree.rs +++ b/src/unit_tests/models/graph/isomorphic_spanning_tree.rs @@ -1,5 +1,5 @@ use super::*; -use crate::solvers::{BruteForce, Solver}; +use crate::solvers::BruteForce; use crate::topology::SimpleGraph; use crate::traits::Problem; @@ -77,12 +77,12 @@ fn test_isomorphicspanningtree_solver_yes() { let problem = IsomorphicSpanningTree::new(graph, tree); let solver = BruteForce::new(); - let sol = solver.find_satisfying(&problem); + let sol = solver.find_witness(&problem); assert!(sol.is_some()); assert!(problem.evaluate(&sol.unwrap())); // All satisfying solutions should be valid - let all = solver.find_all_satisfying(&problem); + let all = solver.find_all_witnesses(&problem); assert!(!all.is_empty()); for s in &all { assert!(problem.evaluate(s)); @@ -97,10 +97,10 @@ fn test_isomorphicspanningtree_solver_no() { let problem = IsomorphicSpanningTree::new(graph, tree); let solver = BruteForce::new(); - let sol = solver.find_satisfying(&problem); + let sol = solver.find_witness(&problem); assert!(sol.is_none()); - let all = solver.find_all_satisfying(&problem); + let all = solver.find_all_witnesses(&problem); assert!(all.is_empty()); } @@ -164,7 +164,7 @@ fn test_isomorphicspanningtree_paper_example() { // All 4! = 24 permutations should work since K4 has every edge let solver = BruteForce::new(); - let all = solver.find_all_satisfying(&problem); + let all = solver.find_all_witnesses(&problem); assert_eq!(all.len(), 24); } diff --git a/src/unit_tests/models/graph/kclique.rs b/src/unit_tests/models/graph/kclique.rs index 4343118bc..fc42e1909 100644 --- a/src/unit_tests/models/graph/kclique.rs +++ b/src/unit_tests/models/graph/kclique.rs @@ -1,5 +1,5 @@ use super::*; -use crate::solvers::{BruteForce, Solver}; +use crate::solvers::BruteForce; use crate::topology::SimpleGraph; use crate::traits::Problem; @@ -52,8 +52,8 @@ fn test_kclique_solver_finds_unique_witness() { let problem = KClique::new(issue_graph(), 3); let solver = BruteForce::new(); - assert_eq!(solver.find_satisfying(&problem), Some(issue_witness())); - assert_eq!(solver.find_all_satisfying(&problem), vec![issue_witness()]); + assert_eq!(solver.find_witness(&problem), Some(issue_witness())); + assert_eq!(solver.find_all_witnesses(&problem), vec![issue_witness()]); } #[test] @@ -73,5 +73,5 @@ fn test_kclique_paper_example() { let solver = BruteForce::new(); assert!(problem.evaluate(&issue_witness())); - assert_eq!(solver.find_all_satisfying(&problem), vec![issue_witness()]); + assert_eq!(solver.find_all_witnesses(&problem), vec![issue_witness()]); } diff --git a/src/unit_tests/models/graph/kcoloring.rs b/src/unit_tests/models/graph/kcoloring.rs index a2ab6e79a..f7747820f 100644 --- a/src/unit_tests/models/graph/kcoloring.rs +++ b/src/unit_tests/models/graph/kcoloring.rs @@ -1,5 +1,5 @@ use super::*; -use crate::solvers::{BruteForce, Solver}; +use crate::solvers::BruteForce; use crate::topology::SimpleGraph; use crate::variant::{K1, K2, K3, K4}; include!("../../jl_helpers.rs"); @@ -45,7 +45,7 @@ fn test_brute_force_path() { let problem = KColoring::::new(SimpleGraph::new(4, vec![(0, 1), (1, 2), (2, 3)])); let solver = BruteForce::new(); - let solutions = solver.find_all_satisfying(&problem); + let solutions = solver.find_all_witnesses(&problem); // All solutions should be valid for sol in &solutions { assert!(problem.evaluate(sol)); @@ -60,7 +60,7 @@ fn test_brute_force_triangle() { let problem = KColoring::::new(SimpleGraph::new(3, vec![(0, 1), (1, 2), (0, 2)])); let solver = BruteForce::new(); - let solutions = solver.find_all_satisfying(&problem); + let solutions = solver.find_all_witnesses(&problem); for sol in &solutions { assert!(problem.evaluate(sol)); // All three vertices have different colors @@ -76,7 +76,7 @@ fn test_triangle_2_colors() { let problem = KColoring::::new(SimpleGraph::new(3, vec![(0, 1), (1, 2), (0, 2)])); let solver = BruteForce::new(); - let solutions = solver.find_all_satisfying(&problem); + let solutions = solver.find_all_witnesses(&problem); // No valid solutions assert!(solutions.is_empty()); } @@ -106,7 +106,7 @@ fn test_empty_graph() { let problem = KColoring::::new(SimpleGraph::new(3, vec![])); let solver = BruteForce::new(); - let solutions = solver.find_all_satisfying(&problem); + let solutions = solver.find_all_witnesses(&problem); // Any coloring is valid when there are no edges assert!(!solutions.is_empty()); for sol in &solutions { @@ -125,7 +125,7 @@ fn test_complete_graph_k4() { )); let solver = BruteForce::new(); - let solutions = solver.find_all_satisfying(&problem); + let solutions = solver.find_all_witnesses(&problem); for sol in &solutions { assert!(problem.evaluate(sol)); } @@ -172,7 +172,7 @@ fn test_jl_parity_evaluation() { config ); } - let all_sat = BruteForce::new().find_all_satisfying(&problem); + let all_sat = BruteForce::new().find_all_witnesses(&problem); let jl_best = jl_parse_configs_set(&instance["best_solutions"]); let rust_sat: HashSet> = all_sat.into_iter().collect(); assert_eq!(rust_sat, jl_best, "KColoring satisfying solutions mismatch"); @@ -209,5 +209,5 @@ fn test_kcoloring_paper_example() { let graph2 = SimpleGraph::new(5, vec![(0, 1), (0, 2), (1, 3), (2, 3), (2, 4), (3, 4)]); let problem2 = KColoring::::new(graph2); let solver = BruteForce::new(); - assert!(solver.find_satisfying(&problem2).is_none()); + assert!(solver.find_witness(&problem2).is_none()); } 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 22c812154..23481792b 100644 --- a/src/unit_tests/models/graph/kth_best_spanning_tree.rs +++ b/src/unit_tests/models/graph/kth_best_spanning_tree.rs @@ -1,5 +1,5 @@ use super::*; -use crate::solvers::{BruteForce, Solver}; +use crate::solvers::BruteForce; use crate::topology::SimpleGraph; use crate::traits::Problem; @@ -91,7 +91,7 @@ fn test_kthbestspanningtree_solver_exhaustive() { let solver = BruteForce::new(); // Exactly 2 spanning trees have weight ≤ 4, so exactly 2! = 2 satisfying configs. - let all = solver.find_all_satisfying(&problem); + let all = solver.find_all_witnesses(&problem); assert_eq!(all.len(), 2); assert!(all.iter().all(|config| problem.evaluate(config))); } @@ -101,8 +101,8 @@ fn test_kthbestspanningtree_solver_no_instance() { let problem = no_instance(); let solver = BruteForce::new(); - assert!(solver.find_satisfying(&problem).is_none()); - assert!(solver.find_all_satisfying(&problem).is_empty()); + assert!(solver.find_witness(&problem).is_none()); + assert!(solver.find_all_witnesses(&problem).is_empty()); } #[test] @@ -110,7 +110,7 @@ fn test_kthbestspanningtree_small_exhaustive_search() { let problem = small_yes_instance(); let solver = BruteForce::new(); - let all = solver.find_all_satisfying(&problem); + let all = solver.find_all_witnesses(&problem); assert_eq!(all.len(), 6); assert!(all.iter().all(|config| problem.evaluate(config))); } 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 f6a891e4b..a3e93c9da 100644 --- a/src/unit_tests/models/graph/length_bounded_disjoint_paths.rs +++ b/src/unit_tests/models/graph/length_bounded_disjoint_paths.rs @@ -1,5 +1,5 @@ use super::*; -use crate::solvers::{BruteForce, Solver}; +use crate::solvers::BruteForce; use crate::topology::SimpleGraph; use crate::traits::Problem; @@ -135,10 +135,10 @@ fn test_length_bounded_disjoint_paths_rejects_non_binary_entries() { fn test_length_bounded_disjoint_paths_solver_yes_and_no() { let yes_problem = sample_yes_problem(); let solver = BruteForce::new(); - assert!(solver.find_satisfying(&yes_problem).is_some()); + assert!(solver.find_witness(&yes_problem).is_some()); let no_problem = LengthBoundedDisjointPaths::new(sample_yes_graph(), 0, 6, 2, 2); - assert!(solver.find_satisfying(&no_problem).is_none()); + assert!(solver.find_witness(&no_problem).is_none()); } #[test] @@ -185,7 +185,7 @@ fn test_length_bounded_disjoint_paths_paper_example() { let config = encode_paths(7, &[&[0, 1, 6], &[0, 2, 3, 6]]); assert!(problem.evaluate(&config)); - let satisfying = BruteForce::new().find_all_satisfying(&problem); + let satisfying = BruteForce::new().find_all_witnesses(&problem); assert_eq!(satisfying.len(), 6); assert!(satisfying .iter() diff --git a/src/unit_tests/models/graph/longest_circuit.rs b/src/unit_tests/models/graph/longest_circuit.rs index c3407f801..b3c4e4798 100644 --- a/src/unit_tests/models/graph/longest_circuit.rs +++ b/src/unit_tests/models/graph/longest_circuit.rs @@ -1,5 +1,5 @@ use super::*; -use crate::solvers::{BruteForce, Solver}; +use crate::solvers::BruteForce; use crate::topology::SimpleGraph; use crate::traits::Problem; @@ -72,7 +72,7 @@ fn test_longest_circuit_rejects_non_binary_and_below_bound_configs() { fn test_longest_circuit_bruteforce_yes_and_no() { let yes_problem = issue_problem(); let solver = BruteForce::new(); - assert!(solver.find_satisfying(&yes_problem).is_some()); + assert!(solver.find_witness(&yes_problem).is_some()); let no_problem = LongestCircuit::new( SimpleGraph::new( @@ -93,7 +93,7 @@ fn test_longest_circuit_bruteforce_yes_and_no() { vec![3, 2, 4, 1, 5, 2, 3, 2, 1, 2], 19, ); - assert!(solver.find_satisfying(&no_problem).is_none()); + assert!(solver.find_witness(&no_problem).is_none()); } #[test] @@ -113,7 +113,7 @@ fn test_longest_circuit_paper_example() { let config = vec![1, 1, 1, 1, 1, 1, 0, 0, 0, 0]; assert!(problem.evaluate(&config)); - let all = BruteForce::new().find_all_satisfying(&problem); + let all = BruteForce::new().find_all_witnesses(&problem); assert!(all.contains(&config)); } diff --git a/src/unit_tests/models/graph/longest_path.rs b/src/unit_tests/models/graph/longest_path.rs index 9b2d54ab0..a8b086f9d 100644 --- a/src/unit_tests/models/graph/longest_path.rs +++ b/src/unit_tests/models/graph/longest_path.rs @@ -1,8 +1,8 @@ use super::*; -use crate::solvers::{BruteForce, Solver}; +use crate::solvers::BruteForce; use crate::topology::SimpleGraph; -use crate::traits::{OptimizationProblem, Problem}; -use crate::types::{Direction, One, SolutionSize}; +use crate::traits::{ObjectiveProblem, Problem}; +use crate::types::{ExtremumSense, Max, One}; fn issue_problem() -> LongestPath { LongestPath::new( @@ -48,7 +48,7 @@ fn test_longest_path_creation() { assert_eq!(problem.dims(), vec![2; 10]); assert_eq!(problem.edge_lengths(), &[3, 2, 4, 1, 5, 2, 3, 2, 4, 1]); assert!(problem.is_weighted()); - assert_eq!(problem.direction(), Direction::Maximize); + assert_eq!(problem.direction(), ExtremumSense::Maximize); problem.set_lengths(vec![1; 10]); assert_eq!(problem.edge_lengths(), &[1; 10]); @@ -61,27 +61,15 @@ fn test_longest_path_creation() { fn test_longest_path_evaluate_valid_and_invalid_configs() { let problem = issue_problem(); - assert_eq!(problem.evaluate(&optimal_config()), SolutionSize::Valid(20)); - assert_eq!( - problem.evaluate(&suboptimal_config()), - SolutionSize::Valid(17) - ); + assert_eq!(problem.evaluate(&optimal_config()), Max(Some(20))); + assert_eq!(problem.evaluate(&suboptimal_config()), Max(Some(17))); assert!(problem.is_valid_solution(&optimal_config())); assert!(problem.is_valid_solution(&suboptimal_config())); - assert_eq!( - problem.evaluate(&[1, 1, 1, 0, 0, 0, 0, 0, 0, 0]), - SolutionSize::Invalid - ); - assert_eq!( - problem.evaluate(&[1, 0, 1, 0, 1, 0, 0, 0, 0, 1]), - SolutionSize::Invalid - ); - assert_eq!( - problem.evaluate(&[1, 0, 1, 1, 1, 1, 1, 1, 1, 1]), - SolutionSize::Invalid - ); - assert_eq!(problem.evaluate(&[0; 10]), SolutionSize::Invalid); + assert_eq!(problem.evaluate(&[1, 1, 1, 0, 0, 0, 0, 0, 0, 0]), Max(None)); + assert_eq!(problem.evaluate(&[1, 0, 1, 0, 1, 0, 0, 0, 0, 1]), Max(None)); + assert_eq!(problem.evaluate(&[1, 0, 1, 1, 1, 1, 1, 1, 1, 1]), Max(None)); + assert_eq!(problem.evaluate(&[0; 10]), Max(None)); assert!(!problem.is_valid_solution(&[1, 0, 1])); assert!(!problem.is_valid_solution(&[1, 0, 1, 0, 1, 0, 1, 0, 1, 2])); } @@ -91,11 +79,11 @@ fn test_longest_path_bruteforce_finds_issue_optimum() { let problem = issue_problem(); let solver = BruteForce::new(); - let best = solver.find_best(&problem).unwrap(); + let best = solver.find_witness(&problem).unwrap(); assert_eq!(best, optimal_config()); - assert_eq!(problem.evaluate(&best), SolutionSize::Valid(20)); + assert_eq!(problem.evaluate(&best), Max(Some(20))); - let all_best = solver.find_all_best(&problem); + let all_best = solver.find_all_witnesses(&problem); assert_eq!(all_best, vec![optimal_config()]); } @@ -110,10 +98,7 @@ fn test_longest_path_serialization() { assert_eq!(restored.source_vertex(), 0); assert_eq!(restored.target_vertex(), 6); assert_eq!(restored.edge_lengths(), &[3, 2, 4, 1, 5, 2, 3, 2, 4, 1]); - assert_eq!( - restored.evaluate(&optimal_config()), - SolutionSize::Valid(20) - ); + assert_eq!(restored.evaluate(&optimal_config()), Max(Some(20))); } #[test] @@ -121,11 +106,11 @@ fn test_longest_path_source_equals_target_only_allows_empty_path() { let problem = LongestPath::new(SimpleGraph::path(3), vec![5, 7], 1, 1); assert!(problem.is_valid_solution(&[0, 0])); - assert_eq!(problem.evaluate(&[0, 0]), SolutionSize::Valid(0)); + assert_eq!(problem.evaluate(&[0, 0]), Max(Some(0))); assert!(!problem.is_valid_solution(&[1, 0])); - assert_eq!(problem.evaluate(&[1, 0]), SolutionSize::Invalid); + assert_eq!(problem.evaluate(&[1, 0]), Max(None)); - let best = BruteForce::new().find_best(&problem).unwrap(); + let best = BruteForce::new().find_witness(&problem).unwrap(); assert_eq!(best, vec![0, 0]); } @@ -133,15 +118,9 @@ fn test_longest_path_source_equals_target_only_allows_empty_path() { fn test_longestpath_paper_example() { let problem = issue_problem(); - assert_eq!(problem.evaluate(&optimal_config()), SolutionSize::Valid(20)); - assert_eq!( - problem.evaluate(&suboptimal_config()), - SolutionSize::Valid(17) - ); - assert_eq!( - problem.evaluate(&[1, 1, 1, 0, 0, 0, 0, 0, 0, 0]), - SolutionSize::Invalid - ); + assert_eq!(problem.evaluate(&optimal_config()), Max(Some(20))); + assert_eq!(problem.evaluate(&suboptimal_config()), Max(Some(17))); + assert_eq!(problem.evaluate(&[1, 1, 1, 0, 0, 0, 0, 0, 0, 0]), Max(None)); } #[test] diff --git a/src/unit_tests/models/graph/max_cut.rs b/src/unit_tests/models/graph/max_cut.rs index 345859e54..957394c62 100644 --- a/src/unit_tests/models/graph/max_cut.rs +++ b/src/unit_tests/models/graph/max_cut.rs @@ -1,5 +1,5 @@ use super::*; -use crate::solvers::{BruteForce, Solver}; +use crate::solvers::BruteForce; use crate::topology::SimpleGraph; include!("../../jl_helpers.rs"); @@ -55,11 +55,11 @@ fn test_edges() { #[test] fn test_direction() { - use crate::traits::OptimizationProblem; - use crate::types::Direction; + use crate::traits::ObjectiveProblem; + use crate::types::ExtremumSense; let problem = MaxCut::<_, i32>::unweighted(SimpleGraph::new(2, vec![(0, 1)])); - assert_eq!(problem.direction(), Direction::Maximize); + assert_eq!(problem.direction(), ExtremumSense::Maximize); } #[test] @@ -122,7 +122,7 @@ fn test_jl_parity_evaluation() { config ); } - let best = BruteForce::new().find_all_best(&problem); + let best = BruteForce::new().find_all_witnesses(&problem); let jl_best = jl_parse_configs_set(&instance["best_solutions"]); let rust_best: HashSet> = best.into_iter().collect(); assert_eq!(rust_best, jl_best, "MaxCut best solutions mismatch"); @@ -160,6 +160,6 @@ fn test_maxcut_paper_example() { assert_eq!(result.unwrap(), 5); let solver = BruteForce::new(); - let best = solver.find_best(&problem).unwrap(); + let best = solver.find_witness(&problem).unwrap(); assert_eq!(problem.evaluate(&best).unwrap(), 5); } diff --git a/src/unit_tests/models/graph/maximal_is.rs b/src/unit_tests/models/graph/maximal_is.rs index be9cf494f..77566f897 100644 --- a/src/unit_tests/models/graph/maximal_is.rs +++ b/src/unit_tests/models/graph/maximal_is.rs @@ -1,5 +1,5 @@ use super::*; -use crate::solvers::{BruteForce, Solver}; +use crate::solvers::BruteForce; use crate::topology::SimpleGraph; include!("../../jl_helpers.rs"); @@ -68,11 +68,11 @@ fn test_is_maximal_independent_set_function() { #[test] fn test_direction() { - use crate::traits::OptimizationProblem; - use crate::types::Direction; + use crate::traits::ObjectiveProblem; + use crate::types::ExtremumSense; let problem = MaximalIS::new(SimpleGraph::new(2, vec![(0, 1)]), vec![1i32; 2]); - assert_eq!(problem.direction(), Direction::Maximize); + assert_eq!(problem.direction(), ExtremumSense::Maximize); } #[test] @@ -159,7 +159,7 @@ fn test_jl_parity_evaluation() { ); } } - let best = BruteForce::new().find_all_best(&problem); + let best = BruteForce::new().find_all_witnesses(&problem); let jl_best = jl_parse_configs_set(&instance["best_solutions"]); let rust_best: HashSet> = best.into_iter().collect(); assert_eq!(rust_best, jl_best, "MaximalIS best solutions mismatch"); @@ -207,6 +207,6 @@ fn test_maximal_is_paper_example() { // Verify optimal weight is 3 let solver = BruteForce::new(); - let best = solver.find_best(&problem).unwrap(); + let best = solver.find_witness(&problem).unwrap(); assert_eq!(problem.evaluate(&best).unwrap(), 3); } diff --git a/src/unit_tests/models/graph/maximum_clique.rs b/src/unit_tests/models/graph/maximum_clique.rs index 1004037a8..196899d91 100644 --- a/src/unit_tests/models/graph/maximum_clique.rs +++ b/src/unit_tests/models/graph/maximum_clique.rs @@ -1,7 +1,7 @@ use super::*; -use crate::solvers::{BruteForce, Solver}; +use crate::solvers::BruteForce; use crate::topology::SimpleGraph; -use crate::types::SolutionSize; +use crate::types::Max; #[test] fn test_clique_creation() { @@ -50,10 +50,10 @@ fn test_evaluate_valid() { ); // Valid: all three form a clique - assert_eq!(problem.evaluate(&[1, 1, 1]), SolutionSize::Valid(3)); + assert_eq!(problem.evaluate(&[1, 1, 1]), Max(Some(3))); // Valid: any pair - assert_eq!(problem.evaluate(&[1, 1, 0]), SolutionSize::Valid(2)); + assert_eq!(problem.evaluate(&[1, 1, 0]), Max(Some(2))); } #[test] @@ -64,10 +64,10 @@ fn test_evaluate_invalid() { let problem = MaximumClique::new(SimpleGraph::new(3, vec![(0, 1), (1, 2)]), vec![1i32; 3]); // Invalid: 0 and 2 are not adjacent - returns Invalid - assert_eq!(problem.evaluate(&[1, 0, 1]), SolutionSize::Invalid); + assert_eq!(problem.evaluate(&[1, 0, 1]), Max(None)); // Invalid: all three selected but not a clique - assert_eq!(problem.evaluate(&[1, 1, 1]), SolutionSize::Invalid); + assert_eq!(problem.evaluate(&[1, 1, 1]), Max(None)); } #[test] @@ -76,7 +76,7 @@ fn test_evaluate_empty() { let problem = MaximumClique::new(SimpleGraph::new(3, vec![(0, 1), (1, 2)]), vec![1i32; 3]); // Empty set is a valid clique with size 0 - assert_eq!(problem.evaluate(&[0, 0, 0]), SolutionSize::Valid(0)); + assert_eq!(problem.evaluate(&[0, 0, 0]), Max(Some(0))); } #[test] @@ -89,10 +89,10 @@ fn test_weighted_solution() { ); // Select vertex 2 (weight 30) - assert_eq!(problem.evaluate(&[0, 0, 1]), SolutionSize::Valid(30)); + assert_eq!(problem.evaluate(&[0, 0, 1]), Max(Some(30))); // Select all three (weights 10 + 20 + 30 = 60) - assert_eq!(problem.evaluate(&[1, 1, 1]), SolutionSize::Valid(60)); + assert_eq!(problem.evaluate(&[1, 1, 1]), Max(Some(60))); } #[test] @@ -104,7 +104,7 @@ fn test_brute_force_triangle() { ); let solver = BruteForce::new(); - let solutions = solver.find_all_best(&problem); + let solutions = solver.find_all_witnesses(&problem); assert_eq!(solutions.len(), 1); assert_eq!(solutions[0], vec![1, 1, 1]); } @@ -117,7 +117,7 @@ fn test_brute_force_path() { let problem = MaximumClique::new(SimpleGraph::new(3, vec![(0, 1), (1, 2)]), vec![1i32; 3]); let solver = BruteForce::new(); - let solutions = solver.find_all_best(&problem); + let solutions = solver.find_all_witnesses(&problem); // Maximum size is 2 for sol in &solutions { let size: usize = sol.iter().sum(); @@ -135,11 +135,11 @@ fn test_brute_force_weighted() { let problem = MaximumClique::new(SimpleGraph::new(3, vec![(0, 1), (1, 2)]), vec![1, 100, 1]); let solver = BruteForce::new(); - let solutions = solver.find_all_best(&problem); + let solutions = solver.find_all_witnesses(&problem); // Should select {0, 1} (weight 101) or {1, 2} (weight 101) assert!(solutions.len() == 2); for sol in &solutions { - assert_eq!(problem.evaluate(sol), SolutionSize::Valid(101)); + assert_eq!(problem.evaluate(sol), Max(Some(101))); } } @@ -168,11 +168,11 @@ fn test_is_clique_function() { #[test] fn test_direction() { - use crate::traits::OptimizationProblem; - use crate::types::Direction; + use crate::traits::ObjectiveProblem; + use crate::types::ExtremumSense; let problem = MaximumClique::new(SimpleGraph::new(3, vec![(0, 1)]), vec![1i32; 3]); - assert_eq!(problem.direction(), Direction::Maximize); + assert_eq!(problem.direction(), ExtremumSense::Maximize); } #[test] @@ -188,7 +188,7 @@ fn test_empty_graph() { let problem = MaximumClique::new(SimpleGraph::new(3, vec![]), vec![1i32; 3]); let solver = BruteForce::new(); - let solutions = solver.find_all_best(&problem); + let solutions = solver.find_all_witnesses(&problem); assert_eq!(solutions.len(), 3); // Each solution should have exactly one vertex selected for sol in &solutions { @@ -206,7 +206,7 @@ fn test_is_clique_method() { assert!(problem.evaluate(&[1, 1, 0]).is_valid()); assert!(problem.evaluate(&[0, 1, 1]).is_valid()); // Invalid: 0-2 not adjacent - returns Invalid - assert_eq!(problem.evaluate(&[1, 0, 1]), SolutionSize::Invalid); + assert_eq!(problem.evaluate(&[1, 0, 1]), Max(None)); } #[test] @@ -247,15 +247,15 @@ fn test_complete_graph() { ); let solver = BruteForce::new(); - let solutions = solver.find_all_best(&problem); + let solutions = solver.find_all_witnesses(&problem); assert_eq!(solutions.len(), 1); assert_eq!(solutions[0], vec![1, 1, 1, 1]); // All vertices form a clique } #[test] fn test_clique_problem() { - use crate::traits::{OptimizationProblem, Problem}; - use crate::types::Direction; + use crate::traits::{ObjectiveProblem, Problem}; + use crate::types::ExtremumSense; // Triangle graph: all pairs connected let p = MaximumClique::new( @@ -264,10 +264,10 @@ fn test_clique_problem() { ); assert_eq!(p.dims(), vec![2, 2, 2]); // Valid clique: select all 3 vertices (triangle is a clique) - assert_eq!(p.evaluate(&[1, 1, 1]), SolutionSize::Valid(3)); + assert_eq!(p.evaluate(&[1, 1, 1]), Max(Some(3))); // Valid clique: select just vertex 0 - assert_eq!(p.evaluate(&[1, 0, 0]), SolutionSize::Valid(1)); - assert_eq!(p.direction(), Direction::Maximize); + assert_eq!(p.evaluate(&[1, 0, 0]), Max(Some(1))); + assert_eq!(p.direction(), ExtremumSense::Maximize); } #[test] @@ -304,6 +304,6 @@ fn test_clique_paper_example() { assert_eq!(result.unwrap(), 3); let solver = BruteForce::new(); - let best = solver.find_best(&problem).unwrap(); + let best = solver.find_witness(&problem).unwrap(); assert_eq!(problem.evaluate(&best).unwrap(), 3); } diff --git a/src/unit_tests/models/graph/maximum_independent_set.rs b/src/unit_tests/models/graph/maximum_independent_set.rs index 5c0f2352d..82ece958e 100644 --- a/src/unit_tests/models/graph/maximum_independent_set.rs +++ b/src/unit_tests/models/graph/maximum_independent_set.rs @@ -1,8 +1,8 @@ use super::*; -use crate::solvers::{BruteForce, Solver}; +use crate::solvers::BruteForce; use crate::topology::SimpleGraph; -use crate::traits::{OptimizationProblem, Problem}; -use crate::types::Direction; +use crate::traits::{ObjectiveProblem, Problem}; +use crate::types::ExtremumSense; include!("../../jl_helpers.rs"); #[test] @@ -67,7 +67,7 @@ fn test_is_independent_set_function() { #[test] fn test_direction() { let problem = MaximumIndependentSet::new(SimpleGraph::new(3, vec![(0, 1)]), vec![1i32; 3]); - assert_eq!(problem.direction(), Direction::Maximize); + assert_eq!(problem.direction(), ExtremumSense::Maximize); } #[test] @@ -155,7 +155,7 @@ fn test_jl_parity_evaluation() { ); } } - let best = BruteForce::new().find_all_best(&problem); + let best = BruteForce::new().find_all_witnesses(&problem); let jl_best = jl_parse_configs_set(&instance["best_solutions"]); let rust_best: HashSet> = best.into_iter().collect(); assert_eq!(rust_best, jl_best, "IS best solutions mismatch"); @@ -215,6 +215,6 @@ fn test_mis_paper_example() { // Verify this is optimal let solver = BruteForce::new(); - let best = solver.find_best(&problem).unwrap(); + let best = solver.find_witness(&problem).unwrap(); assert_eq!(problem.evaluate(&best).unwrap(), 4); } diff --git a/src/unit_tests/models/graph/maximum_matching.rs b/src/unit_tests/models/graph/maximum_matching.rs index d74eea97f..9c358150e 100644 --- a/src/unit_tests/models/graph/maximum_matching.rs +++ b/src/unit_tests/models/graph/maximum_matching.rs @@ -1,8 +1,8 @@ use super::*; -use crate::solvers::{BruteForce, Solver}; +use crate::solvers::BruteForce; use crate::topology::SimpleGraph; -use crate::traits::{OptimizationProblem, Problem}; -use crate::types::{Direction, SolutionSize}; +use crate::traits::{ObjectiveProblem, Problem}; +use crate::types::{ExtremumSense, Max}; include!("../../jl_helpers.rs"); #[test] @@ -61,14 +61,14 @@ fn test_is_matching_function() { #[test] fn test_direction() { let problem = MaximumMatching::<_, i32>::unit_weights(SimpleGraph::new(2, vec![(0, 1)])); - assert_eq!(problem.direction(), Direction::Maximize); + assert_eq!(problem.direction(), ExtremumSense::Maximize); } #[test] fn test_empty_graph() { let problem = MaximumMatching::<_, i32>::unit_weights(SimpleGraph::new(3, vec![])); // Empty matching is valid with size 0 - assert_eq!(Problem::evaluate(&problem, &[]), SolutionSize::Valid(0)); + assert_eq!(Problem::evaluate(&problem, &[]), Max(Some(0))); } #[test] @@ -82,7 +82,7 @@ fn test_edges() { fn test_empty_sets() { let problem = MaximumMatching::<_, i32>::unit_weights(SimpleGraph::new(2, vec![])); // Empty matching - assert_eq!(Problem::evaluate(&problem, &[]), SolutionSize::Valid(0)); + assert_eq!(Problem::evaluate(&problem, &[]), Max(Some(0))); } #[test] @@ -147,7 +147,7 @@ fn test_jl_parity_evaluation() { ); } } - let best = BruteForce::new().find_all_best(&problem); + let best = BruteForce::new().find_all_witnesses(&problem); let jl_best = jl_parse_configs_set(&instance["best_solutions"]); let rust_best: HashSet> = best.into_iter().collect(); assert_eq!(rust_best, jl_best, "Matching best solutions mismatch"); @@ -190,6 +190,6 @@ fn test_matching_paper_example() { assert_eq!(result.unwrap(), 2); let solver = BruteForce::new(); - let best = solver.find_best(&problem).unwrap(); + let best = solver.find_witness(&problem).unwrap(); assert_eq!(problem.evaluate(&best).unwrap(), 2); } diff --git a/src/unit_tests/models/graph/min_max_multicenter.rs b/src/unit_tests/models/graph/min_max_multicenter.rs index de1f018d3..daa7aeb8a 100644 --- a/src/unit_tests/models/graph/min_max_multicenter.rs +++ b/src/unit_tests/models/graph/min_max_multicenter.rs @@ -90,7 +90,7 @@ fn test_minmaxmulticenter_solver() { let problem = example_instance(); let solver = BruteForce::new(); - let solutions = solver.find_all_satisfying(&problem); + let solutions = solver.find_all_witnesses(&problem); // All solutions should evaluate to true assert!(!solutions.is_empty()); 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 5ea0d9ea7..20577117b 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,7 +1,7 @@ use super::*; -use crate::solvers::{BruteForce, Solver}; +use crate::solvers::BruteForce; use crate::topology::SimpleGraph; -use crate::traits::{Problem, SatisfactionProblem}; +use crate::traits::{Problem, WitnessProblem}; /// Build the example instance from issue #228: /// 8 vertices, 12 edges, s=0, t=7, B=5 @@ -127,7 +127,7 @@ fn test_minimumcutintoboundedsets_serialization() { fn test_minimumcutintoboundedsets_solver_satisfying() { let problem = example_instance(6); let solver = BruteForce::new(); - let solution = solver.find_satisfying(&problem); + let solution = solver.find_witness(&problem); assert!( solution.is_some(), "Should find a satisfying partition for K=6" @@ -142,7 +142,7 @@ fn test_minimumcutintoboundedsets_solver_no_solution() { let graph = SimpleGraph::new(4, vec![(0, 1), (1, 2), (2, 3)]); let problem = MinimumCutIntoBoundedSets::new(graph, vec![1, 1, 1], 0, 3, 3, 0); let solver = BruteForce::new(); - let solution = solver.find_satisfying(&problem); + let solution = solver.find_witness(&problem); assert!( solution.is_none(), "Should find no satisfying partition for K=0" @@ -181,7 +181,7 @@ fn test_minimumcutintoboundedsets_all_satisfying() { let graph = SimpleGraph::new(3, vec![(0, 1), (1, 2)]); let problem = MinimumCutIntoBoundedSets::new(graph, vec![1, 1], 0, 2, 2, 1); let solver = BruteForce::new(); - let solutions = solver.find_all_satisfying(&problem); + let solutions = solver.find_all_witnesses(&problem); // Two valid partitions: {0,1}|{2} and {0}|{1,2} assert_eq!(solutions.len(), 2); for sol in &solutions { @@ -202,16 +202,16 @@ fn test_minimumcutintoboundedsets_solver_no_solution_issue_instance() { // Issue #228 NO instance: K=5 on the 8-vertex graph has no valid partition let problem = example_instance(5); let solver = BruteForce::new(); - let solution = solver.find_satisfying(&problem); + let solution = solver.find_witness(&problem); assert!( solution.is_none(), "Should find no satisfying partition for K=5 on the 8-vertex instance" ); } -// Verify SatisfactionProblem marker trait is implemented +// Verify WitnessProblem marker trait is implemented #[test] fn test_minimumcutintoboundedsets_is_satisfaction_problem() { - fn assert_satisfaction() {} + fn assert_satisfaction() {} assert_satisfaction::>(); } diff --git a/src/unit_tests/models/graph/minimum_dominating_set.rs b/src/unit_tests/models/graph/minimum_dominating_set.rs index b891105c3..562265717 100644 --- a/src/unit_tests/models/graph/minimum_dominating_set.rs +++ b/src/unit_tests/models/graph/minimum_dominating_set.rs @@ -1,8 +1,8 @@ use super::*; -use crate::solvers::{BruteForce, Solver}; +use crate::solvers::BruteForce; use crate::topology::SimpleGraph; -use crate::traits::{OptimizationProblem, Problem}; -use crate::types::Direction; +use crate::traits::{ObjectiveProblem, Problem}; +use crate::types::ExtremumSense; include!("../../jl_helpers.rs"); #[test] @@ -63,7 +63,7 @@ fn test_is_dominating_set_function() { #[test] fn test_direction() { let problem = MinimumDominatingSet::new(SimpleGraph::new(2, vec![(0, 1)]), vec![1i32; 2]); - assert_eq!(problem.direction(), Direction::Minimize); + assert_eq!(problem.direction(), ExtremumSense::Minimize); } #[test] @@ -72,7 +72,7 @@ fn test_isolated_vertex() { let problem = MinimumDominatingSet::new(SimpleGraph::new(3, vec![(0, 1)]), vec![1i32; 3]); let solver = BruteForce::new(); - let solutions = solver.find_all_best(&problem); + let solutions = solver.find_all_witnesses(&problem); // Vertex 2 is isolated, must be selected for sol in &solutions { assert_eq!(sol[2], 1); @@ -154,7 +154,7 @@ fn test_jl_parity_evaluation() { ); } } - let best = BruteForce::new().find_all_best(&problem); + let best = BruteForce::new().find_all_witnesses(&problem); let jl_best = jl_parse_configs_set(&instance["best_solutions"]); let rust_best: HashSet> = best.into_iter().collect(); assert_eq!(rust_best, jl_best, "DS best solutions mismatch"); @@ -191,6 +191,6 @@ fn test_mds_paper_example() { assert_eq!(result.unwrap(), 2); let solver = BruteForce::new(); - let best = solver.find_best(&problem).unwrap(); + let best = solver.find_witness(&problem).unwrap(); assert_eq!(problem.evaluate(&best).unwrap(), 2); } 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 1d9415c9b..33cda723e 100644 --- a/src/unit_tests/models/graph/minimum_dummy_activities_pert.rs +++ b/src/unit_tests/models/graph/minimum_dummy_activities_pert.rs @@ -1,8 +1,8 @@ use super::*; -use crate::solvers::{BruteForce, Solver}; +use crate::solvers::BruteForce; use crate::topology::DirectedGraph; -use crate::traits::{OptimizationProblem, Problem}; -use crate::types::{Direction, SolutionSize}; +use crate::traits::{ObjectiveProblem, Problem}; +use crate::types::{ExtremumSense, Min}; fn issue_graph() -> DirectedGraph { DirectedGraph::new(6, vec![(0, 2), (0, 3), (1, 3), (1, 4), (2, 5)]) @@ -48,8 +48,8 @@ fn test_minimum_dummy_activities_pert_rejects_cyclic_input() { fn test_minimum_dummy_activities_pert_issue_example() { let problem = issue_problem(); let config = config_for_merges(&problem, &[(0, 2), (1, 4), (2, 5)]); - assert_eq!(problem.direction(), Direction::Minimize); - assert_eq!(problem.evaluate(&config), SolutionSize::Valid(2)); + assert_eq!(problem.direction(), ExtremumSense::Minimize); + assert_eq!(problem.evaluate(&config), Min(Some(2))); assert!(problem.is_valid_solution(&config)); } @@ -57,15 +57,15 @@ fn test_minimum_dummy_activities_pert_issue_example() { fn test_minimum_dummy_activities_pert_rejects_spurious_reachability() { let problem = issue_problem(); let config = config_for_merges(&problem, &[(0, 3), (1, 3)]); - assert_eq!(problem.evaluate(&config), SolutionSize::Invalid); + assert_eq!(problem.evaluate(&config), Min(None)); assert!(!problem.is_valid_solution(&config)); } #[test] fn test_minimum_dummy_activities_pert_solver_finds_optimum_two() { let problem = issue_problem(); - let solution = BruteForce::new().find_best(&problem).unwrap(); - assert_eq!(problem.evaluate(&solution), SolutionSize::Valid(2)); + let solution = BruteForce::new().find_witness(&problem).unwrap(); + assert_eq!(problem.evaluate(&solution), Min(Some(2))); } #[test] @@ -83,15 +83,15 @@ fn test_minimum_dummy_activities_pert_transitive_arc_zero_dummies() { // satisfied, so the optimal dummy count is 0. let dag = DirectedGraph::new(3, vec![(0, 1), (1, 2), (0, 2)]); let problem = MinimumDummyActivitiesPert::new(dag); - let solution = BruteForce::new().find_best(&problem).unwrap(); - assert_eq!(problem.evaluate(&solution), SolutionSize::Valid(0)); + let solution = BruteForce::new().find_witness(&problem).unwrap(); + assert_eq!(problem.evaluate(&solution), Min(Some(0))); } #[test] fn test_minimum_dummy_activities_pert_paper_example() { let problem = issue_problem(); let config = config_for_merges(&problem, &[(0, 2), (1, 4), (2, 5)]); - assert_eq!(problem.evaluate(&config), SolutionSize::Valid(2)); - let solution = BruteForce::new().find_best(&problem).unwrap(); - assert_eq!(problem.evaluate(&solution), SolutionSize::Valid(2)); + assert_eq!(problem.evaluate(&config), Min(Some(2))); + let solution = BruteForce::new().find_witness(&problem).unwrap(); + assert_eq!(problem.evaluate(&solution), Min(Some(2))); } 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 09dd95f75..864552127 100644 --- a/src/unit_tests/models/graph/minimum_feedback_arc_set.rs +++ b/src/unit_tests/models/graph/minimum_feedback_arc_set.rs @@ -1,8 +1,8 @@ use super::*; -use crate::solvers::{BruteForce, Solver}; +use crate::solvers::BruteForce; use crate::topology::DirectedGraph; -use crate::traits::{OptimizationProblem, Problem}; -use crate::types::Direction; +use crate::traits::{ObjectiveProblem, Problem}; +use crate::types::ExtremumSense; #[test] fn test_minimum_feedback_arc_set_creation() { @@ -32,7 +32,7 @@ fn test_minimum_feedback_arc_set_creation() { fn test_minimum_feedback_arc_set_direction() { let graph = DirectedGraph::new(3, vec![(0, 1), (1, 2), (2, 0)]); let problem = MinimumFeedbackArcSet::new(graph, vec![1i32; 3]); - assert_eq!(problem.direction(), Direction::Minimize); + assert_eq!(problem.direction(), ExtremumSense::Minimize); } #[test] @@ -91,7 +91,7 @@ fn test_minimum_feedback_arc_set_solver_simple_cycle() { let graph = DirectedGraph::new(3, vec![(0, 1), (1, 2), (2, 0)]); let problem = MinimumFeedbackArcSet::new(graph, vec![1i32; 3]); - let solutions = BruteForce::new().find_all_best(&problem); + let solutions = BruteForce::new().find_all_witnesses(&problem); // Minimum FAS has size 1 (remove any one arc) for sol in &solutions { assert_eq!(sol.iter().sum::(), 1); @@ -119,7 +119,7 @@ fn test_minimum_feedback_arc_set_solver_issue_example() { ); let problem = MinimumFeedbackArcSet::new(graph, vec![1i32; 9]); - let solution = BruteForce::new().find_best(&problem).unwrap(); + let solution = BruteForce::new().find_witness(&problem).unwrap(); // The optimal FAS has size 2 let fas_size: usize = solution.iter().sum(); assert_eq!(fas_size, 2); @@ -136,7 +136,7 @@ fn test_minimum_feedback_arc_set_weighted() { let graph = DirectedGraph::new(3, vec![(0, 1), (1, 2), (2, 0)]); let problem = MinimumFeedbackArcSet::new(graph, vec![10i32, 1, 1]); - let solution = BruteForce::new().find_best(&problem).unwrap(); + let solution = BruteForce::new().find_witness(&problem).unwrap(); let result = problem.evaluate(&solution); assert!(result.is_valid()); assert_eq!(result.unwrap(), 1); // should pick a cheap arc @@ -180,7 +180,7 @@ fn test_minimum_feedback_arc_set_two_disjoint_cycles() { let graph = DirectedGraph::new(4, vec![(0, 1), (1, 0), (2, 3), (3, 2)]); let problem = MinimumFeedbackArcSet::new(graph, vec![1i32; 4]); - let solution = BruteForce::new().find_best(&problem).unwrap(); + let solution = BruteForce::new().find_witness(&problem).unwrap(); // Need to remove at least one arc from each cycle -> size 2 assert_eq!(solution.iter().sum::(), 2); } 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 353c585b6..3ccc42ddc 100644 --- a/src/unit_tests/models/graph/minimum_feedback_vertex_set.rs +++ b/src/unit_tests/models/graph/minimum_feedback_vertex_set.rs @@ -1,9 +1,9 @@ use super::is_feedback_vertex_set; use crate::models::graph::MinimumFeedbackVertexSet; -use crate::solvers::{BruteForce, Solver}; +use crate::solvers::BruteForce; use crate::topology::DirectedGraph; -use crate::traits::{OptimizationProblem, Problem}; -use crate::types::Direction; +use crate::traits::{ObjectiveProblem, Problem}; +use crate::types::ExtremumSense; /// Build the 9-vertex, 15-arc example from the issue. /// @@ -63,7 +63,7 @@ fn test_minimum_feedback_vertex_set_basic() { fn test_minimum_feedback_vertex_set_direction() { let graph = DirectedGraph::new(3, vec![(0, 1), (1, 2), (2, 0)]); let problem = MinimumFeedbackVertexSet::new(graph, vec![1i32; 3]); - assert_eq!(problem.direction(), Direction::Minimize); + assert_eq!(problem.direction(), ExtremumSense::Minimize); } #[test] @@ -86,14 +86,14 @@ fn test_minimum_feedback_vertex_set_solver() { let problem = MinimumFeedbackVertexSet::new(graph, vec![1i32; 9]); let solver = BruteForce::new(); - let best = solver.find_best(&problem); + let best = solver.find_witness(&problem); assert!(best.is_some(), "Expected a solution to exist"); let best_config = best.unwrap(); let best_result = problem.evaluate(&best_config); assert!(best_result.is_valid()); assert_eq!(best_result.unwrap(), 3, "Expected optimal FVS size 3"); - let all_best = BruteForce::new().find_all_best(&problem); + let all_best = BruteForce::new().find_all_witnesses(&problem); assert_eq!(all_best.len(), 18, "Expected 18 optimal FVS solutions"); } @@ -208,6 +208,6 @@ fn test_minimum_feedback_vertex_set_paper_example() { // Verify optimal FVS weight is 1 let solver = BruteForce::new(); - let best = solver.find_best(&problem).unwrap(); + let best = solver.find_witness(&problem).unwrap(); assert_eq!(problem.evaluate(&best).unwrap(), 1); } diff --git a/src/unit_tests/models/graph/minimum_multiway_cut.rs b/src/unit_tests/models/graph/minimum_multiway_cut.rs index 85bd2baa7..85f5a0a3c 100644 --- a/src/unit_tests/models/graph/minimum_multiway_cut.rs +++ b/src/unit_tests/models/graph/minimum_multiway_cut.rs @@ -1,8 +1,8 @@ use super::*; use crate::solvers::BruteForce; use crate::topology::SimpleGraph; -use crate::traits::{OptimizationProblem, Problem}; -use crate::types::{Direction, SolutionSize}; +use crate::traits::{ObjectiveProblem, Problem}; +use crate::types::{ExtremumSense, Min}; #[test] fn test_minimummultiwaycut_creation() { @@ -25,7 +25,7 @@ fn test_minimummultiwaycut_evaluate_valid() { // config: [1, 0, 0, 1, 1, 0] => weight 2 + 2 + 4 = 8 let config = vec![1, 0, 0, 1, 1, 0]; let result = problem.evaluate(&config); - assert_eq!(result, SolutionSize::Valid(8)); + assert_eq!(result, Min(Some(8))); } #[test] @@ -36,14 +36,14 @@ fn test_minimummultiwaycut_evaluate_invalid() { // No edges cut: all terminals connected => invalid let config = vec![0, 0, 0, 0, 0, 0]; let result = problem.evaluate(&config); - assert_eq!(result, SolutionSize::Invalid); + assert_eq!(result, Min(None)); } #[test] fn test_minimummultiwaycut_direction() { let graph = SimpleGraph::new(3, vec![(0, 1), (1, 2)]); let problem = MinimumMultiwayCut::new(graph, vec![0, 2], vec![1i32, 1]); - assert_eq!(problem.direction(), Direction::Minimize); + assert_eq!(problem.direction(), ExtremumSense::Minimize); } #[test] @@ -53,11 +53,11 @@ fn test_minimummultiwaycut_brute_force() { let problem = MinimumMultiwayCut::new(graph, vec![0, 2, 4], vec![2, 3, 1, 2, 4, 5]); let solver = BruteForce::new(); - let solutions = solver.find_all_best(&problem); + let solutions = solver.find_all_witnesses(&problem); assert!(!solutions.is_empty()); for sol in &solutions { let val = problem.evaluate(sol); - assert_eq!(val, SolutionSize::Valid(8)); + assert_eq!(val, Min(Some(8))); } // Verify the claimed optimal cut [1,0,0,1,1,0] is among solutions let claimed_optimal = vec![1, 0, 0, 1, 1, 0]; @@ -77,9 +77,9 @@ fn test_minimummultiwaycut_two_terminals() { let problem = MinimumMultiwayCut::new(graph, vec![0, 2], vec![3i32, 5]); let solver = BruteForce::new(); - let solutions = solver.find_all_best(&problem); + let solutions = solver.find_all_witnesses(&problem); for sol in &solutions { - assert_eq!(problem.evaluate(sol), SolutionSize::Valid(3)); + assert_eq!(problem.evaluate(sol), Min(Some(3))); } } @@ -89,7 +89,7 @@ fn test_minimummultiwaycut_all_edges_cut() { let problem = MinimumMultiwayCut::new(graph, vec![0, 2, 4], vec![2, 3, 1, 2, 4, 5]); let config = vec![1, 1, 1, 1, 1, 1]; let result = problem.evaluate(&config); - assert_eq!(result, SolutionSize::Valid(2 + 3 + 1 + 2 + 4 + 5)); + assert_eq!(result, Min(Some(2 + 3 + 1 + 2 + 4 + 5))); } #[test] @@ -100,12 +100,12 @@ fn test_minimummultiwaycut_already_disconnected() { let problem = MinimumMultiwayCut::new(graph, vec![0, 2], vec![1i32, 1]); let config = vec![0, 0]; let result = problem.evaluate(&config); - assert_eq!(result, SolutionSize::Valid(0)); + assert_eq!(result, Min(Some(0))); let solver = BruteForce::new(); - let solutions = solver.find_all_best(&problem); + let solutions = solver.find_all_witnesses(&problem); for sol in &solutions { - assert_eq!(problem.evaluate(sol), SolutionSize::Valid(0)); + assert_eq!(problem.evaluate(sol), Min(Some(0))); } } @@ -172,10 +172,10 @@ fn test_minimummultiwaycut_short_config_no_panic() { // Short config: only 2 of 6 edges specified, terminals remain connected let short_config = vec![1, 0]; let result = problem.evaluate(&short_config); - assert_eq!(result, SolutionSize::Invalid); + assert_eq!(result, Min(None)); // Empty config: no edges cut, all terminals connected let empty_config: Vec = vec![]; let result = problem.evaluate(&empty_config); - assert_eq!(result, SolutionSize::Invalid); + assert_eq!(result, Min(None)); } diff --git a/src/unit_tests/models/graph/minimum_sum_multicenter.rs b/src/unit_tests/models/graph/minimum_sum_multicenter.rs index b6aa0aac0..d6d3338ba 100644 --- a/src/unit_tests/models/graph/minimum_sum_multicenter.rs +++ b/src/unit_tests/models/graph/minimum_sum_multicenter.rs @@ -1,8 +1,8 @@ use super::*; -use crate::solvers::{BruteForce, Solver}; +use crate::solvers::BruteForce; use crate::topology::SimpleGraph; -use crate::traits::{OptimizationProblem, Problem}; -use crate::types::Direction; +use crate::traits::{ObjectiveProblem, Problem}; +use crate::types::ExtremumSense; #[test] fn test_min_sum_multicenter_creation() { @@ -28,7 +28,7 @@ fn test_min_sum_multicenter_size_getters() { fn test_min_sum_multicenter_direction() { let graph = SimpleGraph::new(3, vec![(0, 1), (1, 2)]); let problem = MinimumSumMulticenter::new(graph, vec![1i32; 3], vec![1i32; 2], 1); - assert_eq!(problem.direction(), Direction::Minimize); + assert_eq!(problem.direction(), ExtremumSense::Minimize); } #[test] @@ -127,7 +127,7 @@ fn test_min_sum_multicenter_solver() { let problem = MinimumSumMulticenter::new(graph, vec![1i32; 7], vec![1i32; 8], 2); let solver = BruteForce::new(); - let best = solver.find_best(&problem).unwrap(); + let best = solver.find_witness(&problem).unwrap(); let best_cost = problem.evaluate(&best).unwrap(); // Optimal cost should be 6 (centers at {2, 5}) @@ -226,7 +226,7 @@ fn test_min_sum_multicenter_paper_example() { // Verify optimality with brute force let solver = BruteForce::new(); - let best = solver.find_best(&problem).unwrap(); + let best = solver.find_witness(&problem).unwrap(); assert_eq!(problem.evaluate(&best).unwrap(), 6); } @@ -238,13 +238,13 @@ fn test_min_sum_multicenter_dims() { } #[test] -fn test_min_sum_multicenter_find_all_best() { +fn test_min_sum_multicenter_find_all_witnesses() { // Path: 0-1-2, unit weights, K=1. Center at 1 is optimal (cost 2) let graph = SimpleGraph::new(3, vec![(0, 1), (1, 2)]); let problem = MinimumSumMulticenter::new(graph, vec![1i32; 3], vec![1i32; 2], 1); let solver = BruteForce::new(); - let solutions = solver.find_all_best(&problem); + let solutions = solver.find_all_witnesses(&problem); assert_eq!(solutions.len(), 1); assert_eq!(solutions[0], vec![0, 1, 0]); } diff --git a/src/unit_tests/models/graph/minimum_vertex_cover.rs b/src/unit_tests/models/graph/minimum_vertex_cover.rs index ba2a36648..b4310fce0 100644 --- a/src/unit_tests/models/graph/minimum_vertex_cover.rs +++ b/src/unit_tests/models/graph/minimum_vertex_cover.rs @@ -1,8 +1,8 @@ use super::*; -use crate::solvers::{BruteForce, Solver}; +use crate::solvers::BruteForce; use crate::topology::SimpleGraph; -use crate::traits::{OptimizationProblem, Problem}; -use crate::types::Direction; +use crate::traits::{ObjectiveProblem, Problem}; +use crate::types::ExtremumSense; include!("../../jl_helpers.rs"); #[test] @@ -45,7 +45,7 @@ fn test_is_vertex_cover_function() { #[test] fn test_direction() { let problem = MinimumVertexCover::new(SimpleGraph::new(3, vec![(0, 1)]), vec![1i32; 3]); - assert_eq!(problem.direction(), Direction::Minimize); + assert_eq!(problem.direction(), ExtremumSense::Minimize); } #[test] @@ -59,7 +59,7 @@ fn test_complement_relationship() { let solver = BruteForce::new(); - let is_solutions = solver.find_all_best(&is_problem); + let is_solutions = solver.find_all_witnesses(&is_problem); for is_sol in &is_solutions { // Complement should be a valid vertex cover let vc_config: Vec = is_sol.iter().map(|&x| 1 - x).collect(); @@ -138,7 +138,7 @@ fn test_jl_parity_evaluation() { ); } } - let best = BruteForce::new().find_all_best(&problem); + let best = BruteForce::new().find_all_witnesses(&problem); let jl_best = jl_parse_configs_set(&instance["best_solutions"]); let rust_best: HashSet> = best.into_iter().collect(); assert_eq!(rust_best, jl_best, "VC best solutions mismatch"); @@ -173,6 +173,6 @@ fn test_mvc_paper_example() { assert_eq!(result.unwrap(), 3); let solver = BruteForce::new(); - let best = solver.find_best(&problem).unwrap(); + let best = solver.find_witness(&problem).unwrap(); assert_eq!(problem.evaluate(&best).unwrap(), 3); } diff --git a/src/unit_tests/models/graph/mixed_chinese_postman.rs b/src/unit_tests/models/graph/mixed_chinese_postman.rs index e73da1ff6..74da8ae96 100644 --- a/src/unit_tests/models/graph/mixed_chinese_postman.rs +++ b/src/unit_tests/models/graph/mixed_chinese_postman.rs @@ -1,5 +1,5 @@ use super::*; -use crate::solvers::{BruteForce, Solver}; +use crate::solvers::BruteForce; use crate::topology::MixedGraph; use crate::traits::Problem; @@ -68,7 +68,7 @@ fn test_mixed_chinese_postman_single_edge_walk() { assert!(problem.evaluate(&[1])); let solver = BruteForce::new(); - assert!(solver.find_satisfying(&problem).is_some()); + assert!(solver.find_witness(&problem).is_some()); } #[test] @@ -102,7 +102,7 @@ fn test_mixed_chinese_postman_solver_finds_satisfying_orientation() { let solver = BruteForce::new(); let solution = solver - .find_satisfying(&problem) + .find_witness(&problem) .expect("expected a satisfying orientation"); assert!(problem.evaluate(&solution)); } @@ -112,7 +112,7 @@ fn test_mixed_chinese_postman_solver_reports_unsat_issue_example() { let problem = no_instance(); let solver = BruteForce::new(); - assert!(solver.find_satisfying(&problem).is_none()); + assert!(solver.find_witness(&problem).is_none()); } #[test] diff --git a/src/unit_tests/models/graph/multiple_choice_branching.rs b/src/unit_tests/models/graph/multiple_choice_branching.rs index 9d31d30ed..80ceb6750 100644 --- a/src/unit_tests/models/graph/multiple_choice_branching.rs +++ b/src/unit_tests/models/graph/multiple_choice_branching.rs @@ -1,5 +1,5 @@ use super::*; -use crate::solvers::{BruteForce, Solver}; +use crate::solvers::BruteForce; use crate::topology::DirectedGraph; use crate::traits::Problem; use serde_json; @@ -165,11 +165,11 @@ fn test_multiple_choice_branching_solver_issue_examples() { let yes_problem = yes_instance(); let solver = BruteForce::new(); - let solution = solver.find_satisfying(&yes_problem); + let solution = solver.find_witness(&yes_problem); assert!(solution.is_some()); assert!(yes_problem.evaluate(&solution.unwrap())); - let all_solutions = solver.find_all_satisfying(&yes_problem); + let all_solutions = solver.find_all_witnesses(&yes_problem); assert!(!all_solutions.is_empty()); assert!(all_solutions.contains(&vec![1, 0, 1, 0, 0, 1, 0, 1])); for config in &all_solutions { @@ -177,7 +177,7 @@ fn test_multiple_choice_branching_solver_issue_examples() { } let no_problem = no_instance(); - assert!(solver.find_satisfying(&no_problem).is_none()); + assert!(solver.find_witness(&no_problem).is_none()); } #[test] @@ -187,7 +187,7 @@ fn test_multiple_choice_branching_paper_example() { assert!(problem.evaluate(&config)); - let all_solutions = BruteForce::new().find_all_satisfying(&problem); + let all_solutions = BruteForce::new().find_all_witnesses(&problem); assert_eq!(all_solutions.len(), 11); assert!(all_solutions.contains(&config)); } 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 56a9c7b7d..4f358ff71 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,5 @@ use super::*; -use crate::solvers::{BruteForce, Solver}; +use crate::solvers::BruteForce; use crate::topology::SimpleGraph; use crate::traits::Problem; @@ -94,9 +94,9 @@ fn test_multiple_copy_file_allocation_solver_yes_and_no() { let no_problem = cycle_no_instance(); let solver = BruteForce::new(); - let solution = solver.find_satisfying(&yes_problem).unwrap(); + let solution = solver.find_witness(&yes_problem).unwrap(); assert!(yes_problem.evaluate(&solution)); - assert!(solver.find_satisfying(&no_problem).is_none()); + assert!(solver.find_witness(&no_problem).is_none()); } #[test] @@ -121,7 +121,7 @@ fn test_multiple_copy_file_allocation_paper_example() { assert_eq!(problem.total_cost(&config), Some(33)); let solver = BruteForce::new(); - let all = solver.find_all_satisfying(&problem); + let all = solver.find_all_witnesses(&problem); assert_eq!(all.len(), 36); assert!(all.iter().any(|candidate| candidate == &config)); } diff --git a/src/unit_tests/models/graph/optimal_linear_arrangement.rs b/src/unit_tests/models/graph/optimal_linear_arrangement.rs index c8371791a..94e1e025b 100644 --- a/src/unit_tests/models/graph/optimal_linear_arrangement.rs +++ b/src/unit_tests/models/graph/optimal_linear_arrangement.rs @@ -1,5 +1,5 @@ use super::*; -use crate::solvers::{BruteForce, Solver}; +use crate::solvers::BruteForce; use crate::topology::SimpleGraph; use crate::traits::Problem; @@ -52,7 +52,7 @@ fn test_optimallineararrangement_no_instance() { // Brute-force confirms no arrangement achieves cost <= 9 let solver = BruteForce::new(); - assert!(solver.find_satisfying(&problem).is_none()); + assert!(solver.find_witness(&problem).is_none()); } #[test] @@ -104,13 +104,13 @@ fn test_optimallineararrangement_solver() { let problem = OptimalLinearArrangement::new(graph, 4); let solver = BruteForce::new(); - let solution = solver.find_satisfying(&problem); + let solution = solver.find_witness(&problem); assert!(solution.is_some()); let sol = solution.unwrap(); assert!(problem.evaluate(&sol)); // All satisfying solutions should be valid - let all_sat = solver.find_all_satisfying(&problem); + let all_sat = solver.find_all_witnesses(&problem); assert!(!all_sat.is_empty()); for s in &all_sat { assert!(problem.evaluate(s)); @@ -125,10 +125,10 @@ fn test_optimallineararrangement_solver_no_solution() { let problem = OptimalLinearArrangement::new(graph, 3); let solver = BruteForce::new(); - let solution = solver.find_satisfying(&problem); + let solution = solver.find_witness(&problem); assert!(solution.is_none()); - let all_sat = solver.find_all_satisfying(&problem); + let all_sat = solver.find_all_witnesses(&problem); assert!(all_sat.is_empty()); } @@ -139,7 +139,7 @@ fn test_optimallineararrangement_empty_graph() { let problem = OptimalLinearArrangement::new(graph, 0); let solver = BruteForce::new(); - let all_sat = solver.find_all_satisfying(&problem); + let all_sat = solver.find_all_witnesses(&problem); // All 3! = 6 permutations should be valid assert_eq!(all_sat.len(), 6); for s in &all_sat { @@ -241,7 +241,7 @@ fn test_optimallineararrangement_complete_graph_k4() { let problem = OptimalLinearArrangement::new(graph, 10); let solver = BruteForce::new(); - let all_sat = solver.find_all_satisfying(&problem); + let all_sat = solver.find_all_witnesses(&problem); // All 4! = 24 permutations should be valid since all have cost 10 assert_eq!(all_sat.len(), 24); for sol in &all_sat { 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 c576cdef4..3df87c0bd 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,5 +1,5 @@ use super::*; -use crate::solvers::{BruteForce, Solver}; +use crate::solvers::BruteForce; use crate::topology::SimpleGraph; use crate::traits::Problem; @@ -51,7 +51,7 @@ fn test_partition_into_paths_no_solution() { assert_eq!(problem.num_groups(), 2); let solver = BruteForce::new(); - let solution = solver.find_satisfying(&problem); + let solution = solver.find_witness(&problem); assert!(solution.is_none(), "Expected no solution for this graph"); } @@ -62,7 +62,7 @@ fn test_partition_into_paths_solver() { let problem = PartitionIntoPathsOfLength2::new(graph); let solver = BruteForce::new(); - let solutions = solver.find_all_satisfying(&problem); + let solutions = solver.find_all_witnesses(&problem); assert!(!solutions.is_empty(), "Expected at least one solution"); for sol in &solutions { diff --git a/src/unit_tests/models/graph/partition_into_triangles.rs b/src/unit_tests/models/graph/partition_into_triangles.rs index 47b777e22..f1ad4d4f8 100644 --- a/src/unit_tests/models/graph/partition_into_triangles.rs +++ b/src/unit_tests/models/graph/partition_into_triangles.rs @@ -1,5 +1,5 @@ use super::*; -use crate::solvers::{BruteForce, Solver}; +use crate::solvers::BruteForce; use crate::topology::SimpleGraph; #[test] @@ -51,7 +51,7 @@ fn test_partitionintotriangles_no_solution() { // No valid partition exists since there are no triangles let solver = BruteForce::new(); - let solution = solver.find_satisfying(&problem); + let solution = solver.find_witness(&problem); assert!(solution.is_none()); } @@ -64,13 +64,13 @@ fn test_partitionintotriangles_solver() { let problem = PartitionIntoTriangles::new(graph); let solver = BruteForce::new(); - let solution = solver.find_satisfying(&problem); + let solution = solver.find_witness(&problem); assert!(solution.is_some()); let sol = solution.unwrap(); assert!(problem.evaluate(&sol)); // All solutions should be valid - let all = solver.find_all_satisfying(&problem); + let all = solver.find_all_witnesses(&problem); assert!(!all.is_empty()); for s in &all { assert!(problem.evaluate(s)); @@ -139,6 +139,6 @@ fn test_partitionintotriangles_paper_example() { assert!(problem.evaluate(&[0, 0, 0, 1, 1, 1])); let solver = BruteForce::new(); - let solution = solver.find_satisfying(&problem); + let solution = solver.find_witness(&problem); assert!(solution.is_some()); } 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 0fb6625b7..87cbab8c0 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,5 @@ use super::*; -use crate::solvers::{BruteForce, Solver}; +use crate::solvers::BruteForce; use crate::topology::DirectedGraph; use crate::traits::Problem; @@ -84,11 +84,11 @@ fn test_path_constrained_network_flow_solver_yes_and_no() { let no = no_instance(); let solver = BruteForce::new(); - let satisfying = solver.find_all_satisfying(&yes); + let satisfying = solver.find_all_witnesses(&yes); assert_eq!(satisfying.len(), 2); assert!(satisfying.iter().all(|config| yes.evaluate(config))); - assert!(solver.find_satisfying(&no).is_none()); + assert!(solver.find_witness(&no).is_none()); } #[test] @@ -148,7 +148,7 @@ fn test_path_constrained_network_flow_paper_example() { assert!(problem.evaluate(&config)); - let all = solver.find_all_satisfying(&problem); + let all = solver.find_all_witnesses(&problem); assert_eq!(all.len(), 2); assert!(all.contains(&config)); } diff --git a/src/unit_tests/models/graph/rooted_tree_arrangement.rs b/src/unit_tests/models/graph/rooted_tree_arrangement.rs index 2e162c649..97040a1c3 100644 --- a/src/unit_tests/models/graph/rooted_tree_arrangement.rs +++ b/src/unit_tests/models/graph/rooted_tree_arrangement.rs @@ -1,5 +1,5 @@ use super::*; -use crate::solvers::{BruteForce, Solver}; +use crate::solvers::BruteForce; use crate::topology::SimpleGraph; use crate::traits::Problem; @@ -86,7 +86,7 @@ fn test_rootedtreearrangement_solver_and_serialization() { let solver = BruteForce::new(); let solution = solver - .find_satisfying(&problem) + .find_witness(&problem) .expect("expected satisfying solution"); assert!(problem.evaluate(&solution)); diff --git a/src/unit_tests/models/graph/rural_postman.rs b/src/unit_tests/models/graph/rural_postman.rs index 9e72648bb..9f9364f78 100644 --- a/src/unit_tests/models/graph/rural_postman.rs +++ b/src/unit_tests/models/graph/rural_postman.rs @@ -1,5 +1,5 @@ use super::*; -use crate::solvers::{BruteForce, Solver}; +use crate::solvers::BruteForce; use crate::topology::SimpleGraph; use crate::traits::Problem; @@ -143,7 +143,7 @@ fn test_rural_postman_disconnected_selection() { fn test_rural_postman_brute_force_finds_solution() { let problem = chinese_postman_rpp(); let solver = BruteForce::new(); - let result = solver.find_satisfying(&problem); + let result = solver.find_witness(&problem); assert!(result.is_some()); let sol = result.unwrap(); assert!(problem.evaluate(&sol)); @@ -153,7 +153,7 @@ fn test_rural_postman_brute_force_finds_solution() { fn test_rural_postman_brute_force_hexagon() { let problem = hexagon_rpp(); let solver = BruteForce::new(); - let result = solver.find_satisfying(&problem); + let result = solver.find_witness(&problem); assert!(result.is_some()); let sol = result.unwrap(); assert!(problem.evaluate(&sol)); @@ -171,18 +171,18 @@ fn test_rural_postman_brute_force_no_solution() { let bound = 4; let problem = RuralPostman::new(graph, edge_lengths, required_edges, bound); let solver = BruteForce::new(); - let result = solver.find_satisfying(&problem); + let result = solver.find_witness(&problem); assert!(result.is_none()); } #[test] -fn test_rural_postman_find_all_satisfying() { +fn test_rural_postman_find_all_witnesses() { // Issue #248 instance 1: hexagonal graph, 6 vertices, 8 edges // Required edges E'={{0,1},{2,3},{4,5}}, B=6 // Search space = 3^8 = 6561 let problem = hexagon_rpp(); let solver = BruteForce::new(); - let solutions = solver.find_all_satisfying(&problem); + let solutions = solver.find_all_witnesses(&problem); for sol in &solutions { assert!(problem.evaluate(sol)); } @@ -205,7 +205,7 @@ fn test_rural_postman_find_all_satisfying_empty() { let bound = 4; let problem = RuralPostman::new(graph, edge_lengths, required_edges, bound); let solver = BruteForce::new(); - assert!(solver.find_all_satisfying(&problem).is_empty()); + assert!(solver.find_all_witnesses(&problem).is_empty()); } #[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 0e03b95b9..18f1dbc89 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,5 @@ use super::*; -use crate::solvers::{BruteForce, Solver}; +use crate::solvers::BruteForce; use crate::topology::SimpleGraph; use crate::traits::Problem; @@ -65,11 +65,11 @@ fn test_shortest_weight_constrained_path_accessors() { fn test_shortest_weight_constrained_path_bruteforce() { let problem = issue_problem(); let solver = BruteForce::new(); - let solution = solver.find_satisfying(&problem); + let solution = solver.find_witness(&problem); assert!(solution.is_some()); assert!(problem.evaluate(&solution.unwrap())); - let all = solver.find_all_satisfying(&problem); + let all = solver.find_all_witnesses(&problem); assert_eq!(all.len(), 2); for config in &all { assert!(problem.evaluate(config)); @@ -100,7 +100,7 @@ fn test_shortest_weight_constrained_path_no_solution() { 4, ); let solver = BruteForce::new(); - assert!(solver.find_satisfying(&problem).is_none()); + assert!(solver.find_witness(&problem).is_none()); } #[test] @@ -130,7 +130,7 @@ fn test_shortestweightconstrainedpath_paper_example() { let problem = issue_problem(); assert!(problem.evaluate(&[0, 1, 0, 1, 0, 1, 0, 0])); - let all = BruteForce::new().find_all_satisfying(&problem); + let all = BruteForce::new().find_all_witnesses(&problem); assert_eq!(all.len(), 2); } diff --git a/src/unit_tests/models/graph/spin_glass.rs b/src/unit_tests/models/graph/spin_glass.rs index ae05d127f..239d00c1b 100644 --- a/src/unit_tests/models/graph/spin_glass.rs +++ b/src/unit_tests/models/graph/spin_glass.rs @@ -1,7 +1,7 @@ use super::*; use crate::solvers::BruteForce; -use crate::traits::{OptimizationProblem, Problem}; -use crate::types::Direction; +use crate::traits::{ObjectiveProblem, Problem}; +use crate::types::ExtremumSense; include!("../../jl_helpers.rs"); #[test] @@ -70,7 +70,7 @@ fn test_compute_energy_with_fields() { #[test] fn test_direction() { let problem = SpinGlass::::without_fields(2, vec![]); - assert_eq!(problem.direction(), Direction::Minimize); + assert_eq!(problem.direction(), ExtremumSense::Minimize); } #[test] @@ -130,7 +130,7 @@ fn test_jl_parity_evaluation() { config ); } - let best = BruteForce::new().find_all_best(&problem); + let best = BruteForce::new().find_all_witnesses(&problem); let jl_best = jl_flip_configs_set(&jl_parse_configs_set(&instance["best_solutions"])); let rust_best: HashSet> = best.into_iter().collect(); assert_eq!(rust_best, jl_best, "SpinGlass best solutions mismatch"); @@ -172,7 +172,7 @@ fn test_spinglass_paper_example() { assert_eq!(result.unwrap(), -3); // Verify this is optimal - let all_best = BruteForce::new().find_all_best(&problem); + let all_best = BruteForce::new().find_all_witnesses(&problem); assert!(!all_best.is_empty()); assert_eq!(problem.evaluate(&all_best[0]).unwrap(), -3); } diff --git a/src/unit_tests/models/graph/steiner_tree.rs b/src/unit_tests/models/graph/steiner_tree.rs index 1c9fc9026..98d525843 100644 --- a/src/unit_tests/models/graph/steiner_tree.rs +++ b/src/unit_tests/models/graph/steiner_tree.rs @@ -2,8 +2,8 @@ use super::*; use crate::{ solvers::BruteForce, topology::SimpleGraph, - traits::{OptimizationProblem, Problem}, - types::Direction, + traits::{ObjectiveProblem, Problem}, + types::ExtremumSense, }; /// Issue #122 example: 5 vertices, 7 edges, terminals {0, 2, 4}. @@ -37,7 +37,7 @@ fn test_steiner_tree_rejects_duplicate_terminals() { #[test] fn test_steiner_tree_direction() { let problem = example_instance(); - assert_eq!(problem.direction(), Direction::Minimize); + assert_eq!(problem.direction(), ExtremumSense::Minimize); } #[test] @@ -54,7 +54,7 @@ fn test_steiner_tree_evaluate_optimal() { // Optimal: edges (0,1)=2, (1,2)=2, (1,3)=1, (3,4)=1 => cost 6 // Edge indices: 0=(0,1), 2=(1,2), 3=(1,3), 6=(3,4) let config = vec![1, 0, 1, 1, 0, 0, 1]; - assert_eq!(problem.evaluate(&config), SolutionSize::Valid(6)); + assert_eq!(problem.evaluate(&config), Min(Some(6))); } #[test] @@ -62,7 +62,7 @@ fn test_steiner_tree_evaluate_invalid_disconnected() { let problem = example_instance(); // Only edge (0,1) — terminals 2, 4 unreachable let config = vec![1, 0, 0, 0, 0, 0, 0]; - assert_eq!(problem.evaluate(&config), SolutionSize::Invalid); + assert_eq!(problem.evaluate(&config), Min(None)); } #[test] @@ -70,25 +70,25 @@ fn test_steiner_tree_evaluate_invalid_cycle() { let problem = example_instance(); // Edges (0,1), (0,3), (1,2), (1,3), (3,4) — cycle 0-1-3-0 let config = vec![1, 1, 1, 1, 0, 0, 1]; - assert_eq!(problem.evaluate(&config), SolutionSize::Invalid); + assert_eq!(problem.evaluate(&config), Min(None)); } #[test] fn test_steiner_tree_evaluate_empty() { let problem = example_instance(); let config = vec![0; 7]; - assert_eq!(problem.evaluate(&config), SolutionSize::Invalid); + assert_eq!(problem.evaluate(&config), Min(None)); } #[test] fn test_steiner_tree_brute_force() { let problem = example_instance(); let solver = BruteForce::new(); - let solutions = solver.find_all_best(&problem); + let solutions = solver.find_all_witnesses(&problem); assert!(!solutions.is_empty()); // All optimal solutions should have cost 6 for sol in &solutions { - assert_eq!(problem.evaluate(sol), SolutionSize::Valid(6)); + assert_eq!(problem.evaluate(sol), Min(Some(6))); } } @@ -100,11 +100,11 @@ fn test_steiner_tree_all_terminals() { let terminals = vec![0, 1, 2]; let problem = SteinerTree::new(graph, edge_weights, terminals); let solver = BruteForce::new(); - let solutions = solver.find_all_best(&problem); + let solutions = solver.find_all_witnesses(&problem); assert!(!solutions.is_empty()); // MST = edges (0,1)=1, (1,2)=2 => cost 3 for sol in &solutions { - assert_eq!(problem.evaluate(sol), SolutionSize::Valid(3)); + assert_eq!(problem.evaluate(sol), Min(Some(3))); } } @@ -156,7 +156,7 @@ fn test_steiner_tree_disconnected_non_terminal_edges() { // Edges: 0=(0,1), 1=(1,2), 2=(2,3), 3=(3,4) // Select edges 0, 1, 3 — disconnected: {0,1,2} and {3,4} let config = vec![1, 1, 0, 1]; - assert_eq!(problem.evaluate(&config), SolutionSize::Invalid); + assert_eq!(problem.evaluate(&config), Min(None)); assert!(!problem.is_valid_solution(&config)); } @@ -171,7 +171,7 @@ fn test_steiner_tree_edge_weights_and_set_weights() { assert_eq!(problem.edge_weights(), &[1, 1, 1, 1, 1, 1, 1]); // The same tree (0,1),(1,2),(1,3),(3,4) now costs 4 let config = vec![1, 0, 1, 1, 0, 0, 1]; - assert_eq!(problem.evaluate(&config), SolutionSize::Valid(4)); + assert_eq!(problem.evaluate(&config), Min(Some(4))); } #[test] diff --git a/src/unit_tests/models/graph/steiner_tree_in_graphs.rs b/src/unit_tests/models/graph/steiner_tree_in_graphs.rs index 1333589c2..f84dee7a1 100644 --- a/src/unit_tests/models/graph/steiner_tree_in_graphs.rs +++ b/src/unit_tests/models/graph/steiner_tree_in_graphs.rs @@ -1,8 +1,8 @@ use super::*; -use crate::solvers::{BruteForce, Solver}; +use crate::solvers::BruteForce; use crate::topology::SimpleGraph; -use crate::traits::{OptimizationProblem, Problem}; -use crate::types::Direction; +use crate::traits::{ObjectiveProblem, Problem}; +use crate::types::ExtremumSense; #[test] fn test_steiner_tree_creation() { @@ -51,7 +51,7 @@ fn test_steiner_tree_evaluation() { fn test_steiner_tree_direction() { let graph = SimpleGraph::new(3, vec![(0, 1), (1, 2)]); let problem = SteinerTreeInGraphs::new(graph, vec![0, 2], vec![1i32; 2]); - assert_eq!(problem.direction(), Direction::Minimize); + assert_eq!(problem.direction(), ExtremumSense::Minimize); } #[test] @@ -69,7 +69,7 @@ fn test_steiner_tree_solver() { let problem = SteinerTreeInGraphs::new(graph, vec![0, 3], vec![2, 1, 2, 1]); let solver = BruteForce::new(); - let solution = solver.find_best(&problem).unwrap(); + let solution = solver.find_witness(&problem).unwrap(); let value = problem.evaluate(&solution); assert!(value.is_valid()); assert_eq!(value.unwrap(), 2); @@ -87,7 +87,7 @@ fn test_steiner_tree_with_steiner_vertices() { let problem = SteinerTreeInGraphs::new(graph, vec![0, 2, 3], vec![1i32; 3]); let solver = BruteForce::new(); - let solution = solver.find_best(&problem).unwrap(); + let solution = solver.find_witness(&problem).unwrap(); let value = problem.evaluate(&solution); assert!(value.is_valid()); assert_eq!(value.unwrap(), 3); @@ -158,7 +158,7 @@ fn test_steiner_tree_all_vertices_terminal() { let problem = SteinerTreeInGraphs::new(graph, vec![0, 1, 2], vec![1i32; 2]); let solver = BruteForce::new(); - let solution = solver.find_best(&problem).unwrap(); + let solution = solver.find_witness(&problem).unwrap(); let value = problem.evaluate(&solution); assert!(value.is_valid()); assert_eq!(value.unwrap(), 2); @@ -212,7 +212,7 @@ fn test_steiner_tree_example_from_issue() { // Brute-force verification: independently confirm optimal weight is 12 let solver = BruteForce::new(); - let solution = solver.find_best(&problem).unwrap(); + let solution = solver.find_witness(&problem).unwrap(); let value = problem.evaluate(&solution); assert!(value.is_valid()); assert_eq!(value.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 4231fffac..4577fb5ab 100644 --- a/src/unit_tests/models/graph/strong_connectivity_augmentation.rs +++ b/src/unit_tests/models/graph/strong_connectivity_augmentation.rs @@ -1,5 +1,5 @@ use super::*; -use crate::solvers::{BruteForce, Solver}; +use crate::solvers::BruteForce; use crate::topology::DirectedGraph; use crate::traits::Problem; @@ -117,10 +117,10 @@ fn test_strong_connectivity_augmentation_solver() { let problem = issue_example_yes(); let solver = BruteForce::new(); - let satisfying = solver.find_satisfying(&problem).unwrap(); + let satisfying = solver.find_witness(&problem).unwrap(); assert!(problem.evaluate(&satisfying)); - let all_satisfying = solver.find_all_satisfying(&problem); + let all_satisfying = solver.find_all_witnesses(&problem); assert_eq!(all_satisfying, vec![yes_config()]); } diff --git a/src/unit_tests/models/graph/subgraph_isomorphism.rs b/src/unit_tests/models/graph/subgraph_isomorphism.rs index 8065db508..b8248cd3f 100644 --- a/src/unit_tests/models/graph/subgraph_isomorphism.rs +++ b/src/unit_tests/models/graph/subgraph_isomorphism.rs @@ -1,5 +1,5 @@ use super::*; -use crate::solvers::{BruteForce, Solver}; +use crate::solvers::BruteForce; use crate::topology::SimpleGraph; use crate::traits::Problem; @@ -75,7 +75,7 @@ fn test_subgraph_isomorphism_no_solution() { // No possible mapping should work let solver = BruteForce::new(); - let solution = solver.find_satisfying(&problem); + let solution = solver.find_witness(&problem); assert!(solution.is_none()); } @@ -88,7 +88,7 @@ fn test_subgraph_isomorphism_solver() { let problem = SubgraphIsomorphism::new(host, pattern); let solver = BruteForce::new(); - let solution = solver.find_satisfying(&problem); + let solution = solver.find_witness(&problem); assert!(solution.is_some()); let sol = solution.unwrap(); @@ -104,7 +104,7 @@ fn test_subgraph_isomorphism_all_satisfying() { let problem = SubgraphIsomorphism::new(host, pattern); let solver = BruteForce::new(); - let solutions = solver.find_all_satisfying(&problem); + let solutions = solver.find_all_witnesses(&problem); // 3 edges in host, each can be mapped in 2 directions = 6 solutions assert_eq!(solutions.len(), 6); for sol in &solutions { @@ -188,7 +188,7 @@ fn test_subgraph_isomorphism_issue_example() { // Verify solver can find a solution let solver = BruteForce::new(); - let solution = solver.find_satisfying(&problem); + let solution = solver.find_witness(&problem); assert!(solution.is_some()); assert!(problem.evaluate(&solution.unwrap())); } diff --git a/src/unit_tests/models/graph/traveling_salesman.rs b/src/unit_tests/models/graph/traveling_salesman.rs index 256729509..4c8380387 100644 --- a/src/unit_tests/models/graph/traveling_salesman.rs +++ b/src/unit_tests/models/graph/traveling_salesman.rs @@ -1,8 +1,8 @@ use super::*; -use crate::solvers::{BruteForce, Solver}; +use crate::solvers::BruteForce; use crate::topology::SimpleGraph; -use crate::traits::{OptimizationProblem, Problem}; -use crate::types::{Direction, SolutionSize}; +use crate::traits::{ObjectiveProblem, Problem}; +use crate::types::{ExtremumSense, Min}; fn k4_tsp() -> TravelingSalesman { TravelingSalesman::new( @@ -46,7 +46,7 @@ fn test_evaluate_valid_cycle() { vec![(0, 1), (1, 2), (2, 3), (3, 4), (4, 0)], )); // Select all edges -> valid Hamiltonian cycle, cost = 5 - assert_eq!(problem.evaluate(&[1, 1, 1, 1, 1]), SolutionSize::Valid(5)); + assert_eq!(problem.evaluate(&[1, 1, 1, 1, 1]), Min(Some(5))); } #[test] @@ -55,7 +55,7 @@ fn test_evaluate_invalid_degree() { let problem = k4_tsp(); // edges: 0-1, 0-2, 0-3, 1-2, 1-3, 2-3 // Select first 3 edges (all incident to 0): degree(0)=3 -> Invalid - assert_eq!(problem.evaluate(&[1, 1, 1, 0, 0, 0]), SolutionSize::Invalid); + assert_eq!(problem.evaluate(&[1, 1, 1, 0, 0, 0]), Min(None)); } #[test] @@ -66,7 +66,7 @@ fn test_evaluate_invalid_not_connected() { vec![(0, 1), (1, 2), (0, 2), (3, 4), (4, 5), (3, 5)], )); // Select all 6 edges: two disjoint cycles, not a single Hamiltonian cycle - assert_eq!(problem.evaluate(&[1, 1, 1, 1, 1, 1]), SolutionSize::Invalid); + assert_eq!(problem.evaluate(&[1, 1, 1, 1, 1, 1]), Min(None)); } #[test] @@ -76,7 +76,7 @@ fn test_evaluate_invalid_wrong_edge_count() { 5, vec![(0, 1), (1, 2), (2, 3), (3, 4), (4, 0)], )); - assert_eq!(problem.evaluate(&[1, 1, 1, 1, 0]), SolutionSize::Invalid); + assert_eq!(problem.evaluate(&[1, 1, 1, 1, 0]), Min(None)); } #[test] @@ -85,7 +85,7 @@ fn test_evaluate_no_edges_selected() { 5, vec![(0, 1), (1, 2), (2, 3), (3, 4), (4, 0)], )); - assert_eq!(problem.evaluate(&[0, 0, 0, 0, 0]), SolutionSize::Invalid); + assert_eq!(problem.evaluate(&[0, 0, 0, 0, 0]), Min(None)); } #[test] @@ -93,11 +93,11 @@ fn test_brute_force_k4() { // Instance 1 from issue: K4 with weights let problem = k4_tsp(); let solver = BruteForce::new(); - let solutions = solver.find_all_best(&problem); + let solutions = solver.find_all_witnesses(&problem); assert!(!solutions.is_empty()); // Optimal cycle: 0->1->3->2->0, cost = 10+25+30+15 = 80 for sol in &solutions { - assert_eq!(problem.evaluate(sol), SolutionSize::Valid(80)); + assert_eq!(problem.evaluate(sol), Min(Some(80))); } } @@ -109,7 +109,7 @@ fn test_brute_force_path_graph_no_solution() { vec![(0, 1), (1, 2), (2, 3)], )); let solver = BruteForce::new(); - let solutions = solver.find_all_best(&problem); + let solutions = solver.find_all_witnesses(&problem); assert!(solutions.is_empty()); } @@ -121,10 +121,10 @@ fn test_brute_force_c5_unique_solution() { vec![(0, 1), (1, 2), (2, 3), (3, 4), (4, 0)], )); let solver = BruteForce::new(); - let solutions = solver.find_all_best(&problem); + let solutions = solver.find_all_witnesses(&problem); assert_eq!(solutions.len(), 1); assert_eq!(solutions[0], vec![1, 1, 1, 1, 1]); - assert_eq!(problem.evaluate(&solutions[0]), SolutionSize::Valid(5)); + assert_eq!(problem.evaluate(&solutions[0]), Min(Some(5))); } #[test] @@ -135,7 +135,7 @@ fn test_brute_force_bipartite_no_solution() { vec![(0, 2), (0, 3), (0, 4), (1, 2), (1, 3), (1, 4)], )); let solver = BruteForce::new(); - let solutions = solver.find_all_best(&problem); + let solutions = solver.find_all_witnesses(&problem); assert!(solutions.is_empty()); } @@ -145,7 +145,7 @@ fn test_direction() { 3, vec![(0, 1), (1, 2), (0, 2)], )); - assert_eq!(problem.direction(), Direction::Minimize); + assert_eq!(problem.direction(), ExtremumSense::Minimize); } #[test] @@ -217,10 +217,10 @@ fn test_brute_force_triangle_weighted() { vec![5, 10, 15], ); let solver = BruteForce::new(); - let solutions = solver.find_all_best(&problem); + let solutions = solver.find_all_witnesses(&problem); assert_eq!(solutions.len(), 1); assert_eq!(solutions[0], vec![1, 1, 1]); - assert_eq!(problem.evaluate(&solutions[0]), SolutionSize::Valid(30)); + assert_eq!(problem.evaluate(&solutions[0]), Min(Some(30))); } #[test] @@ -258,9 +258,9 @@ fn test_tsp_paper_example() { // Tour uses edges 0, 2, 3, 5 let config = vec![1, 0, 1, 1, 0, 1]; let result = problem.evaluate(&config); - assert_eq!(result, SolutionSize::Valid(6)); + assert_eq!(result, Min(Some(6))); let solver = BruteForce::new(); - let best = solver.find_best(&problem).unwrap(); - assert_eq!(problem.evaluate(&best), SolutionSize::Valid(6)); + let best = solver.find_witness(&problem).unwrap(); + assert_eq!(problem.evaluate(&best), Min(Some(6))); } 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 9cb507b54..ab54df324 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,5 @@ use super::*; -use crate::solvers::{BruteForce, Solver}; +use crate::solvers::BruteForce; use crate::topology::{Graph, SimpleGraph}; use crate::traits::Problem; @@ -59,7 +59,7 @@ fn test_undirected_flow_lower_bounds_evaluation_yes() { fn test_undirected_flow_lower_bounds_evaluation_no() { let problem = canonical_no_instance(); assert!(!problem.evaluate(&[0, 0, 0, 0])); - assert!(BruteForce::new().find_satisfying(&problem).is_none()); + assert!(BruteForce::new().find_witness(&problem).is_none()); } #[test] @@ -87,7 +87,7 @@ fn test_undirected_flow_lower_bounds_serialization() { fn test_undirected_flow_lower_bounds_solver_yes() { let problem = canonical_yes_instance(); let solution = BruteForce::new() - .find_satisfying(&problem) + .find_witness(&problem) .expect("expected a satisfying orientation"); assert!(problem.evaluate(&solution)); assert_eq!(solution.len(), problem.num_edges()); @@ -99,6 +99,6 @@ fn test_undirected_flow_lower_bounds_paper_example() { let config = yes_orientation_config(); assert!(problem.evaluate(&config)); - let all = BruteForce::new().find_all_satisfying(&problem); + let all = BruteForce::new().find_all_witnesses(&problem); assert!(all.contains(&config)); } 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 e34f7d92a..c34f21bc9 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,5 @@ use super::*; -use crate::solvers::{BruteForce, Solver}; +use crate::solvers::BruteForce; use crate::topology::{Graph, SimpleGraph}; use crate::traits::Problem; @@ -66,7 +66,7 @@ fn test_undirected_two_commodity_integral_flow_evaluation_no_shared_bottleneck() let problem = shared_bottleneck_instance(); assert!(!problem.evaluate(&example_config())); assert!(!problem.is_valid_solution(&example_config())); - assert!(BruteForce::new().find_satisfying(&problem).is_none()); + assert!(BruteForce::new().find_witness(&problem).is_none()); } #[test] @@ -118,7 +118,7 @@ fn test_undirected_two_commodity_integral_flow_paper_example() { let config = example_config(); assert!(problem.evaluate(&config)); - let all = BruteForce::new().find_all_satisfying(&problem); + let all = BruteForce::new().find_all_witnesses(&problem); assert_eq!(all.len(), 2); assert!(all.contains(&config)); } diff --git a/src/unit_tests/models/misc/additional_key.rs b/src/unit_tests/models/misc/additional_key.rs index da348277e..b66bf9148 100644 --- a/src/unit_tests/models/misc/additional_key.rs +++ b/src/unit_tests/models/misc/additional_key.rs @@ -1,5 +1,5 @@ use super::*; -use crate::solvers::{BruteForce, Solver}; +use crate::solvers::BruteForce; use crate::traits::Problem; /// Instance 1: 6 attributes, cyclic FDs, 3 known keys. @@ -81,7 +81,7 @@ fn test_additional_key_no_additional_key() { let problem = instance2(); // Only candidate key is {0}, which is already known. let solver = BruteForce::new(); - let solution = solver.find_satisfying(&problem); + let solution = solver.find_witness(&problem); assert!(solution.is_none()); } @@ -103,7 +103,7 @@ fn test_additional_key_brute_force() { let problem = instance1(); let solver = BruteForce::new(); let solution = solver - .find_satisfying(&problem) + .find_witness(&problem) .expect("should find a solution"); assert!(problem.evaluate(&solution)); } @@ -112,7 +112,7 @@ fn test_additional_key_brute_force() { fn test_additional_key_brute_force_all() { let problem = instance1(); let solver = BruteForce::new(); - let solutions = solver.find_all_satisfying(&problem); + let solutions = solver.find_all_witnesses(&problem); // Exactly 2 additional keys: {0,2} and {0,3,5} assert_eq!(solutions.len(), 2); for sol in &solutions { diff --git a/src/unit_tests/models/misc/bin_packing.rs b/src/unit_tests/models/misc/bin_packing.rs index 079e62da4..a661d9bd5 100644 --- a/src/unit_tests/models/misc/bin_packing.rs +++ b/src/unit_tests/models/misc/bin_packing.rs @@ -1,7 +1,7 @@ use super::*; -use crate::solvers::{BruteForce, Solver}; -use crate::traits::{OptimizationProblem, Problem}; -use crate::types::Direction; +use crate::solvers::BruteForce; +use crate::traits::{ObjectiveProblem, Problem}; +use crate::types::ExtremumSense; #[test] fn test_bin_packing_creation() { @@ -17,7 +17,7 @@ fn test_bin_packing_creation() { #[test] fn test_bin_packing_direction() { let problem = BinPacking::new(vec![1, 2, 3], 5); - assert_eq!(problem.direction(), Direction::Minimize); + assert_eq!(problem.direction(), ExtremumSense::Minimize); } #[test] @@ -70,7 +70,9 @@ fn test_bin_packing_brute_force_solver() { // Optimal: 3 bins (lower bound ceil(30/10) = 3) let problem = BinPacking::new(vec![6, 6, 5, 5, 4, 4], 10); let solver = BruteForce::new(); - let solution = solver.find_best(&problem).expect("should find a solution"); + let solution = solver + .find_witness(&problem) + .expect("should find a solution"); let metric = problem.evaluate(&solution); assert!(metric.is_valid()); assert_eq!(metric.unwrap(), 3); @@ -82,7 +84,9 @@ fn test_bin_packing_brute_force_small() { // Optimal: 2 bins (e.g., {3,4} + {3}) let problem = BinPacking::new(vec![3, 3, 4], 7); let solver = BruteForce::new(); - let solution = solver.find_best(&problem).expect("should find a solution"); + let solution = solver + .find_witness(&problem) + .expect("should find a solution"); let metric = problem.evaluate(&solution); assert!(metric.is_valid()); assert_eq!(metric.unwrap(), 2); 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 c41ee0f94..d036f415e 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 @@ -66,7 +66,7 @@ fn test_bcnf_evaluate_invalid_config_values() { fn test_bcnf_solver_finds_violation() { let problem = canonical_problem(); let solver = BruteForce::new(); - let solutions = solver.find_all_satisfying(&problem); + let solutions = solver.find_all_witnesses(&problem); assert!(!solutions.is_empty()); // All returned solutions must evaluate to true. for sol in &solutions { @@ -81,7 +81,7 @@ fn test_bcnf_no_violation_when_fds_trivial() { // Only trivial FD: {0} → {0}. No non-trivial closure possible. let problem = BoyceCoddNormalFormViolation::new(3, vec![(vec![0], vec![0])], vec![0, 1, 2]); let solver = BruteForce::new(); - let solutions = solver.find_all_satisfying(&problem); + let solutions = solver.find_all_witnesses(&problem); assert!(solutions.is_empty()); } @@ -187,7 +187,7 @@ fn test_bcnf_cyclic_keys_no_violation() { vec![0, 1, 2, 3], ); let solver = BruteForce::new(); - let solutions = solver.find_all_satisfying(&problem); + let solutions = solver.find_all_witnesses(&problem); assert!( solutions.is_empty(), "Cyclic-key instance should have no BCNF violation" diff --git a/src/unit_tests/models/misc/capacity_assignment.rs b/src/unit_tests/models/misc/capacity_assignment.rs index 46fcf78bc..ed9f04932 100644 --- a/src/unit_tests/models/misc/capacity_assignment.rs +++ b/src/unit_tests/models/misc/capacity_assignment.rs @@ -45,7 +45,7 @@ fn test_capacity_assignment_rejects_invalid_configs() { fn test_capacity_assignment_bruteforce_solution_count() { let problem = example_problem(); let solver = BruteForce::new(); - let solutions = solver.find_all_satisfying(&problem); + let solutions = solver.find_all_witnesses(&problem); assert_eq!(solutions.len(), 5); assert!(solutions.contains(&vec![1, 1, 1])); assert!(solutions.contains(&vec![0, 1, 2])); @@ -70,7 +70,7 @@ fn test_capacity_assignment_paper_example() { assert!(problem.evaluate(&config)); let solver = BruteForce::new(); - let solutions = solver.find_all_satisfying(&problem); + let solutions = solver.find_all_witnesses(&problem); assert_eq!(solutions.len(), 5); assert!(solutions.contains(&config)); } diff --git a/src/unit_tests/models/misc/conjunctive_boolean_query.rs b/src/unit_tests/models/misc/conjunctive_boolean_query.rs index 3da1f3cfc..3ca9e701e 100644 --- a/src/unit_tests/models/misc/conjunctive_boolean_query.rs +++ b/src/unit_tests/models/misc/conjunctive_boolean_query.rs @@ -1,5 +1,5 @@ use super::*; -use crate::solvers::{BruteForce, Solver}; +use crate::solvers::BruteForce; use crate::traits::Problem; /// Helper to build the issue example instance. @@ -85,7 +85,7 @@ fn test_conjunctivebooleanquery_brute_force() { let problem = issue_example(); let solver = BruteForce::new(); let solution = solver - .find_satisfying(&problem) + .find_witness(&problem) .expect("should find a solution"); assert!(problem.evaluate(&solution)); } @@ -105,7 +105,7 @@ fn test_conjunctivebooleanquery_unsatisfiable() { ]; let problem = ConjunctiveBooleanQuery::new(2, relations, 1, conjuncts); let solver = BruteForce::new(); - assert!(solver.find_satisfying(&problem).is_none()); + assert!(solver.find_witness(&problem).is_none()); } #[test] @@ -121,7 +121,7 @@ fn test_conjunctivebooleanquery_paper_example() { // Same instance as the issue example — count all satisfying assignments let problem = issue_example(); let solver = BruteForce::new(); - let all = solver.find_all_satisfying(&problem); + let all = solver.find_all_witnesses(&problem); // (0,1) satisfies; verify count manually: // For each (y0, y1) in {0..5}x{0..5}: // need R_0(y0, 3) and R_0(y1, 3) and R_1(y0, y1, 5) diff --git a/src/unit_tests/models/misc/conjunctive_query_foldability.rs b/src/unit_tests/models/misc/conjunctive_query_foldability.rs index 82daba5ae..b465cd46d 100644 --- a/src/unit_tests/models/misc/conjunctive_query_foldability.rs +++ b/src/unit_tests/models/misc/conjunctive_query_foldability.rs @@ -1,5 +1,5 @@ use super::*; -use crate::solvers::{BruteForce, Solver}; +use crate::solvers::BruteForce; use crate::traits::Problem; /// Build the YES instance (foldable): @@ -92,14 +92,14 @@ fn test_conjunctive_query_foldability_yes_instance() { fn test_conjunctive_query_foldability_no_instance() { let problem = no_instance(); // No substitution σ on {U0, U1, U2} maps the triangle into a 2-cycle - let result = BruteForce::new().find_satisfying(&problem); + let result = BruteForce::new().find_witness(&problem); assert_eq!(result, None); } #[test] fn test_conjunctive_query_foldability_solver() { let problem = yes_instance(); - let result = BruteForce::new().find_satisfying(&problem); + let result = BruteForce::new().find_witness(&problem); assert!( result.is_some(), "YES instance must have a satisfying config" @@ -141,7 +141,7 @@ fn test_conjunctive_query_foldability_paper_example() { // Enumerate all satisfying configs. // U(2) (= a) does not appear in Q1, so σ(U2) is a free choice (4 values). // Only σ(U0)=3 and σ(U1)=3 are required; σ(U2) can be anything in 0..4. - let all = BruteForce::new().find_all_satisfying(&problem); + let all = BruteForce::new().find_all_witnesses(&problem); assert_eq!( all.len(), 4, 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 7826720ef..949f2c75a 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,5 @@ use super::*; -use crate::solvers::{BruteForce, Solver}; +use crate::solvers::BruteForce; use crate::traits::Problem; fn issue_yes_instance() -> ConsistencyOfDatabaseFrequencyTables { @@ -113,7 +113,7 @@ fn test_cdft_bruteforce_finds_small_satisfying_assignment() { let problem = small_yes_instance(); let solver = BruteForce::new(); let solution = solver - .find_satisfying(&problem) + .find_witness(&problem) .expect("small instance should be satisfiable"); assert!(problem.evaluate(&solution)); } @@ -122,7 +122,7 @@ fn test_cdft_bruteforce_finds_small_satisfying_assignment() { fn test_cdft_bruteforce_detects_small_unsat_instance() { let problem = small_no_instance(); let solver = BruteForce::new(); - assert!(solver.find_satisfying(&problem).is_none()); + assert!(solver.find_witness(&problem).is_none()); } #[test] diff --git a/src/unit_tests/models/misc/ensemble_computation.rs b/src/unit_tests/models/misc/ensemble_computation.rs index aa19b48bb..eed2489eb 100644 --- a/src/unit_tests/models/misc/ensemble_computation.rs +++ b/src/unit_tests/models/misc/ensemble_computation.rs @@ -1,5 +1,5 @@ use super::*; -use crate::solvers::{BruteForce, Solver}; +use crate::solvers::BruteForce; use crate::traits::Problem; fn issue_problem() -> EnsembleComputation { @@ -62,11 +62,11 @@ fn test_ensemble_computation_small_bruteforce_instance() { let problem = EnsembleComputation::new(2, vec![vec![0, 1]], 1); let solver = BruteForce::new(); - let satisfying = solver.find_all_satisfying(&problem); + let satisfying = solver.find_all_witnesses(&problem); assert_eq!(satisfying.len(), 2); assert!(satisfying.contains(&vec![0, 1])); assert!(satisfying.contains(&vec![1, 0])); - assert_eq!(solver.find_satisfying(&problem), Some(vec![0, 1])); + assert_eq!(solver.find_witness(&problem), Some(vec![0, 1])); } #[test] diff --git a/src/unit_tests/models/misc/expected_retrieval_cost.rs b/src/unit_tests/models/misc/expected_retrieval_cost.rs index 193e90f9c..9eb95fa8c 100644 --- a/src/unit_tests/models/misc/expected_retrieval_cost.rs +++ b/src/unit_tests/models/misc/expected_retrieval_cost.rs @@ -1,5 +1,5 @@ use super::ExpectedRetrievalCost; -use crate::solvers::{BruteForce, Solver}; +use crate::solvers::BruteForce; use crate::traits::Problem; const EPS: f64 = 1e-9; @@ -66,7 +66,7 @@ fn test_expected_retrieval_cost_rejects_invalid_configs() { fn test_expected_retrieval_cost_solver_finds_satisfying_assignment() { let problem = yes_problem(); let solver = BruteForce::new(); - let solution = solver.find_satisfying(&problem).unwrap(); + let solution = solver.find_witness(&problem).unwrap(); assert!(problem.evaluate(&solution)); } @@ -77,7 +77,7 @@ fn test_expected_retrieval_cost_paper_example() { assert!(problem.evaluate(&config)); let solver = BruteForce::new(); - let satisfying = solver.find_all_satisfying(&problem); + let satisfying = solver.find_all_witnesses(&problem); assert_eq!(satisfying.len(), 54); } diff --git a/src/unit_tests/models/misc/factoring.rs b/src/unit_tests/models/misc/factoring.rs index d2f8aa0d6..25f4b1c42 100644 --- a/src/unit_tests/models/misc/factoring.rs +++ b/src/unit_tests/models/misc/factoring.rs @@ -1,7 +1,7 @@ use super::*; use crate::solvers::BruteForce; -use crate::traits::{OptimizationProblem, Problem}; -use crate::types::Direction; +use crate::traits::{ObjectiveProblem, Problem}; +use crate::types::ExtremumSense; include!("../../jl_helpers.rs"); #[test] @@ -53,7 +53,7 @@ fn test_is_factoring_function() { #[test] fn test_direction() { let problem = Factoring::new(2, 2, 6); - assert_eq!(problem.direction(), Direction::Minimize); + assert_eq!(problem.direction(), ExtremumSense::Minimize); } #[test] @@ -90,7 +90,7 @@ fn test_jl_parity_evaluation() { ); } } - let best = BruteForce::new().find_all_best(&problem); + let best = BruteForce::new().find_all_witnesses(&problem); let jl_best = jl_parse_configs_set(&instance["best_solutions"]); let rust_best: HashSet> = best.into_iter().collect(); assert_eq!(rust_best, jl_best, "Factoring best solutions mismatch"); diff --git a/src/unit_tests/models/misc/flow_shop_scheduling.rs b/src/unit_tests/models/misc/flow_shop_scheduling.rs index 58b0490a3..d99880207 100644 --- a/src/unit_tests/models/misc/flow_shop_scheduling.rs +++ b/src/unit_tests/models/misc/flow_shop_scheduling.rs @@ -1,5 +1,5 @@ use super::*; -use crate::solvers::{BruteForce, Solver}; +use crate::solvers::BruteForce; use crate::traits::Problem; #[test] @@ -121,7 +121,7 @@ fn test_flow_shop_scheduling_brute_force_solver() { // Small instance: 2 machines, 3 jobs, generous deadline let problem = FlowShopScheduling::new(2, vec![vec![3, 2], vec![2, 4], vec![1, 3]], 20); let solver = BruteForce::new(); - let solution = solver.find_satisfying(&problem); + let solution = solver.find_witness(&problem); assert!(solution.is_some()); let config = solution.unwrap(); assert!(problem.evaluate(&config)); @@ -137,7 +137,7 @@ fn test_flow_shop_scheduling_brute_force_unsatisfiable() { // Deadline 10 < 15 => unsatisfiable let problem = FlowShopScheduling::new(2, vec![vec![5, 5], vec![5, 5]], 10); let solver = BruteForce::new(); - let solution = solver.find_satisfying(&problem); + let solution = solver.find_witness(&problem); assert!(solution.is_none()); } @@ -151,7 +151,7 @@ fn test_flow_shop_scheduling_empty() { } #[test] -fn test_flow_shop_scheduling_find_all_satisfying() { +fn test_flow_shop_scheduling_find_all_witnesses() { // Issue #507 example: 3 machines, 5 jobs, D=25 // Search space = 5! = 120 permutations let problem = FlowShopScheduling::new( @@ -166,7 +166,7 @@ fn test_flow_shop_scheduling_find_all_satisfying() { 25, ); let solver = BruteForce::new(); - let solutions = solver.find_all_satisfying(&problem); + let solutions = solver.find_all_witnesses(&problem); for sol in &solutions { assert!(problem.evaluate(sol)); } @@ -183,7 +183,7 @@ fn test_flow_shop_scheduling_find_all_satisfying_empty() { // Both orderings give makespan 15 > 10 let problem = FlowShopScheduling::new(2, vec![vec![5, 5], vec![5, 5]], 10); let solver = BruteForce::new(); - assert!(solver.find_all_satisfying(&problem).is_empty()); + assert!(solver.find_all_witnesses(&problem).is_empty()); } #[test] diff --git a/src/unit_tests/models/misc/knapsack.rs b/src/unit_tests/models/misc/knapsack.rs index b87505fa0..12028ce0b 100644 --- a/src/unit_tests/models/misc/knapsack.rs +++ b/src/unit_tests/models/misc/knapsack.rs @@ -1,7 +1,7 @@ use super::*; -use crate::solvers::{BruteForce, Solver}; -use crate::traits::{OptimizationProblem, Problem}; -use crate::types::Direction; +use crate::solvers::BruteForce; +use crate::traits::{ObjectiveProblem, Problem}; +use crate::types::ExtremumSense; #[test] fn test_knapsack_basic() { @@ -11,7 +11,7 @@ fn test_knapsack_basic() { assert_eq!(problem.values(), &[3, 4, 5, 7]); assert_eq!(problem.capacity(), 7); assert_eq!(problem.dims(), vec![2; 4]); - assert_eq!(problem.direction(), Direction::Maximize); + assert_eq!(problem.direction(), ExtremumSense::Maximize); assert_eq!(::NAME, "Knapsack"); assert_eq!(::variant(), vec![]); } @@ -19,44 +19,44 @@ fn test_knapsack_basic() { #[test] fn test_knapsack_evaluate_optimal() { let problem = Knapsack::new(vec![2, 3, 4, 5], vec![3, 4, 5, 7], 7); - assert_eq!(problem.evaluate(&[1, 0, 0, 1]), SolutionSize::Valid(10)); + assert_eq!(problem.evaluate(&[1, 0, 0, 1]), Max(Some(10))); } #[test] fn test_knapsack_evaluate_feasible() { let problem = Knapsack::new(vec![2, 3, 4, 5], vec![3, 4, 5, 7], 7); - assert_eq!(problem.evaluate(&[1, 1, 0, 0]), SolutionSize::Valid(7)); + assert_eq!(problem.evaluate(&[1, 1, 0, 0]), Max(Some(7))); } #[test] fn test_knapsack_evaluate_overweight() { let problem = Knapsack::new(vec![2, 3, 4, 5], vec![3, 4, 5, 7], 7); - assert_eq!(problem.evaluate(&[0, 0, 1, 1]), SolutionSize::Invalid); + assert_eq!(problem.evaluate(&[0, 0, 1, 1]), Max(None)); } #[test] fn test_knapsack_evaluate_empty() { let problem = Knapsack::new(vec![2, 3, 4, 5], vec![3, 4, 5, 7], 7); - assert_eq!(problem.evaluate(&[0, 0, 0, 0]), SolutionSize::Valid(0)); + assert_eq!(problem.evaluate(&[0, 0, 0, 0]), Max(Some(0))); } #[test] fn test_knapsack_evaluate_all_selected() { let problem = Knapsack::new(vec![1, 1, 1], vec![10, 20, 30], 5); - assert_eq!(problem.evaluate(&[1, 1, 1]), SolutionSize::Valid(60)); + assert_eq!(problem.evaluate(&[1, 1, 1]), Max(Some(60))); } #[test] fn test_knapsack_evaluate_wrong_config_length() { let problem = Knapsack::new(vec![2, 3], vec![3, 4], 5); - assert_eq!(problem.evaluate(&[1]), SolutionSize::Invalid); - assert_eq!(problem.evaluate(&[1, 0, 0]), SolutionSize::Invalid); + assert_eq!(problem.evaluate(&[1]), Max(None)); + assert_eq!(problem.evaluate(&[1, 0, 0]), Max(None)); } #[test] fn test_knapsack_evaluate_invalid_variable_value() { let problem = Knapsack::new(vec![2, 3], vec![3, 4], 5); - assert_eq!(problem.evaluate(&[2, 0]), SolutionSize::Invalid); + assert_eq!(problem.evaluate(&[2, 0]), Max(None)); } #[test] @@ -64,16 +64,18 @@ fn test_knapsack_empty_instance() { let problem = Knapsack::new(vec![], vec![], 10); assert_eq!(problem.num_items(), 0); assert_eq!(problem.dims(), Vec::::new()); - assert_eq!(problem.evaluate(&[]), SolutionSize::Valid(0)); + assert_eq!(problem.evaluate(&[]), Max(Some(0))); } #[test] fn test_knapsack_brute_force() { let problem = Knapsack::new(vec![2, 3, 4, 5], vec![3, 4, 5, 7], 7); let solver = BruteForce::new(); - let solution = solver.find_best(&problem).expect("should find a solution"); + let solution = solver + .find_witness(&problem) + .expect("should find a solution"); let metric = problem.evaluate(&solution); - assert_eq!(metric, SolutionSize::Valid(10)); + assert_eq!(metric, Max(Some(10))); } #[test] @@ -90,22 +92,22 @@ fn test_knapsack_serialization() { fn test_knapsack_zero_capacity() { // Capacity 0: only empty set is feasible let problem = Knapsack::new(vec![1, 2], vec![10, 20], 0); - assert_eq!(problem.evaluate(&[0, 0]), SolutionSize::Valid(0)); - assert_eq!(problem.evaluate(&[1, 0]), SolutionSize::Invalid); + assert_eq!(problem.evaluate(&[0, 0]), Max(Some(0))); + assert_eq!(problem.evaluate(&[1, 0]), Max(None)); let solver = BruteForce::new(); - let solution = solver.find_best(&problem).unwrap(); - assert_eq!(problem.evaluate(&solution), SolutionSize::Valid(0)); + let solution = solver.find_witness(&problem).unwrap(); + assert_eq!(problem.evaluate(&solution), Max(Some(0))); } #[test] fn test_knapsack_single_item() { // Single item that fits let problem = Knapsack::new(vec![3], vec![5], 3); - assert_eq!(problem.evaluate(&[1]), SolutionSize::Valid(5)); - assert_eq!(problem.evaluate(&[0]), SolutionSize::Valid(0)); + assert_eq!(problem.evaluate(&[1]), Max(Some(5))); + assert_eq!(problem.evaluate(&[0]), Max(Some(0))); let solver = BruteForce::new(); - let solution = solver.find_best(&problem).unwrap(); - assert_eq!(problem.evaluate(&solution), SolutionSize::Valid(5)); + let solution = solver.find_witness(&problem).unwrap(); + assert_eq!(problem.evaluate(&solution), Max(Some(5))); } #[test] @@ -117,8 +119,8 @@ fn test_knapsack_greedy_not_optimal() { // Capacity=10. Greedy: {0} value=7. Optimal: {1,2} value=10. let problem = Knapsack::new(vec![6, 5, 5], vec![7, 5, 5], 10); let solver = BruteForce::new(); - let solution = solver.find_best(&problem).unwrap(); - assert_eq!(problem.evaluate(&solution), SolutionSize::Valid(10)); + let solution = solver.find_witness(&problem).unwrap(); + assert_eq!(problem.evaluate(&solution), Max(Some(10))); } #[test] diff --git a/src/unit_tests/models/misc/longest_common_subsequence.rs b/src/unit_tests/models/misc/longest_common_subsequence.rs index edc660b2a..325438387 100644 --- a/src/unit_tests/models/misc/longest_common_subsequence.rs +++ b/src/unit_tests/models/misc/longest_common_subsequence.rs @@ -1,5 +1,5 @@ use super::*; -use crate::solvers::{BruteForce, Solver}; +use crate::solvers::BruteForce; use crate::traits::Problem; fn issue_yes_instance() -> LongestCommonSubsequence { @@ -82,7 +82,7 @@ fn test_lcs_bruteforce_yes() { let problem = issue_yes_instance(); let solver = BruteForce::new(); let solution = solver - .find_satisfying(&problem) + .find_witness(&problem) .expect("expected a common subsequence witness"); assert!(problem.evaluate(&solution)); } @@ -91,14 +91,14 @@ fn test_lcs_bruteforce_yes() { fn test_lcs_bruteforce_no() { let problem = issue_no_instance(); let solver = BruteForce::new(); - assert!(solver.find_satisfying(&problem).is_none()); + assert!(solver.find_witness(&problem).is_none()); } #[test] fn test_lcs_find_all_satisfying_contains_issue_witness() { let problem = issue_yes_instance(); let solver = BruteForce::new(); - let satisfying = solver.find_all_satisfying(&problem); + let satisfying = solver.find_all_witnesses(&problem); assert!(satisfying.iter().any(|config| config == &vec![0, 1, 0])); } @@ -117,7 +117,7 @@ fn test_lcs_paper_example() { assert!(problem.evaluate(&[0, 1, 0])); let solver = BruteForce::new(); - let satisfying = solver.find_all_satisfying(&problem); + let satisfying = solver.find_all_witnesses(&problem); assert!(!satisfying.is_empty()); } diff --git a/src/unit_tests/models/misc/minimum_tardiness_sequencing.rs b/src/unit_tests/models/misc/minimum_tardiness_sequencing.rs index 6d85c5ace..6e1c0b1f1 100644 --- a/src/unit_tests/models/misc/minimum_tardiness_sequencing.rs +++ b/src/unit_tests/models/misc/minimum_tardiness_sequencing.rs @@ -1,7 +1,7 @@ use super::*; -use crate::solvers::{BruteForce, Solver}; -use crate::traits::{OptimizationProblem, Problem}; -use crate::types::Direction; +use crate::solvers::BruteForce; +use crate::traits::{ObjectiveProblem, Problem}; +use crate::types::ExtremumSense; #[test] fn test_minimum_tardiness_sequencing_basic() { @@ -15,7 +15,7 @@ fn test_minimum_tardiness_sequencing_basic() { assert_eq!(problem.precedences(), &[(0, 3), (1, 3), (1, 4), (2, 4)]); assert_eq!(problem.num_precedences(), 4); assert_eq!(problem.dims(), vec![5, 4, 3, 2, 1]); - assert_eq!(problem.direction(), Direction::Minimize); + assert_eq!(problem.direction(), ExtremumSense::Minimize); assert_eq!( ::NAME, "MinimumTardinessSequencing" @@ -37,28 +37,28 @@ fn test_minimum_tardiness_sequencing_evaluate_optimal() { // sigma: task 0 at pos 0, task 1 at pos 1, task 3 at pos 2, task 2 at pos 3, task 4 at pos 4. // t0 finishes at 1 <= 5, t1 at 2 <= 5, t3 at 3 <= 3, t2 at 4 <= 5, t4 at 5 > 3 (tardy) let config = vec![0, 0, 1, 0, 0]; - assert_eq!(problem.evaluate(&config), SolutionSize::Valid(1)); + assert_eq!(problem.evaluate(&config), Min(Some(1))); } #[test] fn test_minimum_tardiness_sequencing_evaluate_invalid_lehmer() { let problem = MinimumTardinessSequencing::new(3, vec![2, 3, 1], vec![]); // dims = [3, 2, 1]; config [0, 2, 0] has 2 >= 2 (second dim), invalid Lehmer code - assert_eq!(problem.evaluate(&[0, 2, 0]), SolutionSize::Invalid); + assert_eq!(problem.evaluate(&[0, 2, 0]), Min(None)); } #[test] fn test_minimum_tardiness_sequencing_evaluate_out_of_range() { let problem = MinimumTardinessSequencing::new(3, vec![2, 3, 1], vec![]); // dims = [3, 2, 1]; config [0, 1, 5] has 5 >= 1 (third dim), out of range - assert_eq!(problem.evaluate(&[0, 1, 5]), SolutionSize::Invalid); + assert_eq!(problem.evaluate(&[0, 1, 5]), Min(None)); } #[test] fn test_minimum_tardiness_sequencing_evaluate_wrong_length() { let problem = MinimumTardinessSequencing::new(3, vec![2, 3, 1], vec![]); - assert_eq!(problem.evaluate(&[0, 1]), SolutionSize::Invalid); - assert_eq!(problem.evaluate(&[0, 1, 2, 3]), SolutionSize::Invalid); + assert_eq!(problem.evaluate(&[0, 1]), Min(None)); + assert_eq!(problem.evaluate(&[0, 1, 2, 3]), Min(None)); } #[test] @@ -69,11 +69,11 @@ fn test_minimum_tardiness_sequencing_evaluate_precedence_violation() { vec![(0, 1)], // task 0 must precede task 1 ); // Lehmer [0,0,0] -> schedule [0,1,2] -> sigma [0,1,2]: sigma(0)=0 < sigma(1)=1, valid - assert_eq!(problem.evaluate(&[0, 0, 0]), SolutionSize::Valid(0)); + assert_eq!(problem.evaluate(&[0, 0, 0]), Min(Some(0))); // Lehmer [1,0,0] -> schedule [1,0,2] -> sigma [1,0,2]: sigma(0)=1 >= sigma(1)=0, violates - assert_eq!(problem.evaluate(&[1, 0, 0]), SolutionSize::Invalid); + assert_eq!(problem.evaluate(&[1, 0, 0]), Min(None)); // Lehmer [2,1,0] -> schedule [2,1,0] -> sigma [2,1,0]: sigma(0)=2 >= sigma(1)=1, violates - assert_eq!(problem.evaluate(&[2, 1, 0]), SolutionSize::Invalid); + assert_eq!(problem.evaluate(&[2, 1, 0]), Min(None)); } #[test] @@ -81,9 +81,9 @@ fn test_minimum_tardiness_sequencing_evaluate_all_on_time() { let problem = MinimumTardinessSequencing::new(3, vec![3, 3, 3], vec![]); // All deadlines are 3, so any permutation of 3 tasks is on time // Lehmer [0,0,0] -> schedule [0,1,2] - assert_eq!(problem.evaluate(&[0, 0, 0]), SolutionSize::Valid(0)); + assert_eq!(problem.evaluate(&[0, 0, 0]), Min(Some(0))); // Lehmer [2,1,0] -> schedule [2,1,0] - assert_eq!(problem.evaluate(&[2, 1, 0]), SolutionSize::Valid(0)); + assert_eq!(problem.evaluate(&[2, 1, 0]), Min(Some(0))); } #[test] @@ -94,7 +94,7 @@ fn test_minimum_tardiness_sequencing_evaluate_all_tardy() { let problem = MinimumTardinessSequencing::new(2, vec![0, 0], vec![]); // Lehmer [0,0] -> schedule [0,1] -> sigma [0,1] // pos 0 finishes at 1 > 0 (tardy), pos 1 finishes at 2 > 0 (tardy) - assert_eq!(problem.evaluate(&[0, 0]), SolutionSize::Valid(2)); + assert_eq!(problem.evaluate(&[0, 0]), Min(Some(2))); } #[test] @@ -105,10 +105,12 @@ fn test_minimum_tardiness_sequencing_brute_force() { vec![(0, 3), (1, 3), (1, 4), (2, 4)], ); let solver = BruteForce::new(); - let solution = solver.find_best(&problem).expect("should find a solution"); + let solution = solver + .find_witness(&problem) + .expect("should find a solution"); let metric = problem.evaluate(&solution); // Optimal is 1 tardy task - assert_eq!(metric, SolutionSize::Valid(1)); + assert_eq!(metric, Min(Some(1))); } #[test] @@ -117,10 +119,12 @@ fn test_minimum_tardiness_sequencing_brute_force_no_precedences() { // 3 tasks: deadlines 1, 3, 2. Best is to schedule task with deadline 1 first. let problem = MinimumTardinessSequencing::new(3, vec![1, 3, 2], vec![]); let solver = BruteForce::new(); - let solution = solver.find_best(&problem).expect("should find a solution"); + let solution = solver + .find_witness(&problem) + .expect("should find a solution"); let metric = problem.evaluate(&solution); // All can be on time: t0 at pos 0 (finish 1 <= 1), t2 at pos 1 (finish 2 <= 2), t1 at pos 2 (finish 3 <= 3) - assert_eq!(metric, SolutionSize::Valid(0)); + assert_eq!(metric, Min(Some(0))); } #[test] @@ -138,7 +142,7 @@ fn test_minimum_tardiness_sequencing_empty() { let problem = MinimumTardinessSequencing::new(0, vec![], vec![]); assert_eq!(problem.num_tasks(), 0); assert_eq!(problem.dims(), Vec::::new()); - assert_eq!(problem.evaluate(&[]), SolutionSize::Valid(0)); + assert_eq!(problem.evaluate(&[]), Min(Some(0))); } #[test] @@ -146,11 +150,11 @@ fn test_minimum_tardiness_sequencing_single_task() { let problem = MinimumTardinessSequencing::new(1, vec![1], vec![]); assert_eq!(problem.dims(), vec![1]); // Task at position 0, finishes at 1 <= 1, not tardy - assert_eq!(problem.evaluate(&[0]), SolutionSize::Valid(0)); + assert_eq!(problem.evaluate(&[0]), Min(Some(0))); let problem_tardy = MinimumTardinessSequencing::new(1, vec![0], vec![]); // Task at position 0, finishes at 1 > 0, tardy - assert_eq!(problem_tardy.evaluate(&[0]), SolutionSize::Valid(1)); + assert_eq!(problem_tardy.evaluate(&[0]), Min(Some(1))); } #[test] @@ -170,5 +174,5 @@ fn test_minimum_tardiness_sequencing_cyclic_precedences() { // Cyclic precedences: 0 -> 1 -> 2 -> 0. No valid schedule exists. let problem = MinimumTardinessSequencing::new(3, vec![3, 3, 3], vec![(0, 1), (1, 2), (2, 0)]); let solver = BruteForce::new(); - assert!(solver.find_best(&problem).is_none()); + assert!(solver.find_witness(&problem).is_none()); } diff --git a/src/unit_tests/models/misc/multiprocessor_scheduling.rs b/src/unit_tests/models/misc/multiprocessor_scheduling.rs index ee37b078e..f8853869e 100644 --- a/src/unit_tests/models/misc/multiprocessor_scheduling.rs +++ b/src/unit_tests/models/misc/multiprocessor_scheduling.rs @@ -1,5 +1,5 @@ use super::*; -use crate::solvers::{BruteForce, Solver}; +use crate::solvers::BruteForce; use crate::traits::Problem; #[test] @@ -91,7 +91,7 @@ fn test_multiprocessor_scheduling_three_processors() { fn test_multiprocessor_scheduling_brute_force() { let problem = MultiprocessorScheduling::new(vec![4, 5, 3, 2, 6], 2, 10); let solver = BruteForce::new(); - let solution = solver.find_satisfying(&problem); + let solution = solver.find_witness(&problem); assert!(solution.is_some()); let config = solution.unwrap(); assert!(problem.evaluate(&config)); @@ -102,17 +102,17 @@ fn test_multiprocessor_scheduling_brute_force_infeasible() { // Total length = 20, with 2 processors and deadline 9, impossible let problem = MultiprocessorScheduling::new(vec![4, 5, 3, 2, 6], 2, 9); let solver = BruteForce::new(); - let solution = solver.find_satisfying(&problem); + let solution = solver.find_witness(&problem); assert!(solution.is_none()); } #[test] -fn test_multiprocessor_scheduling_find_all_satisfying() { +fn test_multiprocessor_scheduling_find_all_witnesses() { // Issue #212 example: 5 tasks [4,5,3,2,6], m=2, D=10 // Search space = 2^5 = 32 let problem = MultiprocessorScheduling::new(vec![4, 5, 3, 2, 6], 2, 10); let solver = BruteForce::new(); - let solutions = solver.find_all_satisfying(&problem); + let solutions = solver.find_all_witnesses(&problem); for sol in &solutions { assert!(problem.evaluate(sol)); } @@ -128,7 +128,7 @@ fn test_multiprocessor_scheduling_find_all_satisfying_empty() { // but 20 > 2*9 = 18, so impossible let problem = MultiprocessorScheduling::new(vec![4, 5, 3, 2, 6], 2, 9); let solver = BruteForce::new(); - assert!(solver.find_all_satisfying(&problem).is_empty()); + assert!(solver.find_all_witnesses(&problem).is_empty()); } #[test] diff --git a/src/unit_tests/models/misc/paintshop.rs b/src/unit_tests/models/misc/paintshop.rs index 87c0f1607..72695b1bf 100644 --- a/src/unit_tests/models/misc/paintshop.rs +++ b/src/unit_tests/models/misc/paintshop.rs @@ -1,7 +1,7 @@ use super::*; -use crate::solvers::{BruteForce, Solver}; -use crate::traits::{OptimizationProblem, Problem}; -use crate::types::Direction; +use crate::solvers::BruteForce; +use crate::traits::{ObjectiveProblem, Problem}; +use crate::types::ExtremumSense; include!("../../jl_helpers.rs"); #[test] @@ -58,7 +58,7 @@ fn test_count_paint_switches_function() { #[test] fn test_direction() { let problem = PaintShop::new(vec!["a", "a"]); - assert_eq!(problem.direction(), Direction::Minimize); + assert_eq!(problem.direction(), ExtremumSense::Minimize); } #[test] @@ -66,7 +66,7 @@ fn test_single_car() { let problem = PaintShop::new(vec!["a", "a"]); let solver = BruteForce::new(); - let solutions = solver.find_all_best(&problem); + let solutions = solver.find_all_witnesses(&problem); // Both configs give 1 switch: a(0)->a(1) or a(1)->a(0) assert_eq!(solutions.len(), 2); for sol in &solutions { @@ -80,7 +80,7 @@ fn test_adjacent_same_car() { let problem = PaintShop::new(vec!["a", "a", "b", "b"]); let solver = BruteForce::new(); - let solutions = solver.find_all_best(&problem); + let solutions = solver.find_all_witnesses(&problem); // Best case: [0,0] -> [0,1,0,1] = 3 switches, or [0,1] -> [0,1,1,0] = 2 switches // Actually: [0,0] -> a=0,a=1,b=0,b=1 = [0,1,0,1] = 3 switches // [0,1] -> a=0,a=1,b=1,b=0 = [0,1,1,0] = 2 switches @@ -124,7 +124,7 @@ fn test_jl_parity_evaluation() { config ); } - let best = BruteForce::new().find_all_best(&problem); + let best = BruteForce::new().find_all_witnesses(&problem); let jl_best = jl_parse_configs_set(&instance["best_solutions"]); let rust_best: HashSet> = best.into_iter().collect(); assert_eq!(rust_best, jl_best, "PaintShop best solutions mismatch"); @@ -148,6 +148,6 @@ fn test_paintshop_paper_example() { // Config [0, 0, 1]: A first=0, B first=0, C first=1 // Coloring: A(0), B(0), A(1), C(1), B(1), C(0) -> [0,0,1,1,1,0] -> 2 switches let solver = BruteForce::new(); - let best = solver.find_best(&problem).unwrap(); + let best = solver.find_witness(&problem).unwrap(); assert_eq!(problem.evaluate(&best).unwrap(), 2); } diff --git a/src/unit_tests/models/misc/partially_ordered_knapsack.rs b/src/unit_tests/models/misc/partially_ordered_knapsack.rs index 013dff32d..0e72472ec 100644 --- a/src/unit_tests/models/misc/partially_ordered_knapsack.rs +++ b/src/unit_tests/models/misc/partially_ordered_knapsack.rs @@ -1,7 +1,7 @@ use super::*; -use crate::solvers::{BruteForce, Solver}; -use crate::traits::{OptimizationProblem, Problem}; -use crate::types::Direction; +use crate::solvers::BruteForce; +use crate::traits::{ObjectiveProblem, Problem}; +use crate::types::ExtremumSense; /// Helper: create the example instance from the issue. /// Items: a=0, b=1, c=2, d=3, e=4, f=5 @@ -30,7 +30,7 @@ fn test_partially_ordered_knapsack_basic() { ); assert_eq!(problem.capacity(), 11); assert_eq!(problem.dims(), vec![2; 6]); - assert_eq!(problem.direction(), Direction::Maximize); + assert_eq!(problem.direction(), ExtremumSense::Maximize); assert_eq!( ::NAME, "PartiallyOrderedKnapsack" @@ -44,10 +44,7 @@ fn test_partially_ordered_knapsack_evaluate_valid() { // U' = {a, b, d, e, f} = indices {0, 1, 3, 4, 5} // Total size: 2+3+1+2+3 = 11 <= 11 // Total value: 3+2+4+3+8 = 20 - assert_eq!( - problem.evaluate(&[1, 1, 0, 1, 1, 1]), - SolutionSize::Valid(20) - ); + assert_eq!(problem.evaluate(&[1, 1, 0, 1, 1, 1]), Max(Some(20))); } #[test] @@ -55,7 +52,7 @@ fn test_partially_ordered_knapsack_evaluate_precedence_violation() { let problem = example_instance(); // U' = {d, f} = indices {3, 5} — f requires e and b (transitively), d requires a // Not downward-closed: d selected but a (predecessor of d) not selected - assert_eq!(problem.evaluate(&[0, 0, 0, 1, 0, 1]), SolutionSize::Invalid); + assert_eq!(problem.evaluate(&[0, 0, 0, 1, 0, 1]), Max(None)); } #[test] @@ -64,7 +61,7 @@ fn test_partially_ordered_knapsack_evaluate_transitive_precedence_violation() { // U' = {d, e, f} = indices {3, 4, 5} // f requires d (ok) and e (ok), but d requires a (0) which is not selected // Also e requires b (1) which is not selected - assert_eq!(problem.evaluate(&[0, 0, 0, 1, 1, 1]), SolutionSize::Invalid); + assert_eq!(problem.evaluate(&[0, 0, 0, 1, 1, 1]), Max(None)); } #[test] @@ -72,26 +69,20 @@ fn test_partially_ordered_knapsack_evaluate_overweight() { let problem = example_instance(); // U' = {a, b, c, d, e, f} = all items // Total size: 2+3+4+1+2+3 = 15 > 11 - assert_eq!(problem.evaluate(&[1, 1, 1, 1, 1, 1]), SolutionSize::Invalid); + assert_eq!(problem.evaluate(&[1, 1, 1, 1, 1, 1]), Max(None)); } #[test] fn test_partially_ordered_knapsack_evaluate_empty() { let problem = example_instance(); - assert_eq!( - problem.evaluate(&[0, 0, 0, 0, 0, 0]), - SolutionSize::Valid(0) - ); + assert_eq!(problem.evaluate(&[0, 0, 0, 0, 0, 0]), Max(Some(0))); } #[test] fn test_partially_ordered_knapsack_evaluate_single_root() { let problem = example_instance(); // Just item a (no predecessors) - assert_eq!( - problem.evaluate(&[1, 0, 0, 0, 0, 0]), - SolutionSize::Valid(3) - ); + assert_eq!(problem.evaluate(&[1, 0, 0, 0, 0, 0]), Max(Some(3))); } #[test] @@ -100,36 +91,32 @@ fn test_partially_ordered_knapsack_evaluate_valid_chain() { // U' = {a, d} = indices {0, 3} // a has no predecessors, d's predecessor a is selected: downward-closed // Total size: 2+1 = 3 <= 11, Total value: 3+4 = 7 - assert_eq!( - problem.evaluate(&[1, 0, 0, 1, 0, 0]), - SolutionSize::Valid(7) - ); + assert_eq!(problem.evaluate(&[1, 0, 0, 1, 0, 0]), Max(Some(7))); } #[test] fn test_partially_ordered_knapsack_evaluate_wrong_config_length() { let problem = example_instance(); - assert_eq!(problem.evaluate(&[1, 0]), SolutionSize::Invalid); - assert_eq!( - problem.evaluate(&[1, 0, 0, 0, 0, 0, 0]), - SolutionSize::Invalid - ); + assert_eq!(problem.evaluate(&[1, 0]), Max(None)); + assert_eq!(problem.evaluate(&[1, 0, 0, 0, 0, 0, 0]), Max(None)); } #[test] fn test_partially_ordered_knapsack_evaluate_invalid_variable_value() { let problem = example_instance(); - assert_eq!(problem.evaluate(&[2, 0, 0, 0, 0, 0]), SolutionSize::Invalid); + assert_eq!(problem.evaluate(&[2, 0, 0, 0, 0, 0]), Max(None)); } #[test] fn test_partially_ordered_knapsack_brute_force() { let problem = example_instance(); let solver = BruteForce::new(); - let solution = solver.find_best(&problem).expect("should find a solution"); + let solution = solver + .find_witness(&problem) + .expect("should find a solution"); let metric = problem.evaluate(&solution); // The optimal should be {a, b, d, e, f} with value 20 - assert_eq!(metric, SolutionSize::Valid(20)); + assert_eq!(metric, Max(Some(20))); } #[test] @@ -138,7 +125,7 @@ fn test_partially_ordered_knapsack_empty_instance() { assert_eq!(problem.num_items(), 0); assert_eq!(problem.num_precedences(), 0); assert_eq!(problem.dims(), Vec::::new()); - assert_eq!(problem.evaluate(&[]), SolutionSize::Valid(0)); + assert_eq!(problem.evaluate(&[]), Max(Some(0))); } #[test] @@ -146,20 +133,22 @@ fn test_partially_ordered_knapsack_no_precedences() { // Without precedences, behaves like standard knapsack let problem = PartiallyOrderedKnapsack::new(vec![2, 3, 4, 5], vec![3, 4, 5, 7], vec![], 7); let solver = BruteForce::new(); - let solution = solver.find_best(&problem).expect("should find a solution"); + let solution = solver + .find_witness(&problem) + .expect("should find a solution"); let metric = problem.evaluate(&solution); // Same as standard knapsack: items 0 and 3 give weight 7, value 10 - assert_eq!(metric, SolutionSize::Valid(10)); + assert_eq!(metric, Max(Some(10))); } #[test] fn test_partially_ordered_knapsack_zero_capacity() { let problem = PartiallyOrderedKnapsack::new(vec![1, 2], vec![10, 20], vec![(0, 1)], 0); - assert_eq!(problem.evaluate(&[0, 0]), SolutionSize::Valid(0)); - assert_eq!(problem.evaluate(&[1, 0]), SolutionSize::Invalid); + assert_eq!(problem.evaluate(&[0, 0]), Max(Some(0))); + assert_eq!(problem.evaluate(&[1, 0]), Max(None)); let solver = BruteForce::new(); - let solution = solver.find_best(&problem).unwrap(); - assert_eq!(problem.evaluate(&solution), SolutionSize::Valid(0)); + let solution = solver.find_witness(&problem).unwrap(); + assert_eq!(problem.evaluate(&solution), Max(Some(0))); } #[test] diff --git a/src/unit_tests/models/misc/partition.rs b/src/unit_tests/models/misc/partition.rs index 308d27d49..3f031550f 100644 --- a/src/unit_tests/models/misc/partition.rs +++ b/src/unit_tests/models/misc/partition.rs @@ -1,5 +1,5 @@ use crate::models::misc::Partition; -use crate::solvers::{BruteForce, Solver}; +use crate::solvers::BruteForce; use crate::traits::Problem; #[test] @@ -45,14 +45,14 @@ fn test_partition_odd_total() { // Total = 7 (odd), no equal partition possible let problem = Partition::new(vec![3, 1, 2, 1]); let solver = BruteForce::new(); - assert!(solver.find_satisfying(&problem).is_none()); + assert!(solver.find_witness(&problem).is_none()); } #[test] fn test_partition_solver() { let problem = Partition::new(vec![3, 1, 1, 2, 2, 1]); let solver = BruteForce::new(); - let solution = solver.find_satisfying(&problem); + let solution = solver.find_witness(&problem); assert!(solution.is_some()); assert!(problem.evaluate(&solution.unwrap())); } @@ -61,7 +61,7 @@ fn test_partition_solver() { fn test_partition_solver_all() { let problem = Partition::new(vec![3, 1, 1, 2, 2, 1]); let solver = BruteForce::new(); - let solutions = solver.find_all_satisfying(&problem); + let solutions = solver.find_all_witnesses(&problem); // 10 satisfying configs for {3,1,1,2,2,1} with target half-sum 5 assert_eq!(solutions.len(), 10); for sol in &solutions { @@ -74,14 +74,14 @@ fn test_partition_single_element() { // Single element can never be partitioned equally let problem = Partition::new(vec![5]); let solver = BruteForce::new(); - assert!(solver.find_satisfying(&problem).is_none()); + assert!(solver.find_witness(&problem).is_none()); } #[test] fn test_partition_two_equal_elements() { let problem = Partition::new(vec![4, 4]); let solver = BruteForce::new(); - let solution = solver.find_satisfying(&problem); + let solution = solver.find_witness(&problem); assert!(solution.is_some()); assert!(problem.evaluate(&solution.unwrap())); } diff --git a/src/unit_tests/models/misc/precedence_constrained_scheduling.rs b/src/unit_tests/models/misc/precedence_constrained_scheduling.rs index 8a3ace315..8c30bc4b3 100644 --- a/src/unit_tests/models/misc/precedence_constrained_scheduling.rs +++ b/src/unit_tests/models/misc/precedence_constrained_scheduling.rs @@ -1,5 +1,5 @@ use super::*; -use crate::solvers::{BruteForce, Solver}; +use crate::solvers::BruteForce; use crate::traits::Problem; #[test] @@ -78,7 +78,7 @@ fn test_precedence_constrained_scheduling_brute_force() { let problem = PrecedenceConstrainedScheduling::new(3, 2, 2, vec![(0, 2)]); let solver = BruteForce::new(); let solution = solver - .find_satisfying(&problem) + .find_witness(&problem) .expect("should find a solution"); assert!(problem.evaluate(&solution)); } @@ -87,7 +87,7 @@ fn test_precedence_constrained_scheduling_brute_force() { fn test_precedence_constrained_scheduling_brute_force_all() { let problem = PrecedenceConstrainedScheduling::new(3, 2, 2, vec![(0, 2)]); let solver = BruteForce::new(); - let solutions = solver.find_all_satisfying(&problem); + let solutions = solver.find_all_witnesses(&problem); assert!(!solutions.is_empty()); for sol in &solutions { assert!(problem.evaluate(sol)); @@ -99,7 +99,7 @@ fn test_precedence_constrained_scheduling_unsatisfiable() { // 3 tasks in a chain t0 < t1 < t2, but only deadline 2 (need 3 slots) let problem = PrecedenceConstrainedScheduling::new(3, 1, 2, vec![(0, 1), (1, 2)]); let solver = BruteForce::new(); - assert!(solver.find_satisfying(&problem).is_none()); + assert!(solver.find_witness(&problem).is_none()); } #[test] @@ -129,7 +129,7 @@ fn test_precedence_constrained_scheduling_no_precedences() { assert!(problem.evaluate(&[0, 0, 1, 1])); let solver = BruteForce::new(); let solution = solver - .find_satisfying(&problem) + .find_witness(&problem) .expect("should find a solution"); assert!(problem.evaluate(&solution)); } diff --git a/src/unit_tests/models/misc/rectilinear_picture_compression.rs b/src/unit_tests/models/misc/rectilinear_picture_compression.rs index 095bfdee7..7b649496d 100644 --- a/src/unit_tests/models/misc/rectilinear_picture_compression.rs +++ b/src/unit_tests/models/misc/rectilinear_picture_compression.rs @@ -1,5 +1,5 @@ use super::*; -use crate::solvers::{BruteForce, Solver}; +use crate::solvers::BruteForce; use crate::traits::Problem; fn two_block_matrix() -> Vec> { @@ -96,7 +96,7 @@ fn test_rectilinear_picture_compression_evaluate_invalid_variable_value() { fn test_rectilinear_picture_compression_issue_matrix_satisfiable() { let problem = RectilinearPictureCompression::new(issue_matrix(), 3); let solver = BruteForce::new(); - let solution = solver.find_satisfying(&problem); + let solution = solver.find_witness(&problem); assert!(solution.is_some()); let sol = solution.unwrap(); assert!(problem.evaluate(&sol)); @@ -106,7 +106,7 @@ fn test_rectilinear_picture_compression_issue_matrix_satisfiable() { fn test_rectilinear_picture_compression_issue_matrix_unsatisfiable() { let problem = RectilinearPictureCompression::new(issue_matrix(), 2); let solver = BruteForce::new(); - let solution = solver.find_satisfying(&problem); + let solution = solver.find_witness(&problem); assert!(solution.is_none()); } @@ -115,7 +115,7 @@ fn test_rectilinear_picture_compression_brute_force() { let problem = RectilinearPictureCompression::new(two_block_matrix(), 2); let solver = BruteForce::new(); let solution = solver - .find_satisfying(&problem) + .find_witness(&problem) .expect("should find a solution"); assert!(problem.evaluate(&solution)); } @@ -124,7 +124,7 @@ fn test_rectilinear_picture_compression_brute_force() { fn test_rectilinear_picture_compression_brute_force_all() { let problem = RectilinearPictureCompression::new(two_block_matrix(), 2); let solver = BruteForce::new(); - let solutions = solver.find_all_satisfying(&problem); + let solutions = solver.find_all_witnesses(&problem); // Two disjoint 2x2 blocks with K=2: exactly one satisfying config [1,1]. assert_eq!(solutions.len(), 1); for sol in &solutions { @@ -200,7 +200,7 @@ fn test_rectilinear_picture_compression_overlapping_rectangles() { assert!(rects.contains(&(0, 0, 1, 0))); assert!(rects.contains(&(0, 0, 0, 1))); let solver = BruteForce::new(); - let solution = solver.find_satisfying(&problem).unwrap(); + let solution = solver.find_witness(&problem).unwrap(); assert!(problem.evaluate(&solution)); } diff --git a/src/unit_tests/models/misc/resource_constrained_scheduling.rs b/src/unit_tests/models/misc/resource_constrained_scheduling.rs index bd5e194e8..16f352a6b 100644 --- a/src/unit_tests/models/misc/resource_constrained_scheduling.rs +++ b/src/unit_tests/models/misc/resource_constrained_scheduling.rs @@ -1,5 +1,5 @@ use super::*; -use crate::solvers::{BruteForce, Solver}; +use crate::solvers::BruteForce; use crate::traits::Problem; #[test] @@ -111,7 +111,7 @@ fn test_resource_constrained_scheduling_brute_force_infeasible() { 2, ); let solver = BruteForce::new(); - let solution = solver.find_satisfying(&problem); + let solution = solver.find_witness(&problem); // 1 processor * 2 time slots = 2 tasks max, but we have 4 assert!(solution.is_none()); } @@ -170,7 +170,7 @@ fn test_resource_constrained_scheduling_single_task_exceeds_bound() { assert!(!problem.evaluate(&[0])); assert!(!problem.evaluate(&[1])); let solver = BruteForce::new(); - assert!(solver.find_satisfying(&problem).is_none()); + assert!(solver.find_witness(&problem).is_none()); } #[test] @@ -189,7 +189,7 @@ fn test_resource_constrained_scheduling_canonical_brute_force() { 2, ); let solver = BruteForce::new(); - let all = solver.find_all_satisfying(&problem); + let all = solver.find_all_witnesses(&problem); assert!(!all.is_empty()); // Verify the hardcoded canonical solution is among the brute-force results assert!(all.contains(&vec![0, 0, 0, 1, 1, 1])); 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 6c46ac905..df350b592 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,5 @@ use super::*; -use crate::solvers::{BruteForce, Solver}; +use crate::solvers::BruteForce; use crate::traits::Problem; fn issue_example_problem() -> SchedulingWithIndividualDeadlines { @@ -85,8 +85,8 @@ fn test_scheduling_with_individual_deadlines_brute_force_satisfiable() { let problem = SchedulingWithIndividualDeadlines::new(3, 2, vec![1, 1, 2], vec![(0, 2)]); let solver = BruteForce::new(); - assert_eq!(solver.find_all_satisfying(&problem), vec![vec![0, 0, 1]]); - assert_eq!(solver.find_satisfying(&problem), Some(vec![0, 0, 1])); + assert_eq!(solver.find_all_witnesses(&problem), vec![vec![0, 0, 1]]); + assert_eq!(solver.find_witness(&problem), Some(vec![0, 0, 1])); } #[test] @@ -94,7 +94,7 @@ fn test_scheduling_with_individual_deadlines_brute_force_unsatisfiable() { let problem = SchedulingWithIndividualDeadlines::new(3, 1, vec![1, 1, 1], vec![]); let solver = BruteForce::new(); - assert!(solver.find_satisfying(&problem).is_none()); + assert!(solver.find_witness(&problem).is_none()); } #[test] @@ -114,14 +114,11 @@ fn test_scheduling_with_individual_deadlines_paper_example() { let problem = issue_example_problem(); let solver = BruteForce::new(); - let satisfying = solver.find_all_satisfying(&problem); + let satisfying = solver.find_all_witnesses(&problem); assert!(problem.evaluate(&[0, 0, 0, 1, 2, 1, 1])); assert!(satisfying.contains(&vec![0, 0, 0, 1, 2, 1, 1])); - assert_eq!( - solver.find_satisfying(&problem), - satisfying.into_iter().next() - ); + assert_eq!(solver.find_witness(&problem), satisfying.into_iter().next()); } #[test] 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 fa6a4823f..166204ddf 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,5 @@ use super::*; -use crate::solvers::{BruteForce, Solver}; +use crate::solvers::BruteForce; use crate::traits::Problem; fn issue_example(bound: i64) -> SequencingToMinimizeMaximumCumulativeCost { @@ -76,7 +76,7 @@ fn test_sequencing_to_minimize_maximum_cumulative_cost_brute_force_solver() { let problem = issue_example(4); let solver = BruteForce::new(); let solution = solver - .find_satisfying(&problem) + .find_witness(&problem) .expect("should find a satisfying schedule"); assert!(problem.evaluate(&solution)); } @@ -89,7 +89,7 @@ fn test_sequencing_to_minimize_maximum_cumulative_cost_unsatisfiable_cycle() { 10, ); let solver = BruteForce::new(); - assert!(solver.find_satisfying(&problem).is_none()); + assert!(solver.find_witness(&problem).is_none()); } #[test] @@ -98,7 +98,7 @@ fn test_sequencing_to_minimize_maximum_cumulative_cost_paper_example() { let sample_config = vec![1, 0, 1, 0, 0, 0]; assert!(problem.evaluate(&sample_config)); - let satisfying = BruteForce::new().find_all_satisfying(&problem); + let satisfying = BruteForce::new().find_all_witnesses(&problem); assert_eq!(satisfying.len(), 5); assert!(satisfying.iter().any(|config| config == &sample_config)); } 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 08aeab2c3..d8e5b18d6 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,7 +1,7 @@ use super::*; -use crate::solvers::{BruteForce, Solver}; -use crate::traits::{OptimizationProblem, Problem}; -use crate::types::{Direction, SolutionSize}; +use crate::solvers::BruteForce; +use crate::traits::{ObjectiveProblem, Problem}; +use crate::types::{ExtremumSense, Min}; #[test] fn test_sequencing_to_minimize_weighted_completion_time_basic() { @@ -18,7 +18,7 @@ fn test_sequencing_to_minimize_weighted_completion_time_basic() { assert_eq!(problem.num_precedences(), 2); assert_eq!(problem.total_processing_time(), 9); assert_eq!(problem.dims(), vec![5, 4, 3, 2, 1]); - assert_eq!(problem.direction(), Direction::Minimize); + assert_eq!(problem.direction(), ExtremumSense::Minimize); assert_eq!( ::NAME, "SequencingToMinimizeWeightedCompletionTime" @@ -40,7 +40,7 @@ fn test_sequencing_to_minimize_weighted_completion_time_evaluate_issue_example() // Lehmer [1,2,0,1,0] decodes to schedule [1,3,0,4,2]. // Completion times are [4,1,9,2,6], so the objective is // 3*4 + 5*1 + 1*9 + 4*2 + 2*6 = 46. - assert_eq!(problem.evaluate(&[1, 2, 0, 1, 0]), SolutionSize::Valid(46)); + assert_eq!(problem.evaluate(&[1, 2, 0, 1, 0]), Min(Some(46))); } #[test] @@ -48,8 +48,8 @@ fn test_sequencing_to_minimize_weighted_completion_time_evaluate_invalid_lehmer( let problem = SequencingToMinimizeWeightedCompletionTime::new(vec![2, 1, 3], vec![3, 5, 1], vec![]); - assert_eq!(problem.evaluate(&[0, 2, 0]), SolutionSize::Invalid); - assert_eq!(problem.evaluate(&[0, 1, 5]), SolutionSize::Invalid); + assert_eq!(problem.evaluate(&[0, 2, 0]), Min(None)); + assert_eq!(problem.evaluate(&[0, 1, 5]), Min(None)); } #[test] @@ -57,8 +57,8 @@ fn test_sequencing_to_minimize_weighted_completion_time_evaluate_wrong_length() let problem = SequencingToMinimizeWeightedCompletionTime::new(vec![2, 1, 3], vec![3, 5, 1], vec![]); - assert_eq!(problem.evaluate(&[0, 1]), SolutionSize::Invalid); - assert_eq!(problem.evaluate(&[0, 1, 2, 3]), SolutionSize::Invalid); + assert_eq!(problem.evaluate(&[0, 1]), Min(None)); + assert_eq!(problem.evaluate(&[0, 1, 2, 3]), Min(None)); } #[test] @@ -66,8 +66,8 @@ fn test_sequencing_to_minimize_weighted_completion_time_evaluate_precedence_viol let problem = SequencingToMinimizeWeightedCompletionTime::new(vec![2, 1, 3], vec![3, 5, 1], vec![(0, 1)]); - assert_eq!(problem.evaluate(&[0, 0, 0]), SolutionSize::Valid(27)); - assert_eq!(problem.evaluate(&[1, 0, 0]), SolutionSize::Invalid); + assert_eq!(problem.evaluate(&[0, 0, 0]), Min(Some(27))); + assert_eq!(problem.evaluate(&[1, 0, 0]), Min(None)); } #[test] @@ -78,10 +78,12 @@ fn test_sequencing_to_minimize_weighted_completion_time_brute_force() { vec![(0, 2), (1, 4)], ); let solver = BruteForce::new(); - let solution = solver.find_best(&problem).expect("should find a solution"); + let solution = solver + .find_witness(&problem) + .expect("should find a solution"); assert_eq!(solution, vec![1, 2, 0, 1, 0]); - assert_eq!(problem.evaluate(&solution), SolutionSize::Valid(46)); + assert_eq!(problem.evaluate(&solution), Min(Some(46))); } #[test] @@ -116,7 +118,7 @@ fn test_sequencing_to_minimize_weighted_completion_time_empty() { assert_eq!(problem.num_tasks(), 0); assert_eq!(problem.dims(), Vec::::new()); - assert_eq!(problem.evaluate(&[]), SolutionSize::Valid(0)); + assert_eq!(problem.evaluate(&[]), Min(Some(0))); } #[test] @@ -124,7 +126,7 @@ fn test_sequencing_to_minimize_weighted_completion_time_single_task() { let problem = SequencingToMinimizeWeightedCompletionTime::new(vec![3], vec![2], vec![]); assert_eq!(problem.dims(), vec![1]); - assert_eq!(problem.evaluate(&[0]), SolutionSize::Valid(6)); + assert_eq!(problem.evaluate(&[0]), Min(Some(6))); } #[test] @@ -154,7 +156,7 @@ fn test_sequencing_to_minimize_weighted_completion_time_cyclic_precedences() { ); let solver = BruteForce::new(); - assert!(solver.find_best(&problem).is_none()); + assert!(solver.find_witness(&problem).is_none()); } #[test] @@ -166,10 +168,10 @@ fn test_sequencing_to_minimize_weighted_completion_time_paper_example() { ); let expected = vec![1, 2, 0, 1, 0]; - assert_eq!(problem.evaluate(&expected), SolutionSize::Valid(46)); + assert_eq!(problem.evaluate(&expected), Min(Some(46))); let solver = BruteForce::new(); - let solutions = solver.find_all_best(&problem); + let solutions = solver.find_all_witnesses(&problem); assert_eq!(solutions, vec![expected]); } 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 ca12d961b..b0d45b8f4 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,5 @@ use super::*; -use crate::solvers::{BruteForce, Solver}; +use crate::solvers::BruteForce; use crate::traits::Problem; fn issue_example_yes() -> SequencingToMinimizeWeightedTardiness { @@ -77,7 +77,7 @@ fn test_sequencing_to_minimize_weighted_tardiness_solver_yes() { let problem = issue_example_yes(); let solver = BruteForce::new(); let solution = solver - .find_satisfying(&problem) + .find_witness(&problem) .expect("should find a schedule"); assert!(problem.evaluate(&solution)); assert!(problem.total_weighted_tardiness(&solution).unwrap() <= problem.bound()); @@ -87,8 +87,8 @@ fn test_sequencing_to_minimize_weighted_tardiness_solver_yes() { fn test_sequencing_to_minimize_weighted_tardiness_solver_no() { let problem = issue_example_no(); let solver = BruteForce::new(); - assert!(solver.find_satisfying(&problem).is_none()); - assert!(solver.find_all_satisfying(&problem).is_empty()); + assert!(solver.find_witness(&problem).is_none()); + assert!(solver.find_all_witnesses(&problem).is_empty()); } #[test] @@ -102,9 +102,9 @@ fn test_sequencing_to_minimize_weighted_tardiness_paper_example() { assert!(yes.evaluate(&config)); assert!(!no.evaluate(&config)); - let satisfying = solver.find_all_satisfying(&yes); + let satisfying = solver.find_all_witnesses(&yes); assert_eq!(satisfying, vec![config]); - assert!(solver.find_all_satisfying(&no).is_empty()); + assert!(solver.find_all_witnesses(&no).is_empty()); } #[test] 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 96724ba31..48c34ecfc 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,5 +1,5 @@ use super::*; -use crate::solvers::{BruteForce, Solver}; +use crate::solvers::BruteForce; use crate::traits::Problem; #[test] @@ -35,7 +35,7 @@ fn test_sequencing_rtd_evaluate_feasible() { vec![5, 6, 10, 3, 12], ); let solver = BruteForce::new(); - let solutions = solver.find_all_satisfying(&problem); + let solutions = solver.find_all_witnesses(&problem); // Exactly one feasible schedule exists: Lehmer code [3, 0, 0, 0, 0] assert_eq!(solutions.len(), 1); assert_eq!(solutions[0], vec![3, 0, 0, 0, 0]); @@ -85,7 +85,7 @@ fn test_sequencing_rtd_brute_force() { SequencingWithReleaseTimesAndDeadlines::new(vec![1, 2, 1], vec![0, 0, 2], vec![3, 3, 4]); let solver = BruteForce::new(); let solution = solver - .find_satisfying(&problem) + .find_witness(&problem) .expect("should find a solution"); assert!(problem.evaluate(&solution)); } @@ -94,7 +94,7 @@ fn test_sequencing_rtd_brute_force() { fn test_sequencing_rtd_brute_force_all() { let problem = SequencingWithReleaseTimesAndDeadlines::new(vec![1, 1], vec![0, 0], vec![3, 3]); let solver = BruteForce::new(); - let solutions = solver.find_all_satisfying(&problem); + let solutions = solver.find_all_witnesses(&problem); assert!(!solutions.is_empty()); for sol in &solutions { assert!(problem.evaluate(sol)); @@ -106,7 +106,7 @@ fn test_sequencing_rtd_unsatisfiable() { // Two tasks each need 2 time units but only 3 total time available let problem = SequencingWithReleaseTimesAndDeadlines::new(vec![2, 2], vec![0, 0], vec![3, 3]); let solver = BruteForce::new(); - let solution = solver.find_satisfying(&problem); + let solution = solver.find_witness(&problem); assert!(solution.is_none()); } diff --git a/src/unit_tests/models/misc/sequencing_within_intervals.rs b/src/unit_tests/models/misc/sequencing_within_intervals.rs index cd4e3a534..a2a67bade 100644 --- a/src/unit_tests/models/misc/sequencing_within_intervals.rs +++ b/src/unit_tests/models/misc/sequencing_within_intervals.rs @@ -1,5 +1,5 @@ use super::*; -use crate::solvers::{BruteForce, Solver}; +use crate::solvers::BruteForce; use crate::traits::Problem; #[test] @@ -71,7 +71,7 @@ fn test_sequencing_within_intervals_solver() { // Simple instance: 3 tasks that can be scheduled sequentially let problem = SequencingWithinIntervals::new(vec![0, 2, 4], vec![3, 5, 7], vec![2, 2, 2]); let solver = BruteForce::new(); - let solution = solver.find_satisfying(&problem); + let solution = solver.find_witness(&problem); assert!(solution.is_some()); let config = solution.unwrap(); assert!(problem.evaluate(&config)); @@ -86,7 +86,7 @@ fn test_sequencing_within_intervals_solver_canonical() { vec![2, 2, 2, 3, 2], ); let solver = BruteForce::new(); - let solution = solver.find_satisfying(&problem); + let solution = solver.find_witness(&problem); assert!(solution.is_some()); let config = solution.unwrap(); assert!(problem.evaluate(&config)); @@ -101,7 +101,7 @@ fn test_sequencing_within_intervals_no_solution() { // Task 1: start=0, runs [0,2) -> overlap assert!(!problem.evaluate(&[0, 0])); let solver = BruteForce::new(); - let solution = solver.find_satisfying(&problem); + let solution = solver.find_witness(&problem); assert!(solution.is_none()); } @@ -149,7 +149,7 @@ fn test_sequencing_within_intervals_single_task() { } #[test] -fn test_sequencing_within_intervals_find_all_satisfying() { +fn test_sequencing_within_intervals_find_all_witnesses() { // Issue #219 canonical instance: 5 tasks with overlapping windows // dims = [4, 6, 5, 4, 11], search space = 5280 let problem = SequencingWithinIntervals::new( @@ -158,7 +158,7 @@ fn test_sequencing_within_intervals_find_all_satisfying() { vec![2, 2, 2, 3, 2], ); let solver = BruteForce::new(); - let solutions = solver.find_all_satisfying(&problem); + let solutions = solver.find_all_witnesses(&problem); for sol in &solutions { assert!(problem.evaluate(sol)); } @@ -172,7 +172,7 @@ fn test_sequencing_within_intervals_find_all_satisfying_empty() { // Two tasks that must both use time [0,2), impossible without overlap let problem = SequencingWithinIntervals::new(vec![0, 0], vec![2, 2], vec![2, 2]); let solver = BruteForce::new(); - assert!(solver.find_all_satisfying(&problem).is_empty()); + assert!(solver.find_all_witnesses(&problem).is_empty()); } #[test] diff --git a/src/unit_tests/models/misc/shortest_common_supersequence.rs b/src/unit_tests/models/misc/shortest_common_supersequence.rs index 5634dcca5..fd2b2cdb0 100644 --- a/src/unit_tests/models/misc/shortest_common_supersequence.rs +++ b/src/unit_tests/models/misc/shortest_common_supersequence.rs @@ -1,5 +1,5 @@ use super::*; -use crate::solvers::{BruteForce, Solver}; +use crate::solvers::BruteForce; use crate::traits::Problem; #[test] @@ -71,7 +71,7 @@ fn test_shortestcommonsupersequence_brute_force() { let problem = ShortestCommonSupersequence::new(2, vec![vec![0, 1], vec![1, 0]], 3); let solver = BruteForce::new(); let solution = solver - .find_satisfying(&problem) + .find_witness(&problem) .expect("should find a solution"); assert!(problem.evaluate(&solution)); } @@ -91,7 +91,7 @@ fn test_shortestcommonsupersequence_unsatisfiable() { // "01" contains [0,1] but not [1,0]; "10" contains [1,0] but not [0,1] let problem = ShortestCommonSupersequence::new(2, vec![vec![0, 1], vec![1, 0]], 2); let solver = BruteForce::new(); - assert!(solver.find_satisfying(&problem).is_none()); + assert!(solver.find_witness(&problem).is_none()); } #[test] @@ -114,16 +114,16 @@ fn test_shortestcommonsupersequence_paper_example() { // Verify a solution exists with brute force let solver = BruteForce::new(); - assert!(solver.find_satisfying(&problem).is_some()); + assert!(solver.find_witness(&problem).is_some()); // Bound 3 is too short: LCS("abc","bac")="ac" (len 2), so SCS ≥ 3+3-2 = 4 let tight = ShortestCommonSupersequence::new(3, vec![vec![0, 1, 2], vec![1, 0, 2]], 3); let solver2 = BruteForce::new(); - assert!(solver2.find_satisfying(&tight).is_none()); + assert!(solver2.find_witness(&tight).is_none()); } #[test] -fn test_shortestcommonsupersequence_find_all_satisfying() { +fn test_shortestcommonsupersequence_find_all_witnesses() { // Issue #412 instance 1: Σ={a,b,c}, R={"abcb","bcab","acba"}, K=7 // Search space = 3^7 = 2187 let problem = ShortestCommonSupersequence::new( @@ -132,7 +132,7 @@ fn test_shortestcommonsupersequence_find_all_satisfying() { 7, ); let solver = BruteForce::new(); - let solutions = solver.find_all_satisfying(&problem); + let solutions = solver.find_all_witnesses(&problem); for sol in &solutions { assert!(problem.evaluate(sol)); } @@ -158,7 +158,7 @@ fn test_shortestcommonsupersequence_find_all_satisfying_empty() { 5, ); let solver = BruteForce::new(); - assert!(solver.find_all_satisfying(&problem).is_empty()); + assert!(solver.find_all_witnesses(&problem).is_empty()); } #[test] diff --git a/src/unit_tests/models/misc/stacker_crane.rs b/src/unit_tests/models/misc/stacker_crane.rs index 258d45fec..7e70f78b6 100644 --- a/src/unit_tests/models/misc/stacker_crane.rs +++ b/src/unit_tests/models/misc/stacker_crane.rs @@ -1,5 +1,5 @@ use super::*; -use crate::solvers::{BruteForce, Solver}; +use crate::solvers::BruteForce; use crate::traits::Problem; fn issue_problem(bound: i32) -> StackerCrane { @@ -58,7 +58,7 @@ fn test_stacker_crane_issue_instance_is_unsatisfiable_at_bound_19() { let problem = issue_problem(19); let solver = BruteForce::new(); - assert!(solver.find_all_satisfying(&problem).is_empty()); + assert!(solver.find_all_witnesses(&problem).is_empty()); } #[test] @@ -70,7 +70,7 @@ fn test_stacker_crane_paper_example() { assert!(problem.evaluate(&witness)); let solver = BruteForce::new(); - let satisfying = solver.find_all_satisfying(&problem); + let satisfying = solver.find_all_witnesses(&problem); assert!(!satisfying.is_empty()); assert!(satisfying.contains(&witness)); for config in &satisfying { @@ -84,7 +84,7 @@ fn test_stacker_crane_small_solver_instance() { let solver = BruteForce::new(); let satisfying = solver - .find_satisfying(&problem) + .find_witness(&problem) .expect("small instance should be satisfiable"); let mut sorted = satisfying.clone(); sorted.sort_unstable(); diff --git a/src/unit_tests/models/misc/staff_scheduling.rs b/src/unit_tests/models/misc/staff_scheduling.rs index 0ed041a3a..7e36b205f 100644 --- a/src/unit_tests/models/misc/staff_scheduling.rs +++ b/src/unit_tests/models/misc/staff_scheduling.rs @@ -1,5 +1,5 @@ use super::*; -use crate::solvers::{BruteForce, Solver}; +use crate::solvers::BruteForce; use crate::traits::Problem; fn issue_example_problem() -> StaffScheduling { @@ -74,7 +74,7 @@ fn test_staff_scheduling_rejects_invalid_configs() { #[test] fn test_staff_scheduling_bruteforce_solver_finds_solution() { let problem = issue_example_problem(); - let solution = BruteForce::new().find_satisfying(&problem); + let solution = BruteForce::new().find_witness(&problem); assert!(solution.is_some()); assert!(problem.evaluate(&solution.unwrap())); } @@ -83,7 +83,7 @@ fn test_staff_scheduling_bruteforce_solver_finds_solution() { fn test_staff_scheduling_bruteforce_solver_detects_unsat() { let problem = StaffScheduling::new(1, vec![vec![true, false], vec![false, true]], vec![2, 2], 1); - assert!(BruteForce::new().find_satisfying(&problem).is_none()); + assert!(BruteForce::new().find_witness(&problem).is_none()); } #[test] @@ -106,7 +106,7 @@ fn test_staff_scheduling_paper_example() { let config = vec![1, 1, 1, 1, 0]; assert!(problem.evaluate(&config)); - let satisfying = BruteForce::new().find_all_satisfying(&problem); + let satisfying = BruteForce::new().find_all_witnesses(&problem); assert!(satisfying.contains(&config)); } 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 9b4c25063..f4023a730 100644 --- a/src/unit_tests/models/misc/string_to_string_correction.rs +++ b/src/unit_tests/models/misc/string_to_string_correction.rs @@ -1,5 +1,5 @@ use super::*; -use crate::solvers::{BruteForce, Solver}; +use crate::solvers::BruteForce; use crate::traits::Problem; #[test] @@ -70,7 +70,7 @@ fn test_string_to_string_correction_solver() { let problem = StringToStringCorrection::new(2, vec![0, 1], vec![1, 0], 1); let solver = BruteForce::new(); let solution = solver - .find_satisfying(&problem) + .find_witness(&problem) .expect("should find a solution"); assert!(problem.evaluate(&solution)); } @@ -84,7 +84,7 @@ fn test_string_to_string_correction_paper_example() { // Verify all solutions with brute force let solver = BruteForce::new(); - let all_solutions = solver.find_all_satisfying(&problem); + let all_solutions = solver.find_all_witnesses(&problem); assert!(!all_solutions.is_empty()); // The known solution must be among them assert!(all_solutions.contains(&vec![8, 5])); @@ -101,7 +101,7 @@ fn test_string_to_string_correction_unsatisfiable() { assert!(!problem.evaluate(&[])); let solver = BruteForce::new(); - assert!(solver.find_satisfying(&problem).is_none()); + assert!(solver.find_witness(&problem).is_none()); } #[test] @@ -127,7 +127,7 @@ fn test_string_to_string_correction_delete_only() { let solver = BruteForce::new(); let solution = solver - .find_satisfying(&problem) + .find_witness(&problem) .expect("should find a solution"); assert!(problem.evaluate(&solution)); } diff --git a/src/unit_tests/models/misc/subset_sum.rs b/src/unit_tests/models/misc/subset_sum.rs index 67fb79764..906a122e9 100644 --- a/src/unit_tests/models/misc/subset_sum.rs +++ b/src/unit_tests/models/misc/subset_sum.rs @@ -1,5 +1,5 @@ use super::*; -use crate::solvers::{BruteForce, Solver}; +use crate::solvers::BruteForce; use crate::traits::Problem; use num_bigint::BigUint; @@ -76,7 +76,7 @@ fn test_subsetsum_brute_force() { let problem = SubsetSum::new(vec![3u32, 7, 1, 8, 2, 4], 11u32); let solver = BruteForce::new(); let solution = solver - .find_satisfying(&problem) + .find_witness(&problem) .expect("should find a solution"); assert!(problem.evaluate(&solution)); } @@ -85,7 +85,7 @@ fn test_subsetsum_brute_force() { fn test_subsetsum_brute_force_all() { let problem = SubsetSum::new(vec![3u32, 7, 1, 8, 2, 4], 11u32); let solver = BruteForce::new(); - let solutions = solver.find_all_satisfying(&problem); + let solutions = solver.find_all_witnesses(&problem); assert!(!solutions.is_empty()); for sol in &solutions { assert!(problem.evaluate(sol)); @@ -97,7 +97,7 @@ fn test_subsetsum_unsatisfiable() { // Target 100 is unreachable let problem = SubsetSum::new(vec![1u32, 2, 3], 100u32); let solver = BruteForce::new(); - let solution = solver.find_satisfying(&problem); + let solution = solver.find_witness(&problem); assert!(solution.is_none()); } 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 f0defb70d..46230f812 100644 --- a/src/unit_tests/models/misc/sum_of_squares_partition.rs +++ b/src/unit_tests/models/misc/sum_of_squares_partition.rs @@ -1,5 +1,5 @@ use super::*; -use crate::solvers::{BruteForce, Solver}; +use crate::solvers::BruteForce; use crate::traits::Problem; #[test] @@ -80,7 +80,7 @@ fn test_sum_of_squares_partition_brute_force() { let problem = SumOfSquaresPartition::new(vec![5, 3, 8, 2, 7, 1], 3, 240); let solver = BruteForce::new(); let solution = solver - .find_satisfying(&problem) + .find_witness(&problem) .expect("should find a satisfying solution"); assert!(problem.evaluate(&solution)); } @@ -89,7 +89,7 @@ fn test_sum_of_squares_partition_brute_force() { fn test_sum_of_squares_partition_brute_force_all() { let problem = SumOfSquaresPartition::new(vec![5, 3, 8, 2, 7, 1], 3, 240); let solver = BruteForce::new(); - let solutions = solver.find_all_satisfying(&problem); + let solutions = solver.find_all_witnesses(&problem); assert!(!solutions.is_empty()); for sol in &solutions { assert!(problem.evaluate(sol)); @@ -103,7 +103,7 @@ fn test_sum_of_squares_partition_unsatisfiable() { // Best: each element in its own group -> 100+100+100=300 > 299 let problem = SumOfSquaresPartition::new(vec![10, 10, 10], 3, 299); let solver = BruteForce::new(); - let solution = solver.find_satisfying(&problem); + let solution = solver.find_witness(&problem); assert!(solution.is_none()); } @@ -190,7 +190,7 @@ fn test_sum_of_squares_partition_paper_example() { // Brute force finds satisfying solutions let solver = BruteForce::new(); - let all = solver.find_all_satisfying(&problem); + let all = solver.find_all_witnesses(&problem); assert!(!all.is_empty()); // All solutions must have sum-of-squares <= 240 for sol in &all { diff --git a/src/unit_tests/models/misc/timetable_design.rs b/src/unit_tests/models/misc/timetable_design.rs index 01f907933..45184be81 100644 --- a/src/unit_tests/models/misc/timetable_design.rs +++ b/src/unit_tests/models/misc/timetable_design.rs @@ -1,5 +1,5 @@ use crate::models::misc::TimetableDesign; -use crate::solvers::{BruteForce, Solver}; +use crate::solvers::BruteForce; use crate::traits::Problem; #[cfg(feature = "ilp-solver")] use std::collections::BTreeMap; @@ -124,7 +124,7 @@ fn test_timetable_design_rejects_requirement_mismatch() { #[test] fn test_timetable_design_bruteforce_solver_finds_solution() { let problem = timetable_design_toy_problem(); - let solution = BruteForce::new().find_satisfying(&problem); + let solution = BruteForce::new().find_witness(&problem); assert!(solution.is_some()); assert!(problem.evaluate(&solution.unwrap())); diff --git a/src/unit_tests/models/set/comparative_containment.rs b/src/unit_tests/models/set/comparative_containment.rs index e7e05d001..c677fd45e 100644 --- a/src/unit_tests/models/set/comparative_containment.rs +++ b/src/unit_tests/models/set/comparative_containment.rs @@ -75,11 +75,11 @@ fn test_comparative_containment_contains_selected_subset_requires_valid_config() fn test_comparative_containment_solver() { let solver = BruteForce::new(); - let yes_solutions = solver.find_all_satisfying(&yes_instance()); + let yes_solutions = solver.find_all_witnesses(&yes_instance()); assert!(yes_solutions.contains(&vec![1, 0, 0, 0])); assert!(!yes_solutions.is_empty()); - let no_solutions = solver.find_all_satisfying(&no_instance()); + let no_solutions = solver.find_all_witnesses(&no_instance()); assert!(no_solutions.is_empty()); } @@ -102,7 +102,7 @@ fn test_comparative_containment_paper_example() { assert!(problem.evaluate(&config)); let solver = BruteForce::new(); - let solutions = solver.find_all_satisfying(&problem); + let solutions = solver.find_all_witnesses(&problem); assert_eq!(solutions.len(), 3); assert!(solutions.contains(&config)); } diff --git a/src/unit_tests/models/set/consecutive_sets.rs b/src/unit_tests/models/set/consecutive_sets.rs index c7b86d0a4..d0f249900 100644 --- a/src/unit_tests/models/set/consecutive_sets.rs +++ b/src/unit_tests/models/set/consecutive_sets.rs @@ -39,7 +39,7 @@ fn test_consecutive_sets_no_instance() { // Search space: 4^3 = 64 configs, very fast. let problem = ConsecutiveSets::new(3, vec![vec![0, 1], vec![1, 2], vec![0, 2]], 3); let solver = BruteForce::new(); - let solutions = solver.find_all_satisfying(&problem); + let solutions = solver.find_all_witnesses(&problem); assert!(solutions.is_empty()); } @@ -49,7 +49,7 @@ fn test_consecutive_sets_solver() { // Valid string: [0, 1, 2] — {0,1} at positions 0-1, {1,2} at positions 1-2 let problem = ConsecutiveSets::new(3, vec![vec![0, 1], vec![1, 2]], 3); let solver = BruteForce::new(); - let solutions = solver.find_all_satisfying(&problem); + let solutions = solver.find_all_witnesses(&problem); assert!(!solutions.is_empty()); for sol in &solutions { assert!(problem.evaluate(sol)); @@ -103,7 +103,7 @@ fn test_consecutive_sets_empty_subsets() { // All unused = empty string is fine assert!(problem.evaluate(&[3, 3, 3])); let solver = BruteForce::new(); - let solutions = solver.find_all_satisfying(&problem); + let solutions = solver.find_all_witnesses(&problem); assert!(!solutions.is_empty()); } 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 0911ea866..f5406303b 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 @@ -64,7 +64,7 @@ fn test_exact_cover_by_3_sets_solver() { ); let solver = BruteForce::new(); - let solutions = solver.find_all_satisfying(&problem); + let solutions = solver.find_all_witnesses(&problem); // S0={0,1,2}, S2={3,4,5}, S4={6,7,8} is an exact cover assert!(!solutions.is_empty()); @@ -83,7 +83,7 @@ fn test_exact_cover_by_3_sets_no_solution() { let problem = ExactCoverBy3Sets::new(6, vec![[0, 1, 2], [0, 3, 4], [0, 4, 5]]); let solver = BruteForce::new(); - let solutions = solver.find_all_satisfying(&problem); + let solutions = solver.find_all_witnesses(&problem); assert!(solutions.is_empty()); } @@ -128,7 +128,7 @@ fn test_exact_cover_by_3_sets_empty() { let problem = ExactCoverBy3Sets::new(0, vec![]); assert!(problem.evaluate(&[])); let solver = BruteForce::new(); - let solutions = solver.find_all_satisfying(&problem); + let solutions = solver.find_all_witnesses(&problem); assert_eq!(solutions, vec![Vec::::new()]); } diff --git a/src/unit_tests/models/set/maximum_set_packing.rs b/src/unit_tests/models/set/maximum_set_packing.rs index 9995c34fc..42ccc0ce3 100644 --- a/src/unit_tests/models/set/maximum_set_packing.rs +++ b/src/unit_tests/models/set/maximum_set_packing.rs @@ -1,7 +1,7 @@ use super::*; -use crate::solvers::{BruteForce, Solver}; -use crate::traits::{OptimizationProblem, Problem}; -use crate::types::{Direction, SolutionSize}; +use crate::solvers::BruteForce; +use crate::traits::{ObjectiveProblem, Problem}; +use crate::types::{ExtremumSense, Max}; include!("../../jl_helpers.rs"); #[test] @@ -49,14 +49,14 @@ fn test_is_set_packing_function() { #[test] fn test_direction() { let problem = MaximumSetPacking::::new(vec![vec![0, 1]]); - assert_eq!(problem.direction(), Direction::Maximize); + assert_eq!(problem.direction(), ExtremumSense::Maximize); } #[test] fn test_empty_sets() { let problem = MaximumSetPacking::::new(vec![]); // Empty packing is valid with size 0 - assert_eq!(Problem::evaluate(&problem, &[]), SolutionSize::Valid(0)); + assert_eq!(Problem::evaluate(&problem, &[]), Max(Some(0))); } #[test] @@ -83,8 +83,8 @@ fn test_relationship_to_independent_set() { let solver = BruteForce::new(); - let sp_solutions = solver.find_all_best(&sp_problem); - let is_solutions = solver.find_all_best(&is_problem); + let sp_solutions = solver.find_all_witnesses(&sp_problem); + let is_solutions = solver.find_all_witnesses(&is_problem); // Should have same optimal value let sp_size: usize = sp_solutions[0].iter().sum(); @@ -130,7 +130,7 @@ fn test_jl_parity_evaluation() { ); } } - let best = BruteForce::new().find_all_best(&problem); + let best = BruteForce::new().find_all_witnesses(&problem); let jl_best = jl_parse_configs_set(&instance["best_solutions"]); let rust_best: HashSet> = best.into_iter().collect(); assert_eq!(rust_best, jl_best, "SetPacking best solutions mismatch"); @@ -172,6 +172,6 @@ fn test_setpacking_paper_example() { assert_eq!(result.unwrap(), 2); let solver = BruteForce::new(); - let best = solver.find_best(&problem).unwrap(); + let best = solver.find_witness(&problem).unwrap(); assert_eq!(problem.evaluate(&best).unwrap(), 2); } diff --git a/src/unit_tests/models/set/minimum_cardinality_key.rs b/src/unit_tests/models/set/minimum_cardinality_key.rs index f97a02c00..aa0e742f1 100644 --- a/src/unit_tests/models/set/minimum_cardinality_key.rs +++ b/src/unit_tests/models/set/minimum_cardinality_key.rs @@ -69,7 +69,7 @@ fn test_minimum_cardinality_key_exceeds_bound() { fn test_minimum_cardinality_key_solver() { let problem = instance1(2); let solver = BruteForce::new(); - let solutions = solver.find_all_satisfying(&problem); + let solutions = solver.find_all_witnesses(&problem); let solution_set: HashSet> = solutions.iter().cloned().collect(); assert!(!solutions.is_empty()); @@ -118,7 +118,7 @@ fn test_minimum_cardinality_key_empty_key_candidate() { assert!(!problem.evaluate(&[1])); let solver = BruteForce::new(); - assert_eq!(solver.find_all_satisfying(&problem), vec![vec![0]]); + assert_eq!(solver.find_all_witnesses(&problem), vec![vec![0]]); } #[test] @@ -134,7 +134,7 @@ fn test_minimum_cardinality_key_paper_example() { assert!(problem.evaluate(&solution)); let solver = BruteForce::new(); - let solutions = solver.find_all_satisfying(&problem); + let solutions = solver.find_all_witnesses(&problem); let solution_set: HashSet> = solutions.iter().cloned().collect(); assert!(solution_set.contains(&solution)); // All returned solutions must be valid. diff --git a/src/unit_tests/models/set/minimum_hitting_set.rs b/src/unit_tests/models/set/minimum_hitting_set.rs index effc8659b..5a05fbe56 100644 --- a/src/unit_tests/models/set/minimum_hitting_set.rs +++ b/src/unit_tests/models/set/minimum_hitting_set.rs @@ -1,8 +1,8 @@ use super::*; use crate::registry::declared_size_fields; -use crate::solvers::{BruteForce, Solver}; -use crate::traits::{OptimizationProblem, Problem}; -use crate::types::{Direction, SolutionSize}; +use crate::solvers::BruteForce; +use crate::traits::{ObjectiveProblem, Problem}; +use crate::types::{ExtremumSense, Min}; use std::collections::HashSet; fn issue_example_problem() -> MinimumHittingSet { @@ -44,9 +44,9 @@ fn test_minimum_hitting_set_evaluate_valid_and_invalid() { assert_eq!(problem.selected_elements(&[0, 1, 0, 1]), Some(vec![1, 3])); assert_eq!(problem.selected_elements(&[0, 2, 0, 1]), None); - assert_eq!(problem.evaluate(&[0, 1, 0, 1]), SolutionSize::Valid(2)); - assert_eq!(problem.evaluate(&[1, 0, 0, 0]), SolutionSize::Invalid); - assert_eq!(problem.evaluate(&[0, 2, 0, 1]), SolutionSize::Invalid); + assert_eq!(problem.evaluate(&[0, 1, 0, 1]), Min(Some(2))); + assert_eq!(problem.evaluate(&[1, 0, 0, 0]), Min(None)); + assert_eq!(problem.evaluate(&[0, 2, 0, 1]), Min(None)); assert!(problem.is_valid_solution(&[0, 1, 0, 1])); assert!(!problem.is_valid_solution(&[1, 0, 0, 0])); assert!(!problem.is_valid_solution(&[0, 2, 0, 1])); @@ -56,8 +56,8 @@ fn test_minimum_hitting_set_evaluate_valid_and_invalid() { fn test_minimum_hitting_set_empty_set_is_always_invalid() { let problem = MinimumHittingSet::new(3, vec![vec![0, 1], vec![]]); - assert_eq!(problem.evaluate(&[1, 1, 1]), SolutionSize::Invalid); - assert_eq!(problem.evaluate(&[0, 0, 0]), SolutionSize::Invalid); + assert_eq!(problem.evaluate(&[1, 1, 1]), Min(None)); + assert_eq!(problem.evaluate(&[0, 0, 0]), Min(None)); } #[test] @@ -78,15 +78,15 @@ fn test_minimum_hitting_set_bruteforce_optimum_issue_example() { let problem = issue_example_problem(); let solver = BruteForce::new(); - let best = solver.find_best(&problem).unwrap(); - assert_eq!(problem.evaluate(&best), SolutionSize::Valid(3)); + let best = solver.find_witness(&problem).unwrap(); + assert_eq!(problem.evaluate(&best), Min(Some(3))); - let best_solutions = solver.find_all_best(&problem); + let best_solutions = solver.find_all_witnesses(&problem); let best_solution_set: HashSet> = best_solutions.iter().cloned().collect(); assert!(best_solution_set.contains(&issue_example_config())); assert!(best_solutions .iter() - .all(|config| problem.evaluate(config) == SolutionSize::Valid(3))); + .all(|config| problem.evaluate(config) == Min(Some(3)))); } #[test] @@ -108,16 +108,13 @@ fn test_minimum_hitting_set_serialization_round_trip() { fn test_minimum_hitting_set_paper_example_consistency() { let problem = issue_example_problem(); - assert_eq!( - problem.evaluate(&issue_example_config()), - SolutionSize::Valid(3) - ); + assert_eq!(problem.evaluate(&issue_example_config()), Min(Some(3))); } #[test] fn test_minimum_hitting_set_direction() { let problem = MinimumHittingSet::new(3, vec![vec![0, 1], vec![1, 2]]); - assert_eq!(problem.direction(), Direction::Minimize); + assert_eq!(problem.direction(), ExtremumSense::Minimize); } #[test] @@ -137,7 +134,7 @@ fn test_minimum_hitting_set_canonical_example_spec() { assert_eq!(spec.id, "minimum_hitting_set"); assert_eq!(spec.optimal_config, issue_example_config()); - assert_eq!(spec.optimal_value, serde_json::json!({"Valid": 3})); + assert_eq!(spec.optimal_value, serde_json::json!(3)); let problem: MinimumHittingSet = serde_json::from_value(spec.instance.serialize_json()).unwrap(); @@ -145,6 +142,6 @@ fn test_minimum_hitting_set_canonical_example_spec() { assert_eq!(problem.sets().len(), 7); let solver = BruteForce::new(); - let best = solver.find_best(&problem).unwrap(); - assert_eq!(problem.evaluate(&best), SolutionSize::Valid(3)); + let best = solver.find_witness(&problem).unwrap(); + assert_eq!(problem.evaluate(&best), Min(Some(3))); } diff --git a/src/unit_tests/models/set/minimum_set_covering.rs b/src/unit_tests/models/set/minimum_set_covering.rs index 0063bc698..c950a9cb8 100644 --- a/src/unit_tests/models/set/minimum_set_covering.rs +++ b/src/unit_tests/models/set/minimum_set_covering.rs @@ -1,7 +1,7 @@ use super::*; -use crate::solvers::{BruteForce, Solver}; -use crate::traits::{OptimizationProblem, Problem}; -use crate::types::{Direction, SolutionSize}; +use crate::solvers::BruteForce; +use crate::traits::{ObjectiveProblem, Problem}; +use crate::types::{ExtremumSense, Min}; include!("../../jl_helpers.rs"); #[test] @@ -55,14 +55,14 @@ fn test_get_set() { #[test] fn test_direction() { let problem = MinimumSetCovering::::new(2, vec![vec![0, 1]]); - assert_eq!(problem.direction(), Direction::Minimize); + assert_eq!(problem.direction(), ExtremumSense::Minimize); } #[test] fn test_empty_universe() { let problem = MinimumSetCovering::::new(0, vec![]); // Empty universe is trivially covered with size 0 - assert_eq!(Problem::evaluate(&problem, &[]), SolutionSize::Valid(0)); + assert_eq!(Problem::evaluate(&problem, &[]), Min(Some(0))); } #[test] @@ -100,7 +100,7 @@ fn test_jl_parity_evaluation() { ); } } - let best = BruteForce::new().find_all_best(&problem); + let best = BruteForce::new().find_all_witnesses(&problem); let jl_best = jl_parse_configs_set(&instance["best_solutions"]); let rust_best: HashSet> = best.into_iter().collect(); assert_eq!(rust_best, jl_best, "SetCovering best solutions mismatch"); @@ -127,6 +127,6 @@ fn test_setcovering_paper_example() { assert_eq!(result.unwrap(), 2); let solver = BruteForce::new(); - let best = solver.find_best(&problem).unwrap(); + let best = solver.find_witness(&problem).unwrap(); assert_eq!(problem.evaluate(&best).unwrap(), 2); } diff --git a/src/unit_tests/models/set/prime_attribute_name.rs b/src/unit_tests/models/set/prime_attribute_name.rs index 8359d4bd4..b8999597c 100644 --- a/src/unit_tests/models/set/prime_attribute_name.rs +++ b/src/unit_tests/models/set/prime_attribute_name.rs @@ -92,7 +92,7 @@ fn test_prime_attribute_name_evaluate_invalid_config() { fn test_prime_attribute_name_solver() { let problem = example1(); let solver = BruteForce::new(); - let mut solutions = solver.find_all_satisfying(&problem); + let mut solutions = solver.find_all_witnesses(&problem); solutions.sort(); assert!(!solutions.is_empty()); for sol in &solutions { @@ -108,7 +108,7 @@ fn test_prime_attribute_name_solver() { fn test_prime_attribute_name_no_solution() { let problem = example2(); let solver = BruteForce::new(); - let solutions = solver.find_all_satisfying(&problem); + let solutions = solver.find_all_witnesses(&problem); assert!(solutions.is_empty()); } 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 478c71e6d..def31e1ea 100644 --- a/src/unit_tests/models/set/rooted_tree_storage_assignment.rs +++ b/src/unit_tests/models/set/rooted_tree_storage_assignment.rs @@ -42,7 +42,7 @@ fn test_rooted_tree_storage_assignment_rejects_invalid_tree_configs() { #[test] fn test_rooted_tree_storage_assignment_solver_finds_known_solution() { let problem = yes_instance(1); - let solutions = BruteForce::new().find_all_satisfying(&problem); + let solutions = BruteForce::new().find_all_witnesses(&problem); assert!(!solutions.is_empty()); assert!(solutions.contains(&vec![0, 0, 0, 1, 2])); } @@ -50,7 +50,7 @@ fn test_rooted_tree_storage_assignment_solver_finds_known_solution() { #[test] fn test_rooted_tree_storage_assignment_no_instance() { let problem = yes_instance(0); - let solutions = BruteForce::new().find_all_satisfying(&problem); + let solutions = BruteForce::new().find_all_witnesses(&problem); assert!(solutions.is_empty()); } @@ -71,6 +71,6 @@ fn test_rooted_tree_storage_assignment_paper_example() { assert!(problem.evaluate(&config)); - let solutions = BruteForce::new().find_all_satisfying(&problem); + let solutions = BruteForce::new().find_all_witnesses(&problem); assert!(solutions.contains(&config)); } diff --git a/src/unit_tests/models/set/set_basis.rs b/src/unit_tests/models/set/set_basis.rs index 375251335..3a696b455 100644 --- a/src/unit_tests/models/set/set_basis.rs +++ b/src/unit_tests/models/set/set_basis.rs @@ -42,14 +42,14 @@ fn test_set_basis_no_solution_for_k_two() { assert!(!problem.evaluate(&[1, 1, 0, 0, 0, 0, 1, 0])); let solver = BruteForce::new(); - assert!(solver.find_all_satisfying(&problem).is_empty()); + assert!(solver.find_all_witnesses(&problem).is_empty()); } #[test] fn test_set_basis_solver() { let problem = issue_example_problem(3); let solver = BruteForce::new(); - let solutions = solver.find_all_satisfying(&problem); + let solutions = solver.find_all_witnesses(&problem); let solution_set: HashSet> = solutions.iter().cloned().collect(); assert_eq!(solutions.len(), 12); @@ -78,7 +78,7 @@ fn test_set_basis_paper_example() { assert!(problem.evaluate(&solution)); let solver = BruteForce::new(); - let solutions = solver.find_all_satisfying(&problem); + let solutions = solver.find_all_witnesses(&problem); assert_eq!(solutions.len(), 12); } 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 ffbb7208e..755088d76 100644 --- a/src/unit_tests/models/set/two_dimensional_consecutive_sets.rs +++ b/src/unit_tests/models/set/two_dimensional_consecutive_sets.rs @@ -72,7 +72,7 @@ fn test_two_dimensional_consecutive_sets_no_instance() { ); let solver = BruteForce::new(); - let solutions = solver.find_all_satisfying(&problem); + let solutions = solver.find_all_witnesses(&problem); assert!(solutions.is_empty()); } @@ -82,7 +82,7 @@ fn test_two_dimensional_consecutive_sets_solver() { let problem = TwoDimensionalConsecutiveSets::new(4, vec![vec![0, 1], vec![2, 3], vec![1, 2]]); let solver = BruteForce::new(); - let solutions = solver.find_all_satisfying(&problem); + let solutions = solver.find_all_witnesses(&problem); assert!(!solutions.is_empty()); for sol in &solutions { assert!(problem.evaluate(sol)); @@ -142,7 +142,7 @@ fn test_two_dimensional_consecutive_sets_paper_example() { // Use brute force to find all solutions let solver = BruteForce::new(); - let solutions = solver.find_all_satisfying(&problem); + let solutions = solver.find_all_witnesses(&problem); assert!(!solutions.is_empty()); // The known solution should be among them assert!(solutions.contains(&valid_config)); diff --git a/src/unit_tests/property.rs b/src/unit_tests/property.rs index 537106083..b9a300871 100644 --- a/src/unit_tests/property.rs +++ b/src/unit_tests/property.rs @@ -44,8 +44,8 @@ proptest! { let vc_problem = MinimumVertexCover::new(SimpleGraph::new(n, edges), vec![1i32; n]); let solver = BruteForce::new(); - let is_solutions = solver.find_all_best(&is_problem); - let vc_solutions = solver.find_all_best(&vc_problem); + let is_solutions = solver.find_all_witnesses(&is_problem); + let vc_solutions = solver.find_all_witnesses(&vc_problem); let is_size: usize = is_solutions[0].iter().sum(); let vc_size: usize = vc_solutions[0].iter().sum(); @@ -60,7 +60,7 @@ proptest! { let problem = MaximumIndependentSet::new(SimpleGraph::new(n, edges), vec![1i32; n]); let solver = BruteForce::new(); - for sol in solver.find_all_best(&problem) { + for sol in solver.find_all_witnesses(&problem) { // Any subset of an IS is also an IS for i in 0..n { let mut subset = sol.clone(); @@ -77,7 +77,7 @@ proptest! { let problem = MinimumVertexCover::new(SimpleGraph::new(n, edges), vec![1i32; n]); let solver = BruteForce::new(); - for sol in solver.find_all_best(&problem) { + for sol in solver.find_all_witnesses(&problem) { // Adding any vertex to a VC still gives a valid VC for i in 0..n { let mut superset = sol.clone(); @@ -96,7 +96,7 @@ proptest! { let solver = BruteForce::new(); // Get all valid independent sets (not just optimal) - for sol in solver.find_all_best(&is_problem) { + for sol in solver.find_all_witnesses(&is_problem) { // The complement should be a valid vertex cover let complement: Vec = sol.iter().map(|&x| 1 - x).collect(); prop_assert!(vc_problem.evaluate(&complement).is_valid(), @@ -129,12 +129,12 @@ proptest! { let problem = MaximumIndependentSet::new(SimpleGraph::new(n, edges), vec![1i32; n]); let solver = BruteForce::new(); - for sol in solver.find_all_best(&problem) { + for sol in solver.find_all_witnesses(&problem) { let metric = problem.evaluate(&sol); // Valid solutions have non-negative size prop_assert!(metric.is_valid()); - if let crate::types::SolutionSize::Valid(size) = metric { - prop_assert!(size >= 0); + if let Some(size) = metric.size() { + prop_assert!(*size >= 0); } } } diff --git a/src/unit_tests/reduction_graph.rs b/src/unit_tests/reduction_graph.rs index 4b10607c2..bea6dc7fd 100644 --- a/src/unit_tests/reduction_graph.rs +++ b/src/unit_tests/reduction_graph.rs @@ -2,7 +2,7 @@ use crate::models::formula::KSatisfiability; use crate::prelude::*; -use crate::rules::{MinimizeSteps, ReductionGraph, ReductionMode, TraversalDirection}; +use crate::rules::{MinimizeSteps, ReductionGraph, ReductionMode, TraversalFlow}; use crate::topology::{KingsSubgraph, SimpleGraph, TriangularSubgraph, UnitDiskGraph}; use crate::types::ProblemSize; use crate::variant::K3; @@ -447,7 +447,7 @@ fn test_k_neighbors_outgoing() { "MaximumIndependentSet", default_variant, 1, - TraversalDirection::Outgoing, + TraversalFlow::Outgoing, ); assert!(!neighbors.is_empty()); assert!(neighbors.iter().all(|n| n.hops == 1)); @@ -457,7 +457,7 @@ fn test_k_neighbors_outgoing() { "MaximumIndependentSet", default_variant, 2, - TraversalDirection::Outgoing, + TraversalFlow::Outgoing, ); assert!(neighbors_2.len() >= neighbors.len()); } @@ -468,7 +468,7 @@ fn test_k_neighbors_incoming() { let variants = graph.variants_for("QUBO"); assert!(!variants.is_empty()); - let neighbors = graph.k_neighbors("QUBO", &variants[0], 1, TraversalDirection::Incoming); + let neighbors = graph.k_neighbors("QUBO", &variants[0], 1, TraversalFlow::Incoming); // QUBO is a common target — should have incoming reductions assert!(!neighbors.is_empty()); } @@ -483,19 +483,19 @@ fn test_k_neighbors_both() { "MaximumIndependentSet", default_variant, 1, - TraversalDirection::Outgoing, + TraversalFlow::Outgoing, ); let in_only = graph.k_neighbors( "MaximumIndependentSet", default_variant, 1, - TraversalDirection::Incoming, + TraversalFlow::Incoming, ); let both = graph.k_neighbors( "MaximumIndependentSet", default_variant, 1, - TraversalDirection::Both, + TraversalFlow::Both, ); // Both should be >= max of either direction assert!(both.len() >= out_only.len()); @@ -506,7 +506,7 @@ fn test_k_neighbors_both() { fn test_k_neighbors_unknown_problem() { let graph = ReductionGraph::new(); let empty = BTreeMap::new(); - let neighbors = graph.k_neighbors("NonExistent", &empty, 2, TraversalDirection::Outgoing); + let neighbors = graph.k_neighbors("NonExistent", &empty, 2, TraversalFlow::Outgoing); assert!(neighbors.is_empty()); } @@ -519,7 +519,7 @@ fn test_k_neighbors_zero_hops() { "MaximumIndependentSet", default_variant, 0, - TraversalDirection::Outgoing, + TraversalFlow::Outgoing, ); assert!(neighbors.is_empty()); } diff --git a/src/unit_tests/registry/dispatch.rs b/src/unit_tests/registry/dispatch.rs index 931eb2b46..689124925 100644 --- a/src/unit_tests/registry/dispatch.rs +++ b/src/unit_tests/registry/dispatch.rs @@ -10,7 +10,7 @@ use std::collections::BTreeMap; fn solve_subset_sum_value(any: &dyn Any) -> String { let p = any.downcast_ref::().unwrap(); - if let Some(config) = crate::BruteForce::new().find_satisfying(p) { + if let Some(config) = crate::BruteForce::new().find_witness(p) { format!("{:?}", p.evaluate(&config)) } else { "false".to_string() @@ -19,7 +19,7 @@ fn solve_subset_sum_value(any: &dyn Any) -> String { fn solve_subset_sum_witness(any: &dyn Any) -> Option<(Vec, String)> { let p = any.downcast_ref::()?; - let config = crate::BruteForce::new().find_satisfying(p)?; + let config = crate::BruteForce::new().find_witness(p)?; let eval = format!("{:?}", p.evaluate(&config)); Some((config, eval)) } @@ -38,13 +38,11 @@ impl Problem for AggregateOnlyProblem { } fn evaluate(&self, config: &[usize]) -> Self::Value { - Sum( - config - .iter() - .zip(&self.weights) - .map(|(&c, &w)| if c == 1 { w } else { 0 }) - .sum(), - ) + Sum(config + .iter() + .zip(&self.weights) + .map(|(&c, &w)| if c == 1 { w } else { 0 }) + .sum()) } fn variant() -> Vec<(&'static str, &'static str)> { @@ -76,8 +74,11 @@ fn test_dyn_problem_blanket_impl_exposes_problem_metadata() { #[test] fn test_loaded_dyn_problem_delegates_to_value_and_witness_fns() { let problem = SubsetSum::new(vec![3u32, 7u32, 1u32], 4u32); - let loaded = - LoadedDynProblem::new(Box::new(problem), solve_subset_sum_value, solve_subset_sum_witness); + let loaded = LoadedDynProblem::new( + Box::new(problem), + solve_subset_sum_value, + solve_subset_sum_witness, + ); assert_eq!(loaded.solve_brute_force_value(), "true"); let solved = loaded diff --git a/src/unit_tests/rules/binpacking_ilp.rs b/src/unit_tests/rules/binpacking_ilp.rs index 87633735f..b7b63cf38 100644 --- a/src/unit_tests/rules/binpacking_ilp.rs +++ b/src/unit_tests/rules/binpacking_ilp.rs @@ -1,7 +1,7 @@ use super::*; use crate::solvers::{BruteForce, ILPSolver}; use crate::traits::Problem; -use crate::types::SolutionSize; +use crate::types::Min; #[test] fn test_reduction_creates_valid_ilp() { @@ -29,7 +29,7 @@ fn test_binpacking_to_ilp_closed_loop() { let ilp_solver = ILPSolver::new(); // Solve original with brute force - let bf_solutions = bf.find_all_best(&problem); + let bf_solutions = bf.find_all_witnesses(&problem); let bf_obj = problem.evaluate(&bf_solutions[0]); // Solve via ILP @@ -37,8 +37,8 @@ fn test_binpacking_to_ilp_closed_loop() { let extracted = reduction.extract_solution(&ilp_solution); let ilp_obj = problem.evaluate(&extracted); - assert_eq!(bf_obj, SolutionSize::Valid(2)); - assert_eq!(ilp_obj, SolutionSize::Valid(2)); + assert_eq!(bf_obj, Min(Some(2))); + assert_eq!(ilp_obj, Min(Some(2))); } #[test] @@ -55,7 +55,7 @@ fn test_single_item() { let extracted = reduction.extract_solution(&ilp_solution); assert!(problem.evaluate(&extracted).is_valid()); - assert_eq!(problem.evaluate(&extracted), SolutionSize::Valid(1)); + assert_eq!(problem.evaluate(&extracted), Min(Some(1))); } #[test] @@ -70,7 +70,7 @@ fn test_same_weight_items() { let extracted = reduction.extract_solution(&ilp_solution); assert!(problem.evaluate(&extracted).is_valid()); - assert_eq!(problem.evaluate(&extracted), SolutionSize::Valid(2)); + assert_eq!(problem.evaluate(&extracted), Min(Some(2))); } #[test] @@ -85,7 +85,7 @@ fn test_exact_fill() { let extracted = reduction.extract_solution(&ilp_solution); assert!(problem.evaluate(&extracted).is_valid()); - assert_eq!(problem.evaluate(&extracted), SolutionSize::Valid(1)); + assert_eq!(problem.evaluate(&extracted), Min(Some(1))); } #[test] @@ -139,5 +139,5 @@ fn test_solve_reduced() { .expect("solve_reduced should work"); assert!(problem.evaluate(&solution).is_valid()); - assert_eq!(problem.evaluate(&solution), SolutionSize::Valid(3)); + assert_eq!(problem.evaluate(&solution), Min(Some(3))); } diff --git a/src/unit_tests/rules/circuit_ilp.rs b/src/unit_tests/rules/circuit_ilp.rs index b40b5cc92..60ed7e176 100644 --- a/src/unit_tests/rules/circuit_ilp.rs +++ b/src/unit_tests/rules/circuit_ilp.rs @@ -49,7 +49,7 @@ fn test_circuitsat_to_ilp_xor_gate() { &reduction, "CircuitSAT->ILP XOR gate", ); - assert_eq!(BruteForce::new().find_all_satisfying(&source).len(), 4); + assert_eq!(BruteForce::new().find_all_witnesses(&source).len(), 4); } #[test] diff --git a/src/unit_tests/rules/circuit_spinglass.rs b/src/unit_tests/rules/circuit_spinglass.rs index d1b43c5a1..e648499a3 100644 --- a/src/unit_tests/rules/circuit_spinglass.rs +++ b/src/unit_tests/rules/circuit_spinglass.rs @@ -19,9 +19,10 @@ where + std::ops::Mul + std::fmt::Debug + NumericSize, + ::Sum: std::fmt::Debug + serde::Serialize + serde::de::DeserializeOwned, { let solver = BruteForce::new(); - let solutions = solver.find_all_best(&gadget.problem); + let solutions = solver.find_all_witnesses(&gadget.problem); // For each expected input/output pair, verify there's a matching ground state for (inputs, outputs) in expected { @@ -120,7 +121,7 @@ fn test_set0_gadget() { assert_eq!(gadget.outputs, vec![0]); let solver = BruteForce::new(); - let solutions = solver.find_all_best(&gadget.problem); + let solutions = solver.find_all_witnesses(&gadget.problem); // Ground state should be spin down (0) assert!(solutions.contains(&vec![0])); assert!(!solutions.contains(&vec![1])); @@ -134,7 +135,7 @@ fn test_set1_gadget() { assert_eq!(gadget.outputs, vec![0]); let solver = BruteForce::new(); - let solutions = solver.find_all_best(&gadget.problem); + let solutions = solver.find_all_witnesses(&gadget.problem); // Ground state should be spin up (1) assert!(solutions.contains(&vec![1])); assert!(!solutions.contains(&vec![0])); @@ -152,7 +153,7 @@ fn test_constant_true() { let sg = reduction.target_problem(); let solver = BruteForce::new(); - let solutions = solver.find_all_best(sg); + let solutions = solver.find_all_witnesses(sg); let extracted: Vec> = solutions .iter() @@ -179,7 +180,7 @@ fn test_constant_false() { let sg = reduction.target_problem(); let solver = BruteForce::new(); - let solutions = solver.find_all_best(sg); + let solutions = solver.find_all_witnesses(sg); let extracted: Vec> = solutions .iter() @@ -210,7 +211,7 @@ fn test_multi_input_and() { let sg = reduction.target_problem(); let solver = BruteForce::new(); - let solutions = solver.find_all_best(sg); + let solutions = solver.find_all_witnesses(sg); let extracted: Vec> = solutions .iter() diff --git a/src/unit_tests/rules/closestvectorproblem_qubo.rs b/src/unit_tests/rules/closestvectorproblem_qubo.rs index c1209d227..90938593d 100644 --- a/src/unit_tests/rules/closestvectorproblem_qubo.rs +++ b/src/unit_tests/rules/closestvectorproblem_qubo.rs @@ -75,7 +75,7 @@ fn test_duplicate_target_encodings_have_equal_qubo_value() { let reduction = ReduceTo::>::reduce_to(&canonical_cvp()); let qubo = reduction.target_problem(); let solver = BruteForce::new(); - let best = solver.find_all_best(qubo); + let best = solver.find_all_witnesses(qubo); assert!(best.contains(&vec![0, 0, 1, 0, 0, 1]) || best.contains(&vec![1, 1, 0, 1, 1, 0])); assert_close( diff --git a/src/unit_tests/rules/coloring_ilp.rs b/src/unit_tests/rules/coloring_ilp.rs index 68e83d7af..3bbee8eff 100644 --- a/src/unit_tests/rules/coloring_ilp.rs +++ b/src/unit_tests/rules/coloring_ilp.rs @@ -54,7 +54,7 @@ fn test_coloring_to_ilp_closed_loop() { let ilp_solver = ILPSolver::new(); // Solve with brute force on original problem - use find_all_satisfying for satisfaction problems - let bf_solutions = bf.find_all_satisfying(&problem); + let bf_solutions = bf.find_all_witnesses(&problem); assert!( !bf_solutions.is_empty(), "Brute force should find solutions" diff --git a/src/unit_tests/rules/coloring_qubo.rs b/src/unit_tests/rules/coloring_qubo.rs index 54ba3177c..daa61681a 100644 --- a/src/unit_tests/rules/coloring_qubo.rs +++ b/src/unit_tests/rules/coloring_qubo.rs @@ -11,7 +11,7 @@ fn test_kcoloring_to_qubo_closed_loop() { let qubo = reduction.target_problem(); let solver = BruteForce::new(); - let qubo_solutions = solver.find_all_best(qubo); + let qubo_solutions = solver.find_all_witnesses(qubo); // All solutions should extract to valid colorings for sol in &qubo_solutions { @@ -31,7 +31,7 @@ fn test_kcoloring_to_qubo_path() { let qubo = reduction.target_problem(); let solver = BruteForce::new(); - let qubo_solutions = solver.find_all_best(qubo); + let qubo_solutions = solver.find_all_witnesses(qubo); for sol in &qubo_solutions { let extracted = reduction.extract_solution(sol); @@ -51,7 +51,7 @@ fn test_kcoloring_to_qubo_reversed_edges() { let qubo = reduction.target_problem(); let solver = BruteForce::new(); - let qubo_solutions = solver.find_all_best(qubo); + let qubo_solutions = solver.find_all_witnesses(qubo); for sol in &qubo_solutions { let extracted = reduction.extract_solution(sol); diff --git a/src/unit_tests/rules/factoring_circuit.rs b/src/unit_tests/rules/factoring_circuit.rs index 9b4345e04..5cea21a14 100644 --- a/src/unit_tests/rules/factoring_circuit.rs +++ b/src/unit_tests/rules/factoring_circuit.rs @@ -311,7 +311,7 @@ fn test_jl_parity_factoring_to_circuitsat() { .unwrap(); let solver = BruteForce::new(); let jl_best_source = jl_parse_configs_set(&data["cases"][0]["best_source"]); - let best_source: HashSet> = solver.find_all_best(&source).into_iter().collect(); + let best_source: HashSet> = solver.find_all_witnesses(&source).into_iter().collect(); assert_eq!( best_source, jl_best_source, "Factoring best source mismatch" diff --git a/src/unit_tests/rules/factoring_ilp.rs b/src/unit_tests/rules/factoring_ilp.rs index 61b10a0c9..06f1fd7bf 100644 --- a/src/unit_tests/rules/factoring_ilp.rs +++ b/src/unit_tests/rules/factoring_ilp.rs @@ -177,7 +177,7 @@ fn test_factoring_to_ilp_closed_loop() { // Get brute force solutions let bf = BruteForce::new(); - let bf_solutions = bf.find_all_best(&problem); + let bf_solutions = bf.find_all_witnesses(&problem); // ILP solution should be among brute force solutions let (a, b) = problem.read_factors(&ilp_factors); diff --git a/src/unit_tests/rules/graph.rs b/src/unit_tests/rules/graph.rs index 9228a7571..db64b6fdb 100644 --- a/src/unit_tests/rules/graph.rs +++ b/src/unit_tests/rules/graph.rs @@ -322,7 +322,10 @@ fn test_aggregate_reduction_chain_extracts_value_backwards() { .reduce_aggregate_along_path(&path, &AggregateChainSource as &dyn Any) .expect("expected aggregate reduction chain"); - assert_eq!(chain.target_problem::().dims(), vec![1]); + assert_eq!( + chain.target_problem::().dims(), + vec![1] + ); assert_eq!(chain.extract_value_dyn(json!(7)), json!(12)); } @@ -1280,7 +1283,7 @@ fn test_reduce_along_path_direct() { #[test] fn test_reduction_chain_direct() { - use crate::solvers::{BruteForce, Solver}; + use crate::solvers::BruteForce; use crate::traits::Problem; let graph = ReductionGraph::new(); @@ -1307,7 +1310,7 @@ fn test_reduction_chain_direct() { let target: &MinimumVertexCover = chain.target_problem(); let solver = BruteForce::new(); - let target_solution = solver.find_best(target).unwrap(); + let target_solution = solver.find_witness(target).unwrap(); let source_solution = chain.extract_solution(&target_solution); let metric = problem.evaluate(&source_solution); assert!(metric.is_valid()); @@ -1315,7 +1318,7 @@ fn test_reduction_chain_direct() { #[test] fn test_reduction_chain_multi_step() { - use crate::solvers::{BruteForce, Solver}; + use crate::solvers::BruteForce; use crate::traits::Problem; let graph = ReductionGraph::new(); @@ -1342,7 +1345,7 @@ fn test_reduction_chain_multi_step() { let target: &MaximumSetPacking = chain.target_problem(); let solver = BruteForce::new(); - let target_solution = solver.find_best(target).unwrap(); + let target_solution = solver.find_witness(target).unwrap(); let source_solution = chain.extract_solution(&target_solution); let metric = problem.evaluate(&source_solution); assert!(metric.is_valid()); @@ -1352,7 +1355,7 @@ fn test_reduction_chain_multi_step() { fn test_reduction_chain_with_variant_casts() { use crate::models::formula::{CNFClause, KSatisfiability}; use crate::rules::MinimizeSteps; - use crate::solvers::{BruteForce, Solver}; + use crate::solvers::BruteForce; use crate::topology::UnitDiskGraph; use crate::traits::Problem; use crate::types::ProblemSize; @@ -1393,7 +1396,7 @@ fn test_reduction_chain_with_variant_casts() { let target: &MinimumVertexCover = chain.target_problem(); let solver = BruteForce::new(); - let target_solution = solver.find_best(target).unwrap(); + let target_solution = solver.find_witness(target).unwrap(); let source_solution = chain.extract_solution(&target_solution); let metric = mis.evaluate(&source_solution); assert!(metric.is_valid()); @@ -1435,7 +1438,7 @@ fn test_reduction_chain_with_variant_casts() { .unwrap(); let target: &MaximumIndependentSet = ksat_chain.target_problem(); - let target_solution = solver.find_best(target).unwrap(); + let target_solution = solver.find_witness(target).unwrap(); let original_solution = ksat_chain.extract_solution(&target_solution); // Verify the extracted solution satisfies the original 3-SAT formula diff --git a/src/unit_tests/rules/graphpartitioning_ilp.rs b/src/unit_tests/rules/graphpartitioning_ilp.rs index 30f0ae1eb..bb0ec4e4e 100644 --- a/src/unit_tests/rules/graphpartitioning_ilp.rs +++ b/src/unit_tests/rules/graphpartitioning_ilp.rs @@ -4,7 +4,7 @@ use crate::models::graph::GraphPartitioning; use crate::solvers::{BruteForce, ILPSolver}; use crate::topology::SimpleGraph; use crate::traits::Problem; -use crate::types::SolutionSize; +use crate::types::Min; fn canonical_instance() -> GraphPartitioning { let graph = SimpleGraph::new( @@ -83,15 +83,15 @@ fn test_graphpartitioning_to_ilp_closed_loop() { let bf = BruteForce::new(); let ilp_solver = ILPSolver::new(); - let bf_solutions = bf.find_all_best(&problem); + let bf_solutions = bf.find_all_witnesses(&problem); let bf_obj = problem.evaluate(&bf_solutions[0]); let ilp_solution = ilp_solver.solve(ilp).expect("ILP should be solvable"); let extracted = reduction.extract_solution(&ilp_solution); let ilp_obj = problem.evaluate(&extracted); - assert_eq!(bf_obj, SolutionSize::Valid(3)); - assert_eq!(ilp_obj, SolutionSize::Valid(3)); + assert_eq!(bf_obj, Min(Some(3))); + assert_eq!(ilp_obj, Min(Some(3))); } #[test] @@ -116,7 +116,7 @@ fn test_solution_extraction() { let extracted = reduction.extract_solution(&ilp_solution); assert_eq!(extracted, vec![0, 0, 0, 1, 1, 1]); - assert_eq!(problem.evaluate(&extracted), SolutionSize::Valid(3)); + assert_eq!(problem.evaluate(&extracted), Min(Some(3))); } #[test] @@ -128,5 +128,5 @@ fn test_solve_reduced() { .solve_reduced(&problem) .expect("solve_reduced should work"); - assert_eq!(problem.evaluate(&solution), SolutionSize::Valid(3)); + assert_eq!(problem.evaluate(&solution), Min(Some(3))); } diff --git a/src/unit_tests/rules/hamiltoniancircuit_travelingsalesman.rs b/src/unit_tests/rules/hamiltoniancircuit_travelingsalesman.rs index 3bd08f526..de6300cbc 100644 --- a/src/unit_tests/rules/hamiltoniancircuit_travelingsalesman.rs +++ b/src/unit_tests/rules/hamiltoniancircuit_travelingsalesman.rs @@ -2,9 +2,9 @@ use crate::models::graph::{HamiltonianCircuit, TravelingSalesman}; use crate::rules::test_helpers::assert_satisfaction_round_trip_from_optimization_target; use crate::rules::ReduceTo; use crate::rules::ReductionResult; -use crate::solvers::{BruteForce, Solver}; +use crate::solvers::BruteForce; use crate::topology::{Graph, SimpleGraph}; -use crate::types::SolutionSize; +use crate::types::Min; use crate::Problem; fn cycle4_hc() -> HamiltonianCircuit { @@ -44,13 +44,12 @@ fn test_hamiltoniancircuit_to_travelingsalesman_nonhamiltonian_cost_gap() { let reduction = ReduceTo::>::reduce_to(&source); let target = reduction.target_problem(); let best = BruteForce::new() - .find_best(target) + .find_witness(target) .expect("complete weighted graph should always admit a tour"); - match target.evaluate(&best) { - SolutionSize::Valid(cost) => assert!(cost > 4, "expected cost > 4, got {cost}"), - SolutionSize::Invalid => panic!("best TSP solution evaluated as invalid"), - } + let metric = target.evaluate(&best); + assert!(metric.is_valid(), "best TSP solution evaluated as invalid"); + assert!(metric.unwrap() > 4, "expected cost > 4"); } #[test] @@ -68,7 +67,7 @@ fn test_hamiltoniancircuit_to_travelingsalesman_extract_solution_cycle() { let extracted = reduction.extract_solution(&target_solution); - assert_eq!(target.evaluate(&target_solution), SolutionSize::Valid(4)); + assert_eq!(target.evaluate(&target_solution), Min(Some(4))); assert_eq!(extracted.len(), 4); assert!(source.evaluate(&extracted)); } diff --git a/src/unit_tests/rules/ilp_bool_ilp_i32.rs b/src/unit_tests/rules/ilp_bool_ilp_i32.rs index 4f89d8599..1e824f1fa 100644 --- a/src/unit_tests/rules/ilp_bool_ilp_i32.rs +++ b/src/unit_tests/rules/ilp_bool_ilp_i32.rs @@ -1,6 +1,6 @@ use crate::models::algebraic::{LinearConstraint, ObjectiveSense, ILP}; use crate::rules::traits::{ReduceTo, ReductionResult}; -use crate::solvers::{BruteForce, Solver}; +use crate::solvers::BruteForce; use crate::traits::Problem; #[test] @@ -19,7 +19,7 @@ fn test_ilp_bool_to_ilp_i32_closed_loop() { // Find optimal on source via brute force let solver = BruteForce::new(); let source_best = solver - .find_best(&source) + .find_witness(&source) .expect("source should have optimal"); let source_obj = source.evaluate(&source_best); diff --git a/src/unit_tests/rules/ilp_qubo.rs b/src/unit_tests/rules/ilp_qubo.rs index eea8f38b8..071d28743 100644 --- a/src/unit_tests/rules/ilp_qubo.rs +++ b/src/unit_tests/rules/ilp_qubo.rs @@ -21,7 +21,7 @@ fn test_ilp_to_qubo_closed_loop() { let qubo = reduction.target_problem(); let solver = BruteForce::new(); - let qubo_solutions = solver.find_all_best(qubo); + let qubo_solutions = solver.find_all_witnesses(qubo); for sol in &qubo_solutions { let extracted = reduction.extract_solution(sol); @@ -49,7 +49,7 @@ fn test_ilp_to_qubo_minimize() { let qubo = reduction.target_problem(); let solver = BruteForce::new(); - let qubo_solutions = solver.find_all_best(qubo); + let qubo_solutions = solver.find_all_witnesses(qubo); for sol in &qubo_solutions { let extracted = reduction.extract_solution(sol); @@ -79,7 +79,7 @@ fn test_ilp_to_qubo_equality() { let qubo = reduction.target_problem(); let solver = BruteForce::new(); - let qubo_solutions = solver.find_all_best(qubo); + let qubo_solutions = solver.find_all_witnesses(qubo); // Should have exactly 3 optimal solutions (C(3,2)) assert_eq!(qubo_solutions.len(), 3); @@ -113,7 +113,7 @@ fn test_ilp_to_qubo_ge_with_slack() { assert_eq!(qubo.num_variables(), 5); let solver = BruteForce::new(); - let qubo_solutions = solver.find_all_best(qubo); + let qubo_solutions = solver.find_all_witnesses(qubo); for sol in &qubo_solutions { let extracted = reduction.extract_solution(sol); @@ -147,7 +147,7 @@ fn test_ilp_to_qubo_le_with_slack() { assert_eq!(qubo.num_variables(), 5); let solver = BruteForce::new(); - let qubo_solutions = solver.find_all_best(qubo); + let qubo_solutions = solver.find_all_witnesses(qubo); for sol in &qubo_solutions { let extracted = reduction.extract_solution(sol); diff --git a/src/unit_tests/rules/integralflowbundles_ilp.rs b/src/unit_tests/rules/integralflowbundles_ilp.rs index 10455d9c7..944755ef3 100644 --- a/src/unit_tests/rules/integralflowbundles_ilp.rs +++ b/src/unit_tests/rules/integralflowbundles_ilp.rs @@ -1,6 +1,6 @@ use super::*; use crate::models::algebraic::{Comparison, ObjectiveSense, ILP}; -use crate::solvers::{BruteForce, ILPSolver, Solver}; +use crate::solvers::{BruteForce, ILPSolver}; use crate::topology::DirectedGraph; use crate::traits::Problem; @@ -67,7 +67,7 @@ fn test_integral_flow_bundles_to_ilp_structure() { fn test_integral_flow_bundles_to_ilp_closed_loop() { let problem = yes_instance(); let direct = BruteForce::new() - .find_satisfying(&problem) + .find_witness(&problem) .expect("source instance should be satisfiable"); assert!(problem.evaluate(&direct)); diff --git a/src/unit_tests/rules/knapsack_qubo.rs b/src/unit_tests/rules/knapsack_qubo.rs index 2c18acc1a..568cdab1f 100644 --- a/src/unit_tests/rules/knapsack_qubo.rs +++ b/src/unit_tests/rules/knapsack_qubo.rs @@ -27,7 +27,7 @@ fn test_knapsack_to_qubo_single_item() { assert_eq!(qubo.num_vars(), 2); let solver = BruteForce::new(); - let best_target = solver.find_all_best(qubo); + let best_target = solver.find_all_witnesses(qubo); let extracted = reduction.extract_solution(&best_target[0]); assert_eq!(extracted, vec![1]); } @@ -39,7 +39,7 @@ fn test_knapsack_to_qubo_infeasible_rejected() { let qubo = reduction.target_problem(); let solver = BruteForce::new(); - let best_target = solver.find_all_best(qubo); + let best_target = solver.find_all_witnesses(qubo); for sol in &best_target { let source_sol = reduction.extract_solution(sol); @@ -60,7 +60,7 @@ fn test_knapsack_to_qubo_empty() { assert_eq!(qubo.num_vars(), 3); let solver = BruteForce::new(); - let best_target = solver.find_all_best(qubo); + let best_target = solver.find_all_witnesses(qubo); let extracted = reduction.extract_solution(&best_target[0]); assert_eq!(extracted, vec![0, 0]); } diff --git a/src/unit_tests/rules/ksatisfiability_qubo.rs b/src/unit_tests/rules/ksatisfiability_qubo.rs index ac7fe8d6f..9b64eccee 100644 --- a/src/unit_tests/rules/ksatisfiability_qubo.rs +++ b/src/unit_tests/rules/ksatisfiability_qubo.rs @@ -21,7 +21,7 @@ fn test_ksatisfiability_to_qubo_closed_loop() { let qubo = reduction.target_problem(); let solver = BruteForce::new(); - let qubo_solutions = solver.find_all_best(qubo); + let qubo_solutions = solver.find_all_witnesses(qubo); // Verify all solutions satisfy all clauses for sol in &qubo_solutions { @@ -38,7 +38,7 @@ fn test_ksatisfiability_to_qubo_simple() { let qubo = reduction.target_problem(); let solver = BruteForce::new(); - let qubo_solutions = solver.find_all_best(qubo); + let qubo_solutions = solver.find_all_witnesses(qubo); for sol in &qubo_solutions { let extracted = reduction.extract_solution(sol); @@ -62,7 +62,7 @@ fn test_ksatisfiability_to_qubo_contradiction() { let qubo = reduction.target_problem(); let solver = BruteForce::new(); - let qubo_solutions = solver.find_all_best(qubo); + let qubo_solutions = solver.find_all_witnesses(qubo); // Both x=0 and x=1 satisfy exactly 1 clause assert_eq!(qubo_solutions.len(), 2); @@ -83,7 +83,7 @@ fn test_ksatisfiability_to_qubo_reversed_vars() { let qubo = reduction.target_problem(); let solver = BruteForce::new(); - let qubo_solutions = solver.find_all_best(qubo); + let qubo_solutions = solver.find_all_witnesses(qubo); for sol in &qubo_solutions { let extracted = reduction.extract_solution(sol); @@ -126,7 +126,7 @@ fn test_k3satisfiability_to_qubo_closed_loop() { assert_eq!(qubo.num_variables(), 12); let solver = BruteForce::new(); - let qubo_solutions = solver.find_all_best(qubo); + let qubo_solutions = solver.find_all_witnesses(qubo); // Verify all extracted solutions maximize satisfied clauses for sol in &qubo_solutions { @@ -149,7 +149,7 @@ fn test_k3satisfiability_to_qubo_single_clause() { assert_eq!(qubo.num_variables(), 4); let solver = BruteForce::new(); - let qubo_solutions = solver.find_all_best(qubo); + let qubo_solutions = solver.find_all_witnesses(qubo); // All solutions should satisfy the single clause for sol in &qubo_solutions { @@ -169,7 +169,7 @@ fn test_k3satisfiability_to_qubo_all_negated() { let qubo = reduction.target_problem(); let solver = BruteForce::new(); - let qubo_solutions = solver.find_all_best(qubo); + let qubo_solutions = solver.find_all_witnesses(qubo); for sol in &qubo_solutions { let extracted = reduction.extract_solution(sol); diff --git a/src/unit_tests/rules/ksatisfiability_subsetsum.rs b/src/unit_tests/rules/ksatisfiability_subsetsum.rs index fddf38c34..30ffca725 100644 --- a/src/unit_tests/rules/ksatisfiability_subsetsum.rs +++ b/src/unit_tests/rules/ksatisfiability_subsetsum.rs @@ -1,6 +1,6 @@ use super::*; use crate::models::formula::CNFClause; -use crate::solvers::{BruteForce, Solver}; +use crate::solvers::BruteForce; use crate::traits::Problem; use crate::variant::K3; use num_bigint::BigUint; @@ -25,7 +25,7 @@ fn test_ksatisfiability_to_subsetsum_closed_loop() { assert_eq!(target.target(), &BigUint::from(11144u32)); let solver = BruteForce::new(); - let solutions = solver.find_all_satisfying(target); + let solutions = solver.find_all_witnesses(target); assert!(!solutions.is_empty()); // Every SubsetSum solution must map back to a satisfying 3-SAT assignment @@ -53,7 +53,7 @@ fn test_ksatisfiability_to_subsetsum_unsatisfiable() { let target = reduction.target_problem(); let solver = BruteForce::new(); - let solution = solver.find_satisfying(target); + let solution = solver.find_witness(target); assert!(solution.is_none()); } @@ -68,7 +68,7 @@ fn test_ksatisfiability_to_subsetsum_single_clause() { assert_eq!(target.num_elements(), 8); let solver = BruteForce::new(); - let solutions = solver.find_all_satisfying(target); + let solutions = solver.find_all_witnesses(target); // Each SubsetSum solution maps to a satisfying assignment let mut sat_assignments = std::collections::HashSet::new(); @@ -118,7 +118,7 @@ fn test_ksatisfiability_to_subsetsum_all_negated() { let target = reduction.target_problem(); let solver = BruteForce::new(); - let solutions = solver.find_all_satisfying(target); + let solutions = solver.find_all_witnesses(target); let mut sat_assignments = std::collections::HashSet::new(); for sol in &solutions { diff --git a/src/unit_tests/rules/longestcommonsubsequence_ilp.rs b/src/unit_tests/rules/longestcommonsubsequence_ilp.rs index 90cf2aa71..d173fb5f4 100644 --- a/src/unit_tests/rules/longestcommonsubsequence_ilp.rs +++ b/src/unit_tests/rules/longestcommonsubsequence_ilp.rs @@ -1,6 +1,6 @@ use super::*; use crate::models::algebraic::ILP; -use crate::solvers::{BruteForce, ILPSolver, Solver}; +use crate::solvers::{BruteForce, ILPSolver}; use crate::traits::Problem; #[test] @@ -48,7 +48,7 @@ fn test_lcs_to_ilp_closed_loop_three_strings() { let brute_force = BruteForce::new(); let witness = brute_force - .find_satisfying(&problem) + .find_witness(&problem) .expect("bruteforce should also find a witness"); assert!(problem.evaluate(&witness)); } diff --git a/src/unit_tests/rules/longestpath_ilp.rs b/src/unit_tests/rules/longestpath_ilp.rs index 6f8411e05..d2aea4305 100644 --- a/src/unit_tests/rules/longestpath_ilp.rs +++ b/src/unit_tests/rules/longestpath_ilp.rs @@ -1,9 +1,9 @@ use super::*; use crate::models::algebraic::{ObjectiveSense, ILP}; -use crate::solvers::{BruteForce, ILPSolver, Solver}; +use crate::solvers::{BruteForce, ILPSolver}; use crate::topology::SimpleGraph; use crate::traits::Problem; -use crate::types::SolutionSize; +use crate::types::Max; fn issue_problem() -> LongestPath { LongestPath::new( @@ -59,10 +59,10 @@ fn test_longestpath_to_ilp_closed_loop_on_issue_example() { let problem = issue_problem(); let brute_force = BruteForce::new(); let best = brute_force - .find_best(&problem) + .find_witness(&problem) .expect("brute-force optimum"); let best_value = problem.evaluate(&best); - assert_eq!(best_value, SolutionSize::Valid(20)); + assert_eq!(best_value, Max(Some(20))); let reduction: ReductionLongestPathToILP = ReduceTo::>::reduce_to(&problem); let ilp_solver = ILPSolver::new(); @@ -85,7 +85,7 @@ fn test_solution_extraction_from_handcrafted_ilp_assignment() { let extracted = reduction.extract_solution(&target_solution); assert_eq!(extracted, vec![1, 1]); - assert_eq!(problem.evaluate(&extracted), SolutionSize::Valid(5)); + assert_eq!(problem.evaluate(&extracted), Max(Some(5))); } #[test] @@ -104,5 +104,5 @@ fn test_source_equals_target_uses_empty_path() { let extracted = reduction.extract_solution(&ilp_solution); assert_eq!(extracted, vec![0, 0, 0]); - assert_eq!(problem.evaluate(&extracted), SolutionSize::Valid(0)); + assert_eq!(problem.evaluate(&extracted), Max(Some(0))); } diff --git a/src/unit_tests/rules/maximumclique_maximumindependentset.rs b/src/unit_tests/rules/maximumclique_maximumindependentset.rs index 8e871f7ba..62520343b 100644 --- a/src/unit_tests/rules/maximumclique_maximumindependentset.rs +++ b/src/unit_tests/rules/maximumclique_maximumindependentset.rs @@ -3,7 +3,6 @@ use crate::rules::test_helpers::assert_optimization_round_trip_from_optimization use crate::solvers::BruteForce; use crate::topology::Graph; use crate::traits::Problem; -use crate::types::SolutionSize; #[test] fn test_maximumclique_to_maximumindependentset_closed_loop() { @@ -44,7 +43,7 @@ fn test_maximumclique_to_maximumindependentset_triangle() { assert_eq!(target.graph().num_edges(), 0); let solver = BruteForce::new(); - let target_solutions = solver.find_all_best(target); + let target_solutions = solver.find_all_witnesses(target); // MIS on empty graph is all vertices selected assert!(target_solutions @@ -53,10 +52,7 @@ fn test_maximumclique_to_maximumindependentset_triangle() { // Extract solution: should be the full clique {0,1,2} let source_sol = reduction.extract_solution(&target_solutions[0]); - assert!(matches!( - source.evaluate(&source_sol), - SolutionSize::Valid(3) - )); + assert_eq!(source.evaluate(&source_sol).unwrap(), 3); } #[test] @@ -80,7 +76,7 @@ fn test_maximumclique_to_maximumindependentset_empty_graph() { assert_eq!(target.graph().num_edges(), 3); let solver = BruteForce::new(); - let target_solutions = solver.find_all_best(target); + let target_solutions = solver.find_all_witnesses(target); // MIS on K3 is any single vertex assert!(target_solutions diff --git a/src/unit_tests/rules/maximumindependentset_gridgraph.rs b/src/unit_tests/rules/maximumindependentset_gridgraph.rs index 52c6ee6ca..d7da5b0af 100644 --- a/src/unit_tests/rules/maximumindependentset_gridgraph.rs +++ b/src/unit_tests/rules/maximumindependentset_gridgraph.rs @@ -44,7 +44,7 @@ fn test_mis_simple_one_to_kings_one_closed_loop() { assert!(target.graph().num_vertices() > 5); let solver = BruteForce::new(); - let grid_solutions = solver.find_all_best(target); + let grid_solutions = solver.find_all_witnesses(target); assert!(!grid_solutions.is_empty()); let original_solution = result.extract_solution(&grid_solutions[0]); diff --git a/src/unit_tests/rules/maximumindependentset_ilp.rs b/src/unit_tests/rules/maximumindependentset_ilp.rs index 122ab6abe..5d2146374 100644 --- a/src/unit_tests/rules/maximumindependentset_ilp.rs +++ b/src/unit_tests/rules/maximumindependentset_ilp.rs @@ -4,7 +4,7 @@ use crate::rules::{MinimizeSteps, ReductionChain, ReductionGraph, ReductionPath} use crate::solvers::{BruteForce, ILPSolver}; use crate::topology::SimpleGraph; use crate::traits::Problem; -use crate::types::{ProblemSize, SolutionSize}; +use crate::types::{Max, ProblemSize}; fn reduce_mis_to_ilp( problem: &MaximumIndependentSet, @@ -64,7 +64,7 @@ fn test_maximumindependentset_to_ilp_via_path_closed_loop() { let bf = BruteForce::new(); let ilp_solver = ILPSolver::new(); - let bf_solutions = bf.find_all_best(&problem); + let bf_solutions = bf.find_all_witnesses(&problem); let ilp_solution = ilp_solver.solve(ilp).expect("ILP should be solvable"); let extracted = chain.extract_solution(&ilp_solution); @@ -86,6 +86,6 @@ fn test_maximumindependentset_to_ilp_via_path_weighted() { let ilp_solution = ilp_solver.solve(ilp).expect("ILP should be solvable"); let extracted = chain.extract_solution(&ilp_solution); - assert_eq!(problem.evaluate(&extracted), SolutionSize::Valid(100)); + assert_eq!(problem.evaluate(&extracted), Max(Some(100))); assert_eq!(extracted, vec![0, 1, 0]); } diff --git a/src/unit_tests/rules/maximumindependentset_maximumclique.rs b/src/unit_tests/rules/maximumindependentset_maximumclique.rs index 9225eaf72..a8d485189 100644 --- a/src/unit_tests/rules/maximumindependentset_maximumclique.rs +++ b/src/unit_tests/rules/maximumindependentset_maximumclique.rs @@ -41,7 +41,7 @@ fn test_maximumindependentset_to_maximumclique_weighted() { // In empty graph, max clique is a single vertex. Best is vertex 2 (weight 30). let solver = BruteForce::new(); - let best = solver.find_all_best(target); + let best = solver.find_all_witnesses(target); for sol in &best { let extracted = reduction.extract_solution(sol); let metric = source.evaluate(&extracted); @@ -62,7 +62,7 @@ fn test_maximumindependentset_to_maximumclique_empty_graph() { // All 4 vertices form a clique in complement = all 4 are independent set in source let solver = BruteForce::new(); - let best_target = solver.find_all_best(target); + let best_target = solver.find_all_witnesses(target); assert!(best_target.iter().all(|s| s.iter().sum::() == 4)); } @@ -80,6 +80,6 @@ fn test_maximumindependentset_to_maximumclique_complete_graph() { // Max clique in empty graph is single vertex, max IS in K4 is also single vertex let solver = BruteForce::new(); - let best = solver.find_all_best(target); + let best = solver.find_all_witnesses(target); assert!(best.iter().all(|s| s.iter().sum::() == 1)); } diff --git a/src/unit_tests/rules/maximumindependentset_maximumsetpacking.rs b/src/unit_tests/rules/maximumindependentset_maximumsetpacking.rs index 6ed1983c6..880265a7d 100644 --- a/src/unit_tests/rules/maximumindependentset_maximumsetpacking.rs +++ b/src/unit_tests/rules/maximumindependentset_maximumsetpacking.rs @@ -26,7 +26,7 @@ fn test_empty_graph() { assert_eq!(sp_problem.num_sets(), 3); let solver = BruteForce::new(); - let solutions = solver.find_all_best(sp_problem); + let solutions = solver.find_all_witnesses(sp_problem); // With no overlaps, we can select all sets assert_eq!(solutions[0].iter().sum::(), 3); @@ -81,7 +81,7 @@ fn test_jl_parity_is_to_setpacking() { MaximumIndependentSet::new(SimpleGraph::new(nv, jl_parse_edges(inst)), vec![1i32; nv]); let result = ReduceTo::>::reduce_to(&source); let solver = BruteForce::new(); - let best_source: HashSet> = solver.find_all_best(&source).into_iter().collect(); + let best_source: HashSet> = solver.find_all_witnesses(&source).into_iter().collect(); assert_optimization_round_trip_from_optimization_target( &source, &result, @@ -104,7 +104,7 @@ fn test_jl_parity_setpacking_to_is() { let source = MaximumSetPacking::::new(jl_parse_sets(&inst["sets"])); let result = ReduceTo::>::reduce_to(&source); let solver = BruteForce::new(); - let best_source: HashSet> = solver.find_all_best(&source).into_iter().collect(); + let best_source: HashSet> = solver.find_all_witnesses(&source).into_iter().collect(); assert_optimization_round_trip_from_optimization_target( &source, &result, @@ -129,7 +129,7 @@ fn test_jl_parity_rule_is_to_setpacking() { MaximumIndependentSet::new(SimpleGraph::new(nv, jl_parse_edges(inst)), vec![1i32; nv]); let result = ReduceTo::>::reduce_to(&source); let solver = BruteForce::new(); - let best_source: HashSet> = solver.find_all_best(&source).into_iter().collect(); + let best_source: HashSet> = solver.find_all_witnesses(&source).into_iter().collect(); assert_optimization_round_trip_from_optimization_target( &source, &result, @@ -155,7 +155,7 @@ fn test_jl_parity_doc_is_to_setpacking() { MaximumIndependentSet::new(SimpleGraph::new(nv, jl_parse_edges(inst)), vec![1i32; nv]); let result = ReduceTo::>::reduce_to(&source); let solver = BruteForce::new(); - let best_source: HashSet> = solver.find_all_best(&source).into_iter().collect(); + let best_source: HashSet> = solver.find_all_witnesses(&source).into_iter().collect(); assert_optimization_round_trip_from_optimization_target( &source, &result, @@ -177,7 +177,7 @@ fn test_maximumindependentset_one_to_maximumsetpacking_closed_loop() { assert_eq!(sp_problem.num_sets(), 3); let solver = BruteForce::new(); - let sp_solutions = solver.find_all_best(sp_problem); + let sp_solutions = solver.find_all_witnesses(sp_problem); assert!(!sp_solutions.is_empty()); let original_solution = reduction.extract_solution(&sp_solutions[0]); @@ -197,7 +197,7 @@ fn test_maximumsetpacking_one_to_maximumindependentset_closed_loop() { assert_eq!(is_problem.graph().num_vertices(), 3); let solver = BruteForce::new(); - let is_solutions = solver.find_all_best(is_problem); + let is_solutions = solver.find_all_witnesses(is_problem); assert!(!is_solutions.is_empty()); let original_solution = reduction.extract_solution(&is_solutions[0]); diff --git a/src/unit_tests/rules/maximumindependentset_qubo.rs b/src/unit_tests/rules/maximumindependentset_qubo.rs index 8a7c70365..1e297ba80 100644 --- a/src/unit_tests/rules/maximumindependentset_qubo.rs +++ b/src/unit_tests/rules/maximumindependentset_qubo.rs @@ -1,10 +1,10 @@ use crate::models::algebraic::QUBO; use crate::models::graph::MaximumIndependentSet; use crate::rules::{Minimize, ReductionChain, ReductionGraph, ReductionPath}; -use crate::solvers::{BruteForce, Solver}; +use crate::solvers::BruteForce; use crate::topology::{Graph, SimpleGraph}; use crate::traits::Problem; -use crate::types::{ProblemSize, SolutionSize}; +use crate::types::{Max, ProblemSize}; fn reduce_mis_to_qubo( problem: &MaximumIndependentSet, @@ -51,7 +51,7 @@ fn test_maximumindependentset_to_qubo_via_path_closed_loop() { assert_eq!(qubo.num_variables(), 4); let solver = BruteForce::new(); - let qubo_solutions = solver.find_all_best(qubo); + let qubo_solutions = solver.find_all_witnesses(qubo); for sol in &qubo_solutions { let extracted = chain.extract_solution(sol); assert!(problem.evaluate(&extracted).is_valid()); @@ -68,11 +68,11 @@ fn test_maximumindependentset_to_qubo_via_path_weighted() { let solver = BruteForce::new(); let qubo_solution = solver - .find_best(qubo) + .find_witness(qubo) .expect("QUBO should be solvable via path"); let extracted = chain.extract_solution(&qubo_solution); - assert_eq!(problem.evaluate(&extracted), SolutionSize::Valid(100)); + assert_eq!(problem.evaluate(&extracted), Max(Some(100))); assert_eq!(extracted, vec![0, 1, 0]); } @@ -85,9 +85,9 @@ fn test_maximumindependentset_to_qubo_via_path_empty_graph() { assert_eq!(qubo.num_variables(), 3); let solver = BruteForce::new(); - let qubo_solution = solver.find_best(qubo).expect("QUBO should be solvable"); + let qubo_solution = solver.find_witness(qubo).expect("QUBO should be solvable"); let extracted = chain.extract_solution(&qubo_solution); assert_eq!(extracted, vec![1, 1, 1]); - assert_eq!(problem.evaluate(&extracted), SolutionSize::Valid(3)); + assert_eq!(problem.evaluate(&extracted), Max(Some(3))); } diff --git a/src/unit_tests/rules/maximummatching_ilp.rs b/src/unit_tests/rules/maximummatching_ilp.rs index 15b2a5779..dab0c1680 100644 --- a/src/unit_tests/rules/maximummatching_ilp.rs +++ b/src/unit_tests/rules/maximummatching_ilp.rs @@ -2,7 +2,7 @@ use super::*; use crate::solvers::{BruteForce, ILPSolver}; use crate::topology::SimpleGraph; use crate::traits::Problem; -use crate::types::SolutionSize; +use crate::types::Max; #[test] fn test_reduction_creates_valid_ilp() { @@ -55,7 +55,7 @@ fn test_maximummatching_to_ilp_closed_loop() { let ilp_solver = ILPSolver::new(); // Solve with brute force on original problem - let bf_solutions = bf.find_all_best(&problem); + let bf_solutions = bf.find_all_witnesses(&problem); // Solve via ILP reduction let ilp_solution = ilp_solver.solve(ilp).expect("ILP should be solvable"); @@ -64,8 +64,8 @@ fn test_maximummatching_to_ilp_closed_loop() { // Both should find optimal size = 1 (one edge) let bf_size = problem.evaluate(&bf_solutions[0]); let ilp_size = problem.evaluate(&extracted); - assert_eq!(bf_size, SolutionSize::Valid(1)); - assert_eq!(ilp_size, SolutionSize::Valid(1)); + assert_eq!(bf_size, Max(Some(1))); + assert_eq!(ilp_size, Max(Some(1))); // Verify the ILP solution is valid for the original problem assert!( @@ -86,7 +86,7 @@ fn test_ilp_solution_equals_brute_force_path() { let ilp_solver = ILPSolver::new(); // Solve with brute force - let bf_solutions = bf.find_all_best(&problem); + let bf_solutions = bf.find_all_witnesses(&problem); let bf_size = problem.evaluate(&bf_solutions[0]); // Solve via ILP @@ -94,8 +94,8 @@ fn test_ilp_solution_equals_brute_force_path() { let extracted = reduction.extract_solution(&ilp_solution); let ilp_size = problem.evaluate(&extracted); - assert_eq!(bf_size, SolutionSize::Valid(2)); - assert_eq!(ilp_size, SolutionSize::Valid(2)); + assert_eq!(bf_size, Max(Some(2))); + assert_eq!(ilp_size, Max(Some(2))); // Verify validity assert!(problem.evaluate(&extracted).is_valid()); @@ -114,15 +114,15 @@ fn test_ilp_solution_equals_brute_force_weighted() { let bf = BruteForce::new(); let ilp_solver = ILPSolver::new(); - let bf_solutions = bf.find_all_best(&problem); + let bf_solutions = bf.find_all_witnesses(&problem); let bf_obj = problem.evaluate(&bf_solutions[0]); let ilp_solution = ilp_solver.solve(ilp).expect("ILP should be solvable"); let extracted = reduction.extract_solution(&ilp_solution); let ilp_obj = problem.evaluate(&extracted); - assert_eq!(bf_obj, SolutionSize::Valid(100)); - assert_eq!(ilp_obj, SolutionSize::Valid(100)); + assert_eq!(bf_obj, Max(Some(100))); + assert_eq!(ilp_obj, Max(Some(100))); // Verify the solution selects edge 0 (0-1) assert_eq!(extracted, vec![1, 0]); @@ -169,7 +169,7 @@ fn test_empty_graph() { assert_eq!(ilp.constraints.len(), 0); assert!(problem.evaluate(&[]).is_valid()); - assert_eq!(problem.evaluate(&[]), SolutionSize::Valid(0)); + assert_eq!(problem.evaluate(&[]), Max(Some(0))); } #[test] @@ -191,7 +191,7 @@ fn test_k4_perfect_matching() { let extracted = reduction.extract_solution(&ilp_solution); assert!(problem.evaluate(&extracted).is_valid()); - assert_eq!(problem.evaluate(&extracted), SolutionSize::Valid(2)); // Perfect matching has 2 edges + assert_eq!(problem.evaluate(&extracted), Max(Some(2))); // Perfect matching has 2 edges // Verify all vertices are matched let sum: usize = extracted.iter().sum(); @@ -212,7 +212,7 @@ fn test_star_graph() { let extracted = reduction.extract_solution(&ilp_solution); assert!(problem.evaluate(&extracted).is_valid()); - assert_eq!(problem.evaluate(&extracted), SolutionSize::Valid(1)); + assert_eq!(problem.evaluate(&extracted), Max(Some(1))); } #[test] @@ -231,7 +231,7 @@ fn test_bipartite_graph() { let extracted = reduction.extract_solution(&ilp_solution); assert!(problem.evaluate(&extracted).is_valid()); - assert_eq!(problem.evaluate(&extracted), SolutionSize::Valid(2)); + assert_eq!(problem.evaluate(&extracted), Max(Some(2))); } #[test] @@ -246,5 +246,5 @@ fn test_solve_reduced() { .expect("solve_reduced should work"); assert!(problem.evaluate(&solution).is_valid()); - assert_eq!(problem.evaluate(&solution), SolutionSize::Valid(2)); + assert_eq!(problem.evaluate(&solution), Max(Some(2))); } diff --git a/src/unit_tests/rules/maximummatching_maximumsetpacking.rs b/src/unit_tests/rules/maximummatching_maximumsetpacking.rs index beab0a2c6..b72d0ee3e 100644 --- a/src/unit_tests/rules/maximummatching_maximumsetpacking.rs +++ b/src/unit_tests/rules/maximummatching_maximumsetpacking.rs @@ -3,7 +3,7 @@ use crate::rules::test_helpers::assert_optimization_round_trip_from_optimization use crate::solvers::BruteForce; use crate::topology::SimpleGraph; use crate::traits::Problem; -use crate::types::SolutionSize; +use crate::types::Max; include!("../jl_helpers.rs"); #[test] @@ -37,21 +37,15 @@ fn test_matching_to_setpacking_weighted() { assert_eq!(sp.weights_ref(), &vec![100, 1, 1]); let solver = BruteForce::new(); - let sp_solutions = solver.find_all_best(sp); + let sp_solutions = solver.find_all_witnesses(sp); // Edge 0-1 (weight 100) alone beats edges 0-2 + 1-3 (weight 2) assert!(sp_solutions.contains(&vec![1, 0, 0])); // Verify through direct MaximumMatching solution - let direct_solutions = solver.find_all_best(&matching); - assert_eq!( - matching.evaluate(&sp_solutions[0]), - SolutionSize::Valid(100) - ); - assert_eq!( - matching.evaluate(&direct_solutions[0]), - SolutionSize::Valid(100) - ); + let direct_solutions = solver.find_all_witnesses(&matching); + assert_eq!(matching.evaluate(&sp_solutions[0]), Max(Some(100))); + assert_eq!(matching.evaluate(&direct_solutions[0]), Max(Some(100))); } #[test] @@ -89,7 +83,7 @@ fn test_matching_to_setpacking_single_edge() { assert_eq!(sp.sets()[0], vec![0, 1]); let solver = BruteForce::new(); - let sp_solutions = solver.find_all_best(sp); + let sp_solutions = solver.find_all_witnesses(sp); // Should select the only set assert_eq!(sp_solutions, vec![vec![1]]); @@ -104,7 +98,7 @@ fn test_matching_to_setpacking_disjoint_edges() { let sp = reduction.target_problem(); let solver = BruteForce::new(); - let sp_solutions = solver.find_all_best(sp); + let sp_solutions = solver.find_all_witnesses(sp); // Both edges can be selected (they don't share vertices) assert_eq!(sp_solutions, vec![vec![1, 1]]); @@ -130,7 +124,7 @@ fn test_matching_to_setpacking_star() { let sp = reduction.target_problem(); let solver = BruteForce::new(); - let sp_solutions = solver.find_all_best(sp); + let sp_solutions = solver.find_all_witnesses(sp); // All edges share vertex 0, so max matching = 1 for sol in &sp_solutions { @@ -170,7 +164,8 @@ fn test_jl_parity_matching_to_setpacking() { ); let result = ReduceTo::>::reduce_to(&source); let solver = BruteForce::new(); - let best_source: HashSet> = solver.find_all_best(&source).into_iter().collect(); + let best_source: HashSet> = + solver.find_all_witnesses(&source).into_iter().collect(); assert_optimization_round_trip_from_optimization_target( &source, &result, diff --git a/src/unit_tests/rules/maximumsetpacking_casts.rs b/src/unit_tests/rules/maximumsetpacking_casts.rs index 079602986..7932ba4d1 100644 --- a/src/unit_tests/rules/maximumsetpacking_casts.rs +++ b/src/unit_tests/rules/maximumsetpacking_casts.rs @@ -1,7 +1,7 @@ use super::*; use crate::rules::traits::ReductionResult; use crate::rules::ReduceTo; -use crate::solvers::{BruteForce, Solver}; +use crate::solvers::BruteForce; use crate::traits::Problem; #[test] @@ -14,7 +14,7 @@ fn test_maximumsetpacking_one_to_i32_cast_closed_loop() { assert_eq!(sp_i32.weights_ref(), &vec![1i32, 1, 1]); let solver = BruteForce::new(); - let target_solution = solver.find_best(sp_i32).unwrap(); + let target_solution = solver.find_witness(sp_i32).unwrap(); let source_solution = reduction.extract_solution(&target_solution); let metric = sp_one.evaluate(&source_solution); @@ -31,7 +31,7 @@ fn test_maximumsetpacking_i32_to_f64_cast_closed_loop() { assert_eq!(sp_f64.weights_ref(), &vec![2.0f64, 3.0, 5.0]); let solver = BruteForce::new(); - let target_solution = solver.find_best(sp_f64).unwrap(); + let target_solution = solver.find_witness(sp_f64).unwrap(); let source_solution = reduction.extract_solution(&target_solution); let metric = sp_i32.evaluate(&source_solution); diff --git a/src/unit_tests/rules/maximumsetpacking_ilp.rs b/src/unit_tests/rules/maximumsetpacking_ilp.rs index 8cf4cdc26..ca288083c 100644 --- a/src/unit_tests/rules/maximumsetpacking_ilp.rs +++ b/src/unit_tests/rules/maximumsetpacking_ilp.rs @@ -1,7 +1,7 @@ use super::*; use crate::solvers::{BruteForce, ILPSolver}; use crate::traits::Problem; -use crate::types::SolutionSize; +use crate::types::Max; #[test] fn test_reduction_creates_valid_ilp() { @@ -47,7 +47,7 @@ fn test_maximumsetpacking_to_ilp_closed_loop() { let bf = BruteForce::new(); let ilp_solver = ILPSolver::new(); - let bf_solutions = bf.find_all_best(&problem); + let bf_solutions = bf.find_all_witnesses(&problem); let ilp_solution = ilp_solver.solve(ilp).expect("ILP should be solvable"); let extracted = reduction.extract_solution(&ilp_solution); @@ -74,15 +74,15 @@ fn test_ilp_solution_equals_brute_force_weighted() { let bf = BruteForce::new(); let ilp_solver = ILPSolver::new(); - let bf_solutions = bf.find_all_best(&problem); + let bf_solutions = bf.find_all_witnesses(&problem); let bf_obj = problem.evaluate(&bf_solutions[0]); let ilp_solution = ilp_solver.solve(ilp).expect("ILP should be solvable"); let extracted = reduction.extract_solution(&ilp_solution); let ilp_obj = problem.evaluate(&extracted); - assert_eq!(bf_obj, SolutionSize::Valid(6)); - assert_eq!(ilp_obj, SolutionSize::Valid(6)); + assert_eq!(bf_obj, Max(Some(6))); + assert_eq!(ilp_obj, Max(Some(6))); assert_eq!(extracted, vec![0, 1, 1]); } @@ -112,7 +112,7 @@ fn test_disjoint_sets() { assert_eq!(extracted, vec![1, 1, 1, 1]); assert!(problem.evaluate(&extracted).is_valid()); - assert_eq!(problem.evaluate(&extracted), SolutionSize::Valid(4)); + assert_eq!(problem.evaluate(&extracted), Max(Some(4))); } #[test] @@ -125,5 +125,5 @@ fn test_solve_reduced() { .expect("solve_reduced should work"); assert!(problem.evaluate(&solution).is_valid()); - assert_eq!(problem.evaluate(&solution), SolutionSize::Valid(2)); + assert_eq!(problem.evaluate(&solution), Max(Some(2))); } diff --git a/src/unit_tests/rules/maximumsetpacking_qubo.rs b/src/unit_tests/rules/maximumsetpacking_qubo.rs index b0274d0fa..dfad90aa2 100644 --- a/src/unit_tests/rules/maximumsetpacking_qubo.rs +++ b/src/unit_tests/rules/maximumsetpacking_qubo.rs @@ -12,7 +12,7 @@ fn test_setpacking_to_qubo_closed_loop() { let qubo = reduction.target_problem(); let solver = BruteForce::new(); - let qubo_solutions = solver.find_all_best(qubo); + let qubo_solutions = solver.find_all_witnesses(qubo); for sol in &qubo_solutions { let extracted = reduction.extract_solution(sol); @@ -29,7 +29,7 @@ fn test_setpacking_to_qubo_disjoint() { let qubo = reduction.target_problem(); let solver = BruteForce::new(); - let qubo_solutions = solver.find_all_best(qubo); + let qubo_solutions = solver.find_all_witnesses(qubo); for sol in &qubo_solutions { let extracted = reduction.extract_solution(sol); @@ -47,7 +47,7 @@ fn test_setpacking_to_qubo_all_overlap() { let qubo = reduction.target_problem(); let solver = BruteForce::new(); - let qubo_solutions = solver.find_all_best(qubo); + let qubo_solutions = solver.find_all_witnesses(qubo); for sol in &qubo_solutions { let extracted = reduction.extract_solution(sol); diff --git a/src/unit_tests/rules/minimumdominatingset_ilp.rs b/src/unit_tests/rules/minimumdominatingset_ilp.rs index 01cd49e57..073c304ce 100644 --- a/src/unit_tests/rules/minimumdominatingset_ilp.rs +++ b/src/unit_tests/rules/minimumdominatingset_ilp.rs @@ -1,7 +1,7 @@ use super::*; use crate::solvers::{BruteForce, ILPSolver}; use crate::traits::Problem; -use crate::types::SolutionSize; +use crate::types::Min; #[test] fn test_reduction_creates_valid_ilp() { @@ -60,7 +60,7 @@ fn test_minimumdominatingset_to_ilp_closed_loop() { let ilp_solver = ILPSolver::new(); // Solve with brute force on original problem - let bf_solutions = bf.find_all_best(&problem); + let bf_solutions = bf.find_all_witnesses(&problem); let bf_size = problem.evaluate(&bf_solutions[0]); // Solve via ILP reduction @@ -69,8 +69,8 @@ fn test_minimumdominatingset_to_ilp_closed_loop() { let ilp_size = problem.evaluate(&extracted); // Both should find optimal size = 1 (just the center) - assert_eq!(bf_size, SolutionSize::Valid(1)); - assert_eq!(ilp_size, SolutionSize::Valid(1)); + assert_eq!(bf_size, Min(Some(1))); + assert_eq!(ilp_size, Min(Some(1))); // Verify the ILP solution is valid for the original problem assert!( @@ -93,7 +93,7 @@ fn test_ilp_solution_equals_brute_force_path() { let ilp_solver = ILPSolver::new(); // Solve with brute force - let bf_solutions = bf.find_all_best(&problem); + let bf_solutions = bf.find_all_witnesses(&problem); let bf_size = problem.evaluate(&bf_solutions[0]); // Solve via ILP @@ -101,8 +101,8 @@ fn test_ilp_solution_equals_brute_force_path() { let extracted = reduction.extract_solution(&ilp_solution); let ilp_size = problem.evaluate(&extracted); - assert_eq!(bf_size, SolutionSize::Valid(2)); - assert_eq!(ilp_size, SolutionSize::Valid(2)); + assert_eq!(bf_size, Min(Some(2))); + assert_eq!(ilp_size, Min(Some(2))); // Verify validity assert!(problem.evaluate(&extracted).is_valid()); @@ -122,15 +122,15 @@ fn test_ilp_solution_equals_brute_force_weighted() { let bf = BruteForce::new(); let ilp_solver = ILPSolver::new(); - let bf_solutions = bf.find_all_best(&problem); + let bf_solutions = bf.find_all_witnesses(&problem); let bf_obj = problem.evaluate(&bf_solutions[0]); let ilp_solution = ilp_solver.solve(ilp).expect("ILP should be solvable"); let extracted = reduction.extract_solution(&ilp_solution); let ilp_obj = problem.evaluate(&extracted); - assert_eq!(bf_obj, SolutionSize::Valid(3)); - assert_eq!(ilp_obj, SolutionSize::Valid(3)); + assert_eq!(bf_obj, Min(Some(3))); + assert_eq!(ilp_obj, Min(Some(3))); // Verify the solution selects all leaves assert_eq!(extracted, vec![0, 1, 1, 1]); @@ -196,7 +196,7 @@ fn test_complete_graph() { let extracted = reduction.extract_solution(&ilp_solution); assert!(problem.evaluate(&extracted).is_valid()); - assert_eq!(problem.evaluate(&extracted), SolutionSize::Valid(1)); + assert_eq!(problem.evaluate(&extracted), Min(Some(1))); } #[test] @@ -213,7 +213,7 @@ fn test_single_vertex() { assert_eq!(extracted, vec![1]); assert!(problem.evaluate(&extracted).is_valid()); - assert_eq!(problem.evaluate(&extracted), SolutionSize::Valid(1)); + assert_eq!(problem.evaluate(&extracted), Min(Some(1))); } #[test] @@ -230,7 +230,7 @@ fn test_cycle_graph() { let bf = BruteForce::new(); let ilp_solver = ILPSolver::new(); - let bf_solutions = bf.find_all_best(&problem); + let bf_solutions = bf.find_all_witnesses(&problem); let bf_size = problem.evaluate(&bf_solutions[0]); let ilp_solution = ilp_solver.solve(ilp).expect("ILP should be solvable"); diff --git a/src/unit_tests/rules/minimumfeedbackvertexset_ilp.rs b/src/unit_tests/rules/minimumfeedbackvertexset_ilp.rs index 66be3cbff..762157e7e 100644 --- a/src/unit_tests/rules/minimumfeedbackvertexset_ilp.rs +++ b/src/unit_tests/rules/minimumfeedbackvertexset_ilp.rs @@ -2,7 +2,7 @@ use super::*; use crate::solvers::{BruteForce, ILPSolver}; use crate::topology::DirectedGraph; use crate::traits::Problem; -use crate::types::SolutionSize; +use crate::types::Min; #[test] fn test_reduction_creates_valid_ilp() { @@ -32,7 +32,7 @@ fn test_minimumfeedbackvertexset_to_ilp_closed_loop() { let ilp_solver = ILPSolver::new(); // Solve with brute force on original problem - let bf_solutions = bf.find_all_best(&problem); + let bf_solutions = bf.find_all_witnesses(&problem); let bf_size = problem.evaluate(&bf_solutions[0]); // Solve via ILP reduction @@ -41,8 +41,8 @@ fn test_minimumfeedbackvertexset_to_ilp_closed_loop() { let ilp_size = problem.evaluate(&extracted); // Both should find optimal size = 1 - assert_eq!(bf_size, SolutionSize::Valid(1)); - assert_eq!(ilp_size, SolutionSize::Valid(1)); + assert_eq!(bf_size, Min(Some(1))); + assert_eq!(ilp_size, Min(Some(1))); // Verify the ILP solution is valid for the original problem assert!( @@ -89,7 +89,7 @@ fn test_cycle_of_triangles() { let extracted = reduction.extract_solution(&ilp_solution); let size = problem.evaluate(&extracted); - assert_eq!(size, SolutionSize::Valid(3), "FVS should be 3"); + assert_eq!(size, Min(Some(3)), "FVS should be 3"); } #[test] @@ -105,7 +105,7 @@ fn test_dag_no_removal() { let extracted = reduction.extract_solution(&ilp_solution); let size = problem.evaluate(&extracted); - assert_eq!(size, SolutionSize::Valid(0), "DAG needs no removal"); + assert_eq!(size, Min(Some(0)), "DAG needs no removal"); assert_eq!(extracted, vec![0, 0, 0]); } @@ -126,7 +126,7 @@ fn test_single_vertex() { let extracted = reduction.extract_solution(&ilp_solution); assert_eq!(extracted, vec![0]); - assert_eq!(problem.evaluate(&extracted), SolutionSize::Valid(0)); + assert_eq!(problem.evaluate(&extracted), Min(Some(0))); } #[test] @@ -153,7 +153,7 @@ fn test_weighted() { // Should remove vertex 1 (cheapest) assert_eq!(extracted[1], 1, "Should remove vertex 1 (cheapest)"); - assert_eq!(problem.evaluate(&extracted), SolutionSize::Valid(1)); + assert_eq!(problem.evaluate(&extracted), Min(Some(1))); } #[test] @@ -164,7 +164,7 @@ fn test_two_disjoint_cycles() { let problem = MinimumFeedbackVertexSet::new(graph, vec![1i32; 4]); let bf = BruteForce::new(); - let bf_solutions = bf.find_all_best(&problem); + let bf_solutions = bf.find_all_witnesses(&problem); let bf_size = problem.evaluate(&bf_solutions[0]); let reduction: ReductionMFVSToILP = ReduceTo::>::reduce_to(&problem); @@ -174,8 +174,8 @@ fn test_two_disjoint_cycles() { let extracted = reduction.extract_solution(&ilp_solution); let ilp_size = problem.evaluate(&extracted); - assert_eq!(bf_size, SolutionSize::Valid(2)); - assert_eq!(ilp_size, SolutionSize::Valid(2)); + assert_eq!(bf_size, Min(Some(2))); + assert_eq!(ilp_size, Min(Some(2))); } #[test] diff --git a/src/unit_tests/rules/minimummultiwaycut_ilp.rs b/src/unit_tests/rules/minimummultiwaycut_ilp.rs index 8dfd32747..4ed31f8b7 100644 --- a/src/unit_tests/rules/minimummultiwaycut_ilp.rs +++ b/src/unit_tests/rules/minimummultiwaycut_ilp.rs @@ -3,7 +3,7 @@ use crate::models::algebraic::ObjectiveSense; use crate::solvers::{BruteForce, ILPSolver}; use crate::topology::SimpleGraph; use crate::traits::Problem; -use crate::types::SolutionSize; +use crate::types::Min; /// Build the canonical 5-vertex, 3-terminal example from issue #185. fn canonical_instance() -> MinimumMultiwayCut { @@ -37,7 +37,7 @@ fn test_minimummultiwaycut_to_ilp_closed_loop() { let ilp_solver = ILPSolver::new(); // Solve original with brute force - let bf_solutions = bf.find_all_best(&problem); + let bf_solutions = bf.find_all_witnesses(&problem); let bf_obj = problem.evaluate(&bf_solutions[0]); // Solve via ILP @@ -46,8 +46,8 @@ fn test_minimummultiwaycut_to_ilp_closed_loop() { let ilp_obj = problem.evaluate(&extracted); // Optimal cut cost is 8 - assert_eq!(bf_obj, SolutionSize::Valid(8)); - assert_eq!(ilp_obj, SolutionSize::Valid(8)); + assert_eq!(bf_obj, Min(Some(8))); + assert_eq!(ilp_obj, Min(Some(8))); } #[test] @@ -66,7 +66,7 @@ fn test_triangle_with_3_terminals() { let extracted = reduction.extract_solution(&ilp_solution); let obj = problem.evaluate(&extracted); - assert_eq!(obj, SolutionSize::Valid(6)); + assert_eq!(obj, Min(Some(6))); } #[test] @@ -84,7 +84,7 @@ fn test_two_terminals() { let extracted = reduction.extract_solution(&ilp_solution); let obj = problem.evaluate(&extracted); - assert_eq!(obj, SolutionSize::Valid(1)); + assert_eq!(obj, Min(Some(1))); } #[test] @@ -122,7 +122,7 @@ fn test_solution_extraction() { assert_eq!(extracted, vec![1, 0, 0, 1, 1, 0]); let obj = problem.evaluate(&extracted); - assert_eq!(obj, SolutionSize::Valid(8)); + assert_eq!(obj, Min(Some(8))); } #[test] @@ -135,5 +135,5 @@ fn test_solve_reduced() { .expect("solve_reduced should work"); assert!(problem.evaluate(&solution).is_valid()); - assert_eq!(problem.evaluate(&solution), SolutionSize::Valid(8)); + assert_eq!(problem.evaluate(&solution), Min(Some(8))); } diff --git a/src/unit_tests/rules/minimummultiwaycut_qubo.rs b/src/unit_tests/rules/minimummultiwaycut_qubo.rs index 5453b9fad..4b42b97c7 100644 --- a/src/unit_tests/rules/minimummultiwaycut_qubo.rs +++ b/src/unit_tests/rules/minimummultiwaycut_qubo.rs @@ -1,7 +1,7 @@ use super::*; use crate::solvers::BruteForce; use crate::traits::Problem; -use crate::types::SolutionSize; +use crate::types::Min; #[test] fn test_minimummultiwaycut_to_qubo_closed_loop() { @@ -13,7 +13,7 @@ fn test_minimummultiwaycut_to_qubo_closed_loop() { let qubo = reduction.target_problem(); let solver = BruteForce::new(); - let qubo_solutions = solver.find_all_best(qubo); + let qubo_solutions = solver.find_all_witnesses(qubo); assert!(!qubo_solutions.is_empty(), "QUBO solver found no solutions"); @@ -21,7 +21,7 @@ fn test_minimummultiwaycut_to_qubo_closed_loop() { for sol in &qubo_solutions { let extracted = reduction.extract_solution(sol); let metric = source.evaluate(&extracted); - assert_eq!(metric, SolutionSize::Valid(8)); + assert_eq!(metric, Min(Some(8))); } } @@ -35,7 +35,7 @@ fn test_minimummultiwaycut_to_qubo_small() { let qubo = reduction.target_problem(); let solver = BruteForce::new(); - let qubo_solutions = solver.find_all_best(qubo); + let qubo_solutions = solver.find_all_witnesses(qubo); assert!(!qubo_solutions.is_empty(), "QUBO solver found no solutions"); @@ -44,7 +44,7 @@ fn test_minimummultiwaycut_to_qubo_small() { let extracted = reduction.extract_solution(sol); let metric = source.evaluate(&extracted); // With 2 terminals and path 0-1-2, minimum cut is 1 (cut either edge) - assert_eq!(metric, SolutionSize::Valid(1)); + assert_eq!(metric, Min(Some(1))); } } @@ -70,7 +70,7 @@ fn test_minimummultiwaycut_to_qubo_terminal_pinning() { let qubo = reduction.target_problem(); let solver = BruteForce::new(); - let qubo_solutions = solver.find_all_best(qubo); + let qubo_solutions = solver.find_all_witnesses(qubo); let k = terminals.len(); for sol in &qubo_solutions { diff --git a/src/unit_tests/rules/minimumsetcovering_ilp.rs b/src/unit_tests/rules/minimumsetcovering_ilp.rs index 523b6a569..eda045703 100644 --- a/src/unit_tests/rules/minimumsetcovering_ilp.rs +++ b/src/unit_tests/rules/minimumsetcovering_ilp.rs @@ -1,7 +1,7 @@ use super::*; use crate::solvers::{BruteForce, ILPSolver}; use crate::traits::Problem; -use crate::types::SolutionSize; +use crate::types::Min; #[test] fn test_reduction_creates_valid_ilp() { @@ -52,7 +52,7 @@ fn test_minimumsetcovering_to_ilp_closed_loop() { let ilp_solver = ILPSolver::new(); // Solve with brute force on original problem - let bf_solutions = bf.find_all_best(&problem); + let bf_solutions = bf.find_all_witnesses(&problem); // Solve via ILP reduction let ilp_solution = ilp_solver.solve(ilp).expect("ILP should be solvable"); @@ -88,15 +88,15 @@ fn test_ilp_solution_equals_brute_force_weighted() { let bf = BruteForce::new(); let ilp_solver = ILPSolver::new(); - let bf_solutions = bf.find_all_best(&problem); + let bf_solutions = bf.find_all_witnesses(&problem); let bf_obj = problem.evaluate(&bf_solutions[0]); let ilp_solution = ilp_solver.solve(ilp).expect("ILP should be solvable"); let extracted = reduction.extract_solution(&ilp_solution); let ilp_obj = problem.evaluate(&extracted); - assert_eq!(bf_obj, SolutionSize::Valid(6)); - assert_eq!(ilp_obj, SolutionSize::Valid(6)); + assert_eq!(bf_obj, Min(Some(6))); + assert_eq!(ilp_obj, Min(Some(6))); // Verify the solution selects S1 and S2 assert_eq!(extracted, vec![0, 1, 1]); @@ -143,7 +143,7 @@ fn test_single_set_covers_all() { assert_eq!(extracted, vec![1, 0, 0, 0]); assert!(problem.evaluate(&extracted).is_valid()); - assert_eq!(problem.evaluate(&extracted), SolutionSize::Valid(1)); + assert_eq!(problem.evaluate(&extracted), Min(Some(1))); } #[test] @@ -162,7 +162,7 @@ fn test_overlapping_sets() { assert_eq!(extracted, vec![1, 1]); assert!(problem.evaluate(&extracted).is_valid()); - assert_eq!(problem.evaluate(&extracted), SolutionSize::Valid(2)); + assert_eq!(problem.evaluate(&extracted), Min(Some(2))); } #[test] @@ -188,7 +188,7 @@ fn test_solve_reduced() { .expect("solve_reduced should work"); assert!(problem.evaluate(&solution).is_valid()); - assert_eq!(problem.evaluate(&solution), SolutionSize::Valid(2)); + assert_eq!(problem.evaluate(&solution), Min(Some(2))); } #[test] diff --git a/src/unit_tests/rules/minimumvertexcover_ilp.rs b/src/unit_tests/rules/minimumvertexcover_ilp.rs index d58d372f5..57f47c507 100644 --- a/src/unit_tests/rules/minimumvertexcover_ilp.rs +++ b/src/unit_tests/rules/minimumvertexcover_ilp.rs @@ -4,7 +4,7 @@ use crate::rules::{MinimizeSteps, ReductionChain, ReductionGraph, ReductionPath} use crate::solvers::{BruteForce, ILPSolver}; use crate::topology::SimpleGraph; use crate::traits::Problem; -use crate::types::{ProblemSize, SolutionSize}; +use crate::types::{Min, ProblemSize}; fn reduce_vc_to_ilp( problem: &MinimumVertexCover, @@ -61,7 +61,7 @@ fn test_minimumvertexcover_to_ilp_via_path_closed_loop() { let bf = BruteForce::new(); let ilp_solver = ILPSolver::new(); - let bf_solutions = bf.find_all_best(&problem); + let bf_solutions = bf.find_all_witnesses(&problem); let ilp_solution = ilp_solver.solve(ilp).expect("ILP should be solvable"); let extracted = chain.extract_solution(&ilp_solution); @@ -83,6 +83,6 @@ fn test_minimumvertexcover_to_ilp_via_path_weighted() { let ilp_solution = ilp_solver.solve(ilp).expect("ILP should be solvable"); let extracted = chain.extract_solution(&ilp_solution); - assert_eq!(problem.evaluate(&extracted), SolutionSize::Valid(1)); + assert_eq!(problem.evaluate(&extracted), Min(Some(1))); assert_eq!(extracted, vec![0, 1, 0]); } diff --git a/src/unit_tests/rules/minimumvertexcover_maximumindependentset.rs b/src/unit_tests/rules/minimumvertexcover_maximumindependentset.rs index dd4533fd6..850680ec7 100644 --- a/src/unit_tests/rules/minimumvertexcover_maximumindependentset.rs +++ b/src/unit_tests/rules/minimumvertexcover_maximumindependentset.rs @@ -42,7 +42,7 @@ fn test_jl_parity_is_to_vertexcovering() { MaximumIndependentSet::new(SimpleGraph::new(nv, jl_parse_edges(inst)), vec![1i32; nv]); let result = ReduceTo::>::reduce_to(&source); let solver = BruteForce::new(); - let best_source: HashSet> = solver.find_all_best(&source).into_iter().collect(); + let best_source: HashSet> = solver.find_all_witnesses(&source).into_iter().collect(); assert_optimization_round_trip_from_optimization_target(&source, &result, "JL parity MIS->VC"); for case in data["cases"].as_array().unwrap() { assert_eq!(best_source, jl_parse_configs_set(&case["best_source"])); @@ -63,7 +63,7 @@ fn test_jl_parity_rule_is_to_vertexcovering() { MaximumIndependentSet::new(SimpleGraph::new(nv, jl_parse_edges(inst)), vec![1i32; nv]); let result = ReduceTo::>::reduce_to(&source); let solver = BruteForce::new(); - let best_source: HashSet> = solver.find_all_best(&source).into_iter().collect(); + let best_source: HashSet> = solver.find_all_witnesses(&source).into_iter().collect(); assert_optimization_round_trip_from_optimization_target( &source, &result, diff --git a/src/unit_tests/rules/minimumvertexcover_minimumfeedbackvertexset.rs b/src/unit_tests/rules/minimumvertexcover_minimumfeedbackvertexset.rs index 28816eea5..4ae5fd263 100644 --- a/src/unit_tests/rules/minimumvertexcover_minimumfeedbackvertexset.rs +++ b/src/unit_tests/rules/minimumvertexcover_minimumfeedbackvertexset.rs @@ -6,7 +6,7 @@ use crate::rules::test_helpers::assert_optimization_round_trip_from_optimization use crate::rules::traits::ReductionResult; use crate::rules::ReduceTo; #[cfg(feature = "example-db")] -use crate::solvers::{BruteForce, Solver}; +use crate::solvers::BruteForce; use crate::topology::{Graph, SimpleGraph}; #[cfg(feature = "example-db")] use crate::traits::Problem; @@ -119,10 +119,10 @@ fn test_canonical_rule_example_spec_builds() { ); let best_source = BruteForce::new() - .find_best(&source) + .find_witness(&source) .expect("source example should have an optimum"); let best_target = BruteForce::new() - .find_best(&target) + .find_witness(&target) .expect("target example should have an optimum"); assert_eq!(source_metric, source.evaluate(&best_source)); diff --git a/src/unit_tests/rules/minimumvertexcover_minimumsetcovering.rs b/src/unit_tests/rules/minimumvertexcover_minimumsetcovering.rs index d3387ac46..1214555e9 100644 --- a/src/unit_tests/rules/minimumvertexcover_minimumsetcovering.rs +++ b/src/unit_tests/rules/minimumvertexcover_minimumsetcovering.rs @@ -60,8 +60,8 @@ fn test_vc_to_sc_weighted() { // Solve both ways let solver = BruteForce::new(); - let vc_solutions = solver.find_all_best(&vc_problem); - let sc_solutions = solver.find_all_best(sc_problem); + let vc_solutions = solver.find_all_witnesses(&vc_problem); + let sc_solutions = solver.find_all_witnesses(sc_problem); // Both should select vertex 1 (weight 1) assert_eq!(vc_solutions[0], vec![0, 1, 0]); @@ -104,7 +104,7 @@ fn test_vc_to_sc_star_graph() { // Minimum cover should be just vertex 0 let solver = BruteForce::new(); - let solutions = solver.find_all_best(&vc_problem); + let solutions = solver.find_all_witnesses(&vc_problem); assert_eq!(solutions[0], vec![1, 0, 0, 0]); } @@ -124,7 +124,7 @@ fn test_jl_parity_vc_to_setcovering() { ); let result = ReduceTo::>::reduce_to(&source); let solver = BruteForce::new(); - let best_source: HashSet> = solver.find_all_best(&source).into_iter().collect(); + let best_source: HashSet> = solver.find_all_witnesses(&source).into_iter().collect(); assert_optimization_round_trip_from_optimization_target( &source, &result, @@ -151,7 +151,7 @@ fn test_jl_parity_rule_vc_to_setcovering() { ); let result = ReduceTo::>::reduce_to(&source); let solver = BruteForce::new(); - let best_source: HashSet> = solver.find_all_best(&source).into_iter().collect(); + let best_source: HashSet> = solver.find_all_witnesses(&source).into_iter().collect(); assert_optimization_round_trip_from_optimization_target( &source, &result, diff --git a/src/unit_tests/rules/minimumvertexcover_qubo.rs b/src/unit_tests/rules/minimumvertexcover_qubo.rs index 95bdff9d6..8b4dd1711 100644 --- a/src/unit_tests/rules/minimumvertexcover_qubo.rs +++ b/src/unit_tests/rules/minimumvertexcover_qubo.rs @@ -1,10 +1,10 @@ use crate::models::algebraic::QUBO; use crate::models::graph::MinimumVertexCover; use crate::rules::{Minimize, ReductionChain, ReductionGraph, ReductionPath}; -use crate::solvers::{BruteForce, Solver}; +use crate::solvers::BruteForce; use crate::topology::{Graph, SimpleGraph}; use crate::traits::Problem; -use crate::types::{ProblemSize, SolutionSize}; +use crate::types::{Min, ProblemSize}; fn reduce_vc_to_qubo( problem: &MinimumVertexCover, @@ -56,7 +56,7 @@ fn test_minimumvertexcover_to_qubo_via_path_closed_loop() { assert_eq!(qubo.num_variables(), 4); let solver = BruteForce::new(); - let qubo_solutions = solver.find_all_best(qubo); + let qubo_solutions = solver.find_all_witnesses(qubo); for sol in &qubo_solutions { let extracted = chain.extract_solution(sol); assert!(problem.evaluate(&extracted).is_valid()); @@ -73,11 +73,11 @@ fn test_minimumvertexcover_to_qubo_via_path_weighted() { let solver = BruteForce::new(); let qubo_solution = solver - .find_best(qubo) + .find_witness(qubo) .expect("QUBO should be solvable via path"); let extracted = chain.extract_solution(&qubo_solution); - assert_eq!(problem.evaluate(&extracted), SolutionSize::Valid(1)); + assert_eq!(problem.evaluate(&extracted), Min(Some(1))); assert_eq!(extracted, vec![0, 1, 0]); } @@ -93,9 +93,9 @@ fn test_minimumvertexcover_to_qubo_via_path_star_graph() { assert_eq!(qubo.num_variables(), 4); let solver = BruteForce::new(); - let qubo_solution = solver.find_best(qubo).expect("QUBO should be solvable"); + let qubo_solution = solver.find_witness(qubo).expect("QUBO should be solvable"); let extracted = chain.extract_solution(&qubo_solution); - assert_eq!(problem.evaluate(&extracted), SolutionSize::Valid(1)); + assert_eq!(problem.evaluate(&extracted), Min(Some(1))); assert_eq!(extracted.iter().filter(|&&x| x == 1).count(), 1); } diff --git a/src/unit_tests/rules/partition_knapsack.rs b/src/unit_tests/rules/partition_knapsack.rs index 127247788..e308d172c 100644 --- a/src/unit_tests/rules/partition_knapsack.rs +++ b/src/unit_tests/rules/partition_knapsack.rs @@ -1,9 +1,9 @@ use super::*; use crate::models::misc::Partition; use crate::rules::test_helpers::assert_satisfaction_round_trip_from_optimization_target; -use crate::solvers::{BruteForce, Solver}; +use crate::solvers::BruteForce; use crate::traits::Problem; -use crate::types::SolutionSize; +use crate::types::Max; #[test] fn test_partition_to_knapsack_closed_loop() { @@ -35,10 +35,10 @@ fn test_partition_to_knapsack_odd_total_is_not_satisfying() { let reduction = ReduceTo::::reduce_to(&source); let target = reduction.target_problem(); let best = BruteForce::new() - .find_best(target) + .find_witness(target) .expect("Knapsack target should always have an optimal solution"); - assert_eq!(target.evaluate(&best), SolutionSize::Valid(5)); + assert_eq!(target.evaluate(&best), Max(Some(5))); let extracted = reduction.extract_solution(&best); assert!(!source.evaluate(&extracted)); diff --git a/src/unit_tests/rules/qubo_ilp.rs b/src/unit_tests/rules/qubo_ilp.rs index e95c8f568..924a25723 100644 --- a/src/unit_tests/rules/qubo_ilp.rs +++ b/src/unit_tests/rules/qubo_ilp.rs @@ -30,7 +30,7 @@ fn test_qubo_to_ilp_diagonal_only() { assert!(ilp.constraints.is_empty()); let solver = BruteForce::new(); - let best = solver.find_all_best(ilp); + let best = solver.find_all_witnesses(ilp); let extracted = reduction.extract_solution(&best[0]); assert_eq!(extracted, vec![0, 1]); } @@ -53,7 +53,7 @@ fn test_qubo_to_ilp_3var() { assert_eq!(ilp.constraints.len(), 6); let solver = BruteForce::new(); - let best = solver.find_all_best(ilp); + let best = solver.find_all_witnesses(ilp); let extracted = reduction.extract_solution(&best[0]); assert_eq!(extracted, vec![1, 0, 1]); } diff --git a/src/unit_tests/rules/reduction_path_parity.rs b/src/unit_tests/rules/reduction_path_parity.rs index 9388fa5c9..f86c1ec1d 100644 --- a/src/unit_tests/rules/reduction_path_parity.rs +++ b/src/unit_tests/rules/reduction_path_parity.rs @@ -7,7 +7,7 @@ use crate::models::graph::{MaxCut, SpinGlass}; use crate::models::misc::Factoring; use crate::rules::test_helpers::assert_optimization_round_trip_chain; use crate::rules::{MinimizeSteps, ReductionGraph}; -use crate::solvers::{BruteForce, Solver}; +use crate::solvers::BruteForce; use crate::topology::SimpleGraph; use crate::traits::Problem; use crate::types::ProblemSize; @@ -58,7 +58,7 @@ fn test_jl_parity_maxcut_to_spinglass_path() { assert_eq!(SpinGlass::::NAME, "SpinGlass"); let solver = BruteForce::new(); - let target_solution = solver.find_best(target).unwrap(); + let target_solution = solver.find_witness(target).unwrap(); let source_solution = chain.extract_solution(&target_solution); // Source solution should be valid @@ -115,7 +115,7 @@ fn test_jl_parity_maxcut_to_qubo_path() { /// Julia: factoring = Factoring(2, 1, 3) /// Julia: paths = reduction_paths(Factoring, SpinGlass) -/// Julia: all(solution_size.(Ref(factoring), extract_solution.(Ref(res), sol)) .== Ref(SolutionSize(0, true))) +/// Julia: all(solution_size.(Ref(factoring), extract_solution.(Ref(res), sol)) .== Ref(valid objective 0)) #[cfg(feature = "ilp-solver")] #[test] fn test_jl_parity_factoring_to_spinglass_path() { diff --git a/src/unit_tests/rules/sat_circuitsat.rs b/src/unit_tests/rules/sat_circuitsat.rs index 85a403ea9..6fb021b05 100644 --- a/src/unit_tests/rules/sat_circuitsat.rs +++ b/src/unit_tests/rules/sat_circuitsat.rs @@ -30,7 +30,7 @@ fn test_sat_to_circuitsat_unsatisfiable() { let sat = Satisfiability::new(1, vec![CNFClause::new(vec![1]), CNFClause::new(vec![-1])]); let result = ReduceTo::::reduce_to(&sat); let solver = BruteForce::new(); - let best_target = solver.find_all_satisfying(result.target_problem()); + let best_target = solver.find_all_witnesses(result.target_problem()); assert!( best_target.is_empty(), "Unsatisfiable SAT -> CircuitSAT should have no solutions" diff --git a/src/unit_tests/rules/sat_coloring.rs b/src/unit_tests/rules/sat_coloring.rs index 0c058e24a..a5cba5f87 100644 --- a/src/unit_tests/rules/sat_coloring.rs +++ b/src/unit_tests/rules/sat_coloring.rs @@ -75,7 +75,7 @@ fn test_unsatisfiable_formula() { // Solve the coloring problem - use find_all_satisfying since KColoring is a satisfaction problem let solver = BruteForce::new(); - let solutions = solver.find_all_satisfying(coloring); + let solutions = solver.find_all_witnesses(coloring); // For an unsatisfiable formula, the coloring should have no valid solutions // OR no valid coloring exists that extracts to a satisfying SAT assignment @@ -190,7 +190,7 @@ fn test_single_literal_clauses() { let coloring = reduction.target_problem(); let solver = BruteForce::new(); - let solutions = solver.find_all_satisfying(coloring); + let solutions = solver.find_all_witnesses(coloring); let mut found_correct = false; for sol in &solutions { @@ -325,7 +325,7 @@ fn test_jl_parity_sat_to_coloring() { .expect("ILP should find a coloring"); let extracted = result.extract_solution(&target_sol); let best_source: HashSet> = BruteForce::new() - .find_all_satisfying(&source) + .find_all_witnesses(&source) .into_iter() .collect(); assert!( diff --git a/src/unit_tests/rules/sat_ksat.rs b/src/unit_tests/rules/sat_ksat.rs index 0748038f3..cca0c1c9b 100644 --- a/src/unit_tests/rules/sat_ksat.rs +++ b/src/unit_tests/rules/sat_ksat.rs @@ -120,8 +120,8 @@ fn test_sat_to_ksat_closed_loop() { // Solve both problems - use find_all_satisfying for satisfaction problems let solver = BruteForce::new(); - let sat_solutions = solver.find_all_satisfying(&sat); - let ksat_solutions = solver.find_all_satisfying(ksat); + let sat_solutions = solver.find_all_witnesses(&sat); + let ksat_solutions = solver.find_all_witnesses(ksat); // If SAT is satisfiable, K-SAT should be too let sat_satisfiable = !sat_solutions.is_empty(); @@ -148,7 +148,7 @@ fn test_sat_to_3sat_solution_extraction() { // Solve K-SAT - use find_all_satisfying for satisfaction problems let solver = BruteForce::new(); - let ksat_solutions = solver.find_all_satisfying(ksat); + let ksat_solutions = solver.find_all_witnesses(ksat); // Extract and verify solutions for ksat_sol in &ksat_solutions { @@ -211,9 +211,9 @@ fn test_roundtrip_sat_3sat_sat() { // Solve all three - use find_all_satisfying for satisfaction problems let solver = BruteForce::new(); - let orig_solutions = solver.find_all_satisfying(&original_sat); - let ksat_solutions = solver.find_all_satisfying(ksat); - let final_solutions = solver.find_all_satisfying(final_sat); + let orig_solutions = solver.find_all_witnesses(&original_sat); + let ksat_solutions = solver.find_all_witnesses(ksat); + let final_solutions = solver.find_all_witnesses(final_sat); // All should be satisfiable (have at least one solution) assert!(!orig_solutions.is_empty()); @@ -303,8 +303,8 @@ fn test_unsatisfiable_formula() { let ksat = reduction.target_problem(); let solver = BruteForce::new(); - let best_target = solver.find_all_satisfying(ksat); - let best_source: HashSet> = solver.find_all_satisfying(&sat).into_iter().collect(); + let best_target = solver.find_all_witnesses(ksat); + let best_source: HashSet> = solver.find_all_witnesses(&sat).into_iter().collect(); // Both should be empty (unsatisfiable) assert!(best_source.is_empty()); @@ -324,8 +324,7 @@ fn test_jl_parity_sat_to_ksat() { let source = Satisfiability::new(num_vars, clauses); let result = ReduceTo::>::reduce_to(&source); let solver = BruteForce::new(); - let best_source: HashSet> = - solver.find_all_satisfying(&source).into_iter().collect(); + let best_source: HashSet> = solver.find_all_witnesses(&source).into_iter().collect(); assert_satisfaction_round_trip_from_satisfaction_target( &source, &result, @@ -349,8 +348,7 @@ fn test_jl_parity_ksat_to_sat() { let source = KSatisfiability::::new(num_vars, clauses); let result = ReduceTo::::reduce_to(&source); let solver = BruteForce::new(); - let best_source: HashSet> = - solver.find_all_satisfying(&source).into_iter().collect(); + let best_source: HashSet> = solver.find_all_witnesses(&source).into_iter().collect(); assert_satisfaction_round_trip_from_satisfaction_target( &source, &result, @@ -374,8 +372,7 @@ fn test_jl_parity_rule_sat_to_ksat() { let source = Satisfiability::new(num_vars, clauses); let result = ReduceTo::>::reduce_to(&source); let solver = BruteForce::new(); - let best_source: HashSet> = - solver.find_all_satisfying(&source).into_iter().collect(); + let best_source: HashSet> = solver.find_all_witnesses(&source).into_iter().collect(); assert_satisfaction_round_trip_from_satisfaction_target( &source, &result, diff --git a/src/unit_tests/rules/sat_maximumindependentset.rs b/src/unit_tests/rules/sat_maximumindependentset.rs index bc4ba6af8..9c2bd8f12 100644 --- a/src/unit_tests/rules/sat_maximumindependentset.rs +++ b/src/unit_tests/rules/sat_maximumindependentset.rs @@ -72,7 +72,7 @@ fn test_two_clause_sat_to_is() { // Maximum IS should have size 1 (can't select both) let solver = BruteForce::new(); - let solutions = solver.find_all_best(is_problem); + let solutions = solver.find_all_witnesses(is_problem); for sol in &solutions { assert_eq!(sol.iter().sum::(), 1); } @@ -212,7 +212,7 @@ fn test_jl_parity_sat_to_independentset() { let result = ReduceTo::>::reduce_to(&source); let solver = BruteForce::new(); let sat_solutions: HashSet> = - solver.find_all_satisfying(&source).into_iter().collect(); + solver.find_all_witnesses(&source).into_iter().collect(); for case in data["cases"].as_array().unwrap() { if sat_solutions.is_empty() { let target_solution = solve_optimization_problem(result.target_problem()) diff --git a/src/unit_tests/rules/sat_minimumdominatingset.rs b/src/unit_tests/rules/sat_minimumdominatingset.rs index 50902fda3..a421375b0 100644 --- a/src/unit_tests/rules/sat_minimumdominatingset.rs +++ b/src/unit_tests/rules/sat_minimumdominatingset.rs @@ -200,7 +200,7 @@ fn test_jl_parity_sat_to_dominatingset() { let result = ReduceTo::>::reduce_to(&source); let solver = BruteForce::new(); let sat_solutions: HashSet> = - solver.find_all_satisfying(&source).into_iter().collect(); + solver.find_all_witnesses(&source).into_iter().collect(); for case in data["cases"].as_array().unwrap() { if sat_solutions.is_empty() { let target_solution = solve_optimization_problem(result.target_problem()) diff --git a/src/unit_tests/rules/sequencingtominimizeweightedcompletiontime_ilp.rs b/src/unit_tests/rules/sequencingtominimizeweightedcompletiontime_ilp.rs index f62686b51..3f39efa50 100644 --- a/src/unit_tests/rules/sequencingtominimizeweightedcompletiontime_ilp.rs +++ b/src/unit_tests/rules/sequencingtominimizeweightedcompletiontime_ilp.rs @@ -1,9 +1,9 @@ use super::*; use crate::models::algebraic::{ObjectiveSense, ILP}; use crate::models::misc::SequencingToMinimizeWeightedCompletionTime; -use crate::solvers::{BruteForce, ILPSolver, Solver}; +use crate::solvers::{BruteForce, ILPSolver}; use crate::traits::Problem; -use crate::types::SolutionSize; +use crate::types::Min; #[test] fn test_reduction_creates_expected_ilp_shape() { @@ -44,7 +44,7 @@ fn test_extract_solution_encodes_schedule_as_lehmer_code() { // y_{0,1} = 0 means task 1 before task 0. let extracted = reduction.extract_solution(&[3, 1, 0]); assert_eq!(extracted, vec![1, 0]); - assert_eq!(problem.evaluate(&extracted), SolutionSize::Valid(14)); + assert_eq!(problem.evaluate(&extracted), Min(Some(14))); } #[test] @@ -61,7 +61,7 @@ fn test_issue_example_closed_loop() { let extracted = reduction.extract_solution(&ilp_solution); assert_eq!(extracted, vec![1, 2, 0, 1, 0]); - assert_eq!(problem.evaluate(&extracted), SolutionSize::Valid(46)); + assert_eq!(problem.evaluate(&extracted), Min(Some(46))); } #[test] @@ -74,7 +74,7 @@ fn test_ilp_matches_bruteforce_optimum() { let brute_force = BruteForce::new(); let brute_force_solution = brute_force - .find_best(&problem) + .find_witness(&problem) .expect("brute force should find a schedule"); let brute_force_metric = problem.evaluate(&brute_force_solution); @@ -155,5 +155,5 @@ fn test_solve_reduced_matches_source_optimum() { let source_solution = reduction.extract_solution(&ilp_solution); assert_eq!(source_solution, vec![1, 2, 0, 1, 0]); - assert_eq!(problem.evaluate(&source_solution), SolutionSize::Valid(46)); + assert_eq!(problem.evaluate(&source_solution), Min(Some(46))); } diff --git a/src/unit_tests/rules/spinglass_maxcut.rs b/src/unit_tests/rules/spinglass_maxcut.rs index 013bc4777..b6dcac86d 100644 --- a/src/unit_tests/rules/spinglass_maxcut.rs +++ b/src/unit_tests/rules/spinglass_maxcut.rs @@ -97,7 +97,7 @@ fn test_jl_parity_spinglass_to_maxcut() { let source = SpinGlass::::new(nv, interactions, h_values); let result = ReduceTo::>::reduce_to(&source); let solver = BruteForce::new(); - let best_source: HashSet> = solver.find_all_best(&source).into_iter().collect(); + let best_source: HashSet> = solver.find_all_witnesses(&source).into_iter().collect(); assert_optimization_round_trip_from_optimization_target( &source, &result, @@ -124,7 +124,7 @@ fn test_jl_parity_maxcut_to_spinglass() { let source = MaxCut::new(SimpleGraph::new(nv, edges), weights); let result = ReduceTo::>::reduce_to(&source); let solver = BruteForce::new(); - let best_source: HashSet> = solver.find_all_best(&source).into_iter().collect(); + let best_source: HashSet> = solver.find_all_witnesses(&source).into_iter().collect(); assert_optimization_round_trip_from_optimization_target( &source, &result, @@ -153,7 +153,7 @@ fn test_jl_parity_rule_maxcut_to_spinglass() { ); let result = ReduceTo::>::reduce_to(&source); let solver = BruteForce::new(); - let best_source: HashSet> = solver.find_all_best(&source).into_iter().collect(); + let best_source: HashSet> = solver.find_all_witnesses(&source).into_iter().collect(); assert_optimization_round_trip_from_optimization_target( &source, &result, @@ -181,7 +181,7 @@ fn test_jl_parity_rule_spinglass_to_maxcut() { let source = SpinGlass::::new(nv, interactions, h_values); let result = ReduceTo::>::reduce_to(&source); let solver = BruteForce::new(); - let best_source: HashSet> = solver.find_all_best(&source).into_iter().collect(); + let best_source: HashSet> = solver.find_all_witnesses(&source).into_iter().collect(); assert_optimization_round_trip_from_optimization_target( &source, &result, diff --git a/src/unit_tests/rules/spinglass_qubo.rs b/src/unit_tests/rules/spinglass_qubo.rs index 633cb95b5..dc3494c02 100644 --- a/src/unit_tests/rules/spinglass_qubo.rs +++ b/src/unit_tests/rules/spinglass_qubo.rs @@ -12,7 +12,7 @@ fn test_spinglass_to_qubo_closed_loop() { let qubo = reduction.target_problem(); let solver = BruteForce::new(); - let solutions = solver.find_all_best(qubo); + let solutions = solver.find_all_witnesses(qubo); // Anti-ferromagnetic: opposite spins are optimal for sol in &solutions { @@ -33,7 +33,7 @@ fn test_with_onsite_fields() { let qubo = reduction.target_problem(); let solver = BruteForce::new(); - let solutions = solver.find_all_best(qubo); + let solutions = solver.find_all_witnesses(qubo); assert_eq!(solutions.len(), 1); assert_eq!(solutions[0], vec![0], "Should prefer x=0 (s=-1)"); @@ -84,7 +84,7 @@ fn test_jl_parity_spinglass_to_qubo() { let source = SpinGlass::::new(nv, interactions, h_values); let result = ReduceTo::>::reduce_to(&source); let solver = BruteForce::new(); - let best_source: HashSet> = solver.find_all_best(&source).into_iter().collect(); + let best_source: HashSet> = solver.find_all_witnesses(&source).into_iter().collect(); assert_optimization_round_trip_from_optimization_target( &source, &result, @@ -126,7 +126,7 @@ fn test_jl_parity_qubo_to_spinglass() { let source = QUBO::from_matrix(rust_matrix); let result = ReduceTo::>::reduce_to(&source); let solver = BruteForce::new(); - let best_source: HashSet> = solver.find_all_best(&source).into_iter().collect(); + let best_source: HashSet> = solver.find_all_witnesses(&source).into_iter().collect(); assert_optimization_round_trip_from_optimization_target( &source, &result, @@ -169,7 +169,7 @@ fn test_jl_parity_rule_qubo_to_spinglass() { let source = QUBO::from_matrix(rust_matrix); let result = ReduceTo::>::reduce_to(&source); let solver = BruteForce::new(); - let best_source: HashSet> = solver.find_all_best(&source).into_iter().collect(); + let best_source: HashSet> = solver.find_all_witnesses(&source).into_iter().collect(); assert_optimization_round_trip_from_optimization_target( &source, &result, diff --git a/src/unit_tests/rules/steinertree_ilp.rs b/src/unit_tests/rules/steinertree_ilp.rs index 25420eaa9..24a7436a0 100644 --- a/src/unit_tests/rules/steinertree_ilp.rs +++ b/src/unit_tests/rules/steinertree_ilp.rs @@ -5,7 +5,7 @@ use crate::rules::ReduceTo; use crate::solvers::{BruteForce, ILPSolver}; use crate::topology::SimpleGraph; use crate::traits::Problem; -use crate::types::SolutionSize; +use crate::types::Min; fn canonical_instance() -> SteinerTree { let graph = SimpleGraph::new( @@ -46,12 +46,12 @@ fn test_steinertree_to_ilp_closed_loop() { let bf = BruteForce::new(); let ilp_solver = ILPSolver::new(); - let best_source = bf.find_all_best(&problem); + let best_source = bf.find_all_witnesses(&problem); let ilp_solution = ilp_solver.solve(ilp).expect("ILP should be solvable"); let extracted = reduction.extract_solution(&ilp_solution); - assert_eq!(problem.evaluate(&best_source[0]), SolutionSize::Valid(6)); - assert_eq!(problem.evaluate(&extracted), SolutionSize::Valid(6)); + assert_eq!(problem.evaluate(&best_source[0]), Min(Some(6))); + assert_eq!(problem.evaluate(&extracted), Min(Some(6))); assert!(problem.is_valid_solution(&extracted)); } @@ -77,7 +77,7 @@ fn test_solve_reduced_uses_new_rule() { let solution = ILPSolver::new() .solve_reduced(&problem) .expect("solve_reduced should find the Steiner tree via ILP"); - assert_eq!(problem.evaluate(&solution), SolutionSize::Valid(6)); + assert_eq!(problem.evaluate(&solution), Min(Some(6))); } #[test] diff --git a/src/unit_tests/rules/subsetsum_closestvectorproblem.rs b/src/unit_tests/rules/subsetsum_closestvectorproblem.rs index 8eb81def0..818038c3a 100644 --- a/src/unit_tests/rules/subsetsum_closestvectorproblem.rs +++ b/src/unit_tests/rules/subsetsum_closestvectorproblem.rs @@ -1,9 +1,9 @@ use super::*; use crate::models::algebraic::{ClosestVectorProblem, VarBounds}; use crate::rules::test_helpers::assert_satisfaction_round_trip_from_optimization_target; -use crate::solvers::{BruteForce, Solver}; +use crate::solvers::BruteForce; use crate::traits::Problem; -use crate::types::SolutionSize; +use crate::types::Min; use std::collections::HashSet; #[test] @@ -42,7 +42,7 @@ fn test_subsetsum_to_closestvectorproblem_issue_example_minimizers() { let reduction = ReduceTo::>::reduce_to(&source); let target = reduction.target_problem(); let solutions: HashSet> = BruteForce::new() - .find_all_best(target) + .find_all_witnesses(target) .into_iter() .collect(); @@ -50,7 +50,7 @@ fn test_subsetsum_to_closestvectorproblem_issue_example_minimizers() { assert_eq!(solutions, expected); for solution in &solutions { - assert_eq!(target.evaluate(solution), SolutionSize::Valid(1.0)); + assert_eq!(target.evaluate(solution), Min(Some(1.0))); } } @@ -60,13 +60,12 @@ fn test_subsetsum_to_closestvectorproblem_unsatisfiable_instance() { let reduction = ReduceTo::>::reduce_to(&source); let target = reduction.target_problem(); let best = BruteForce::new() - .find_best(target) + .find_witness(target) .expect("unsatisfiable instance should still have a best CVP assignment"); - match target.evaluate(&best) { - SolutionSize::Valid(value) => assert!(value > (source.num_elements() as f64).sqrt() / 2.0), - SolutionSize::Invalid => panic!("CVP solution should be valid"), - } + let metric = target.evaluate(&best); + assert!(metric.is_valid(), "CVP solution should be valid"); + assert!(metric.unwrap() > (source.num_elements() as f64).sqrt() / 2.0); } #[test] diff --git a/src/unit_tests/rules/travelingsalesman_ilp.rs b/src/unit_tests/rules/travelingsalesman_ilp.rs index acf626957..c81a477f4 100644 --- a/src/unit_tests/rules/travelingsalesman_ilp.rs +++ b/src/unit_tests/rules/travelingsalesman_ilp.rs @@ -2,7 +2,7 @@ use super::*; use crate::solvers::{BruteForce, ILPSolver}; use crate::topology::SimpleGraph; use crate::traits::Problem; -use crate::types::SolutionSize; +use crate::types::Min; fn k4_tsp() -> TravelingSalesman { TravelingSalesman::new( @@ -43,7 +43,7 @@ fn test_reduction_c4_closed_loop() { // Verify extracted solution is valid on source problem let metric = problem.evaluate(&extracted); assert!(metric.is_valid(), "Extracted solution must be valid"); - assert_eq!(metric, SolutionSize::Valid(4)); + assert_eq!(metric, Min(Some(4))); } #[test] @@ -60,7 +60,7 @@ fn test_reduction_k4_weighted_closed_loop() { // Solve via brute force for cross-check let bf = BruteForce::new(); - let bf_solutions = bf.find_all_best(&problem); + let bf_solutions = bf.find_all_witnesses(&problem); let bf_metric = problem.evaluate(&bf_solutions[0]); let ilp_metric = problem.evaluate(&extracted); @@ -87,7 +87,7 @@ fn test_reduction_c5_unweighted_closed_loop() { let metric = problem.evaluate(&extracted); assert!(metric.is_valid()); - assert_eq!(metric, SolutionSize::Valid(5)); + assert_eq!(metric, Min(Some(5))); } #[test] @@ -144,6 +144,6 @@ fn test_solve_reduced() { // Cross-check with brute force let bf = BruteForce::new(); - let bf_solutions = bf.find_all_best(&problem); + let bf_solutions = bf.find_all_witnesses(&problem); assert_eq!(metric, problem.evaluate(&bf_solutions[0])); } diff --git a/src/unit_tests/rules/travelingsalesman_qubo.rs b/src/unit_tests/rules/travelingsalesman_qubo.rs index 7976d68c6..1095a6937 100644 --- a/src/unit_tests/rules/travelingsalesman_qubo.rs +++ b/src/unit_tests/rules/travelingsalesman_qubo.rs @@ -1,7 +1,7 @@ use super::*; use crate::solvers::BruteForce; use crate::traits::Problem; -use crate::types::SolutionSize; +use crate::types::Min; #[test] fn test_travelingsalesman_to_qubo_closed_loop() { @@ -12,7 +12,7 @@ fn test_travelingsalesman_to_qubo_closed_loop() { let qubo = reduction.target_problem(); let solver = BruteForce::new(); - let qubo_solutions = solver.find_all_best(qubo); + let qubo_solutions = solver.find_all_witnesses(qubo); // All QUBO solutions should extract to valid TSP solutions for sol in &qubo_solutions { @@ -20,7 +20,7 @@ fn test_travelingsalesman_to_qubo_closed_loop() { let metric = tsp.evaluate(&extracted); assert!(metric.is_valid(), "Extracted solution should be valid"); // K3 has only one Hamiltonian cycle (all 3 edges), cost = 1+2+3 = 6 - assert_eq!(metric, SolutionSize::Valid(6)); + assert_eq!(metric, Min(Some(6))); } // There are multiple QUBO optima (different position assignments for the same tour), @@ -40,14 +40,14 @@ fn test_travelingsalesman_to_qubo_k4() { let qubo = reduction.target_problem(); let solver = BruteForce::new(); - let qubo_solutions = solver.find_all_best(qubo); + let qubo_solutions = solver.find_all_witnesses(qubo); // Every Hamiltonian cycle in K4 uses exactly 4 edges, so cost = 4 for sol in &qubo_solutions { let extracted = reduction.extract_solution(sol); let metric = tsp.evaluate(&extracted); assert!(metric.is_valid(), "Extracted solution should be valid"); - assert_eq!(metric, SolutionSize::Valid(4)); + assert_eq!(metric, Min(Some(4))); } // K4 has 3 distinct Hamiltonian cycles, but each has multiple position encodings diff --git a/src/unit_tests/solvers/brute_force.rs b/src/unit_tests/solvers/brute_force.rs index 1ec05a591..11800481b 100644 --- a/src/unit_tests/solvers/brute_force.rs +++ b/src/unit_tests/solvers/brute_force.rs @@ -96,13 +96,11 @@ impl Problem for SumProblem { } fn evaluate(&self, config: &[usize]) -> Self::Value { - Sum( - config - .iter() - .zip(&self.weights) - .map(|(&c, &w)| if c == 1 { w } else { 0 }) - .sum(), - ) + Sum(config + .iter() + .zip(&self.weights) + .map(|(&c, &w)| if c == 1 { w } else { 0 }) + .sum()) } fn variant() -> Vec<(&'static str, &'static str)> { @@ -201,8 +199,8 @@ fn test_solver_find_all_witnesses_returns_empty_for_sum_problem() { #[test] fn test_solver_with_real_mis() { use crate::models::graph::MaximumIndependentSet; - use crate::traits::Problem; use crate::topology::SimpleGraph; + use crate::traits::Problem; let problem = MaximumIndependentSet::new( SimpleGraph::new(3, vec![(0, 1), (1, 2), (0, 2)]), @@ -210,7 +208,7 @@ fn test_solver_with_real_mis() { ); let solver = BruteForce::new(); - let best = solver.find_all_best(&problem); + let best = solver.find_all_witnesses(&problem); assert_eq!(best.len(), 3); for sol in &best { assert_eq!(sol.iter().sum::(), 1); @@ -229,7 +227,7 @@ fn test_solver_with_real_sat() { ); let solver = BruteForce::new(); - let solutions = solver.find_all_satisfying(&problem); + let solutions = solver.find_all_witnesses(&problem); assert_eq!(solutions.len(), 2); for sol in &solutions { assert!(problem.evaluate(sol)); diff --git a/src/unit_tests/solvers/ilp/solver.rs b/src/unit_tests/solvers/ilp/solver.rs index f4da81fa8..7d395969e 100644 --- a/src/unit_tests/solvers/ilp/solver.rs +++ b/src/unit_tests/solvers/ilp/solver.rs @@ -69,7 +69,7 @@ fn test_ilp_solver_matches_brute_force() { let bf = BruteForce::new(); let ilp_solver = ILPSolver::new(); - let bf_solutions = bf.find_all_best(&ilp); + let bf_solutions = bf.find_all_witnesses(&ilp); let ilp_solution = ilp_solver.solve(&ilp).unwrap(); // Both should find optimal value (2) @@ -207,7 +207,7 @@ fn test_ilp_multiple_constraints() { // Check against brute force let bf = BruteForce::new(); - let bf_solutions = bf.find_all_best(&ilp); + let bf_solutions = bf.find_all_witnesses(&ilp); let bf_size = ilp.evaluate(&bf_solutions[0]).unwrap(); assert!( diff --git a/src/unit_tests/trait_consistency.rs b/src/unit_tests/trait_consistency.rs index f45862e05..70beb0d12 100644 --- a/src/unit_tests/trait_consistency.rs +++ b/src/unit_tests/trait_consistency.rs @@ -240,76 +240,76 @@ fn test_all_problems_implement_trait_correctly() { #[test] fn test_direction() { - use crate::traits::OptimizationProblem; - use crate::types::Direction; + use crate::traits::ObjectiveProblem; + use crate::types::ExtremumSense; // Minimization problems assert_eq!( MinimumVertexCover::new(SimpleGraph::new(2, vec![(0, 1)]), vec![1i32; 2]).direction(), - Direction::Minimize + ExtremumSense::Minimize ); assert_eq!( MinimumDominatingSet::new(SimpleGraph::new(2, vec![(0, 1)]), vec![1i32; 2]).direction(), - Direction::Minimize + ExtremumSense::Minimize ); assert_eq!( MinimumSetCovering::::new(2, vec![vec![0, 1]]).direction(), - Direction::Minimize + ExtremumSense::Minimize ); assert_eq!( PaintShop::new(vec!["a", "a"]).direction(), - Direction::Minimize + ExtremumSense::Minimize ); assert_eq!( QUBO::from_matrix(vec![vec![1.0]]).direction(), - Direction::Minimize + ExtremumSense::Minimize ); assert_eq!( SpinGlass::new(1, vec![], vec![0.0]).direction(), - Direction::Minimize + ExtremumSense::Minimize ); assert_eq!( BMF::new(vec![vec![true]], 1).direction(), - Direction::Minimize + ExtremumSense::Minimize ); - assert_eq!(Factoring::new(6, 2, 2).direction(), Direction::Minimize); + assert_eq!(Factoring::new(6, 2, 2).direction(), ExtremumSense::Minimize); assert_eq!( BicliqueCover::new(BipartiteGraph::new(2, 2, vec![(0, 0)]), 1).direction(), - Direction::Minimize + ExtremumSense::Minimize ); assert_eq!( QuadraticAssignment::new(vec![vec![0, 1], vec![1, 0]], vec![vec![0, 1], vec![1, 0]]) .direction(), - Direction::Minimize + ExtremumSense::Minimize ); // Maximization problems assert_eq!( MaximumIndependentSet::new(SimpleGraph::new(2, vec![(0, 1)]), vec![1i32; 2]).direction(), - Direction::Maximize + ExtremumSense::Maximize ); assert_eq!( MaximalIS::new(SimpleGraph::new(2, vec![(0, 1)]), vec![1i32; 2]).direction(), - Direction::Maximize + ExtremumSense::Maximize ); assert_eq!( MaxCut::new(SimpleGraph::new(2, vec![(0, 1)]), vec![1i32]).direction(), - Direction::Maximize + ExtremumSense::Maximize ); assert_eq!( MaximumMatching::new(SimpleGraph::new(2, vec![(0, 1)]), vec![1i32]).direction(), - Direction::Maximize + ExtremumSense::Maximize ); assert_eq!( MaximumSetPacking::::new(vec![vec![0]]).direction(), - Direction::Maximize + ExtremumSense::Maximize ); assert_eq!( MaximumClique::new(SimpleGraph::new(2, vec![(0, 1)]), vec![1i32; 2]).direction(), - Direction::Maximize + ExtremumSense::Maximize ); assert_eq!( PartiallyOrderedKnapsack::new(vec![2, 3], vec![3, 2], vec![(0, 1)], 5).direction(), - Direction::Maximize + ExtremumSense::Maximize ); } diff --git a/src/unit_tests/types.rs b/src/unit_tests/types.rs index a010cf886..47524d7fa 100644 --- a/src/unit_tests/types.rs +++ b/src/unit_tests/types.rs @@ -73,41 +73,57 @@ fn test_or_witness_hooks() { } #[test] -fn test_solution_size_valid() { - let size: SolutionSize = SolutionSize::Valid(42); +fn test_max_helpers() { + let size = Max(Some(42)); assert!(size.is_valid()); assert_eq!(size.size(), Some(&42)); + assert_eq!(size.unwrap(), 42); } #[test] -fn test_solution_size_invalid() { - let size: SolutionSize = SolutionSize::Invalid; +fn test_max_invalid() { + let size = Max::(None); assert!(!size.is_valid()); assert_eq!(size.size(), None); } #[test] -fn test_solution_size_unwrap() { - let valid: SolutionSize = SolutionSize::Valid(10); - assert_eq!(valid.unwrap(), 10); +#[should_panic(expected = "called unwrap on invalid Max value")] +fn test_max_unwrap_panics() { + let invalid = Max::(None); + invalid.unwrap(); +} + +#[test] +fn test_min_helpers() { + let size = Min(Some(10)); + assert!(size.is_valid()); + assert_eq!(size.size(), Some(&10)); + assert_eq!(size.unwrap(), 10); } #[test] -#[should_panic(expected = "called unwrap on Invalid")] -fn test_solution_size_unwrap_panics() { - let invalid: SolutionSize = SolutionSize::Invalid; +#[should_panic(expected = "called unwrap on invalid Min value")] +fn test_min_unwrap_panics() { + let invalid = Min::(None); invalid.unwrap(); } #[test] -fn test_solution_size_map() { - let valid: SolutionSize = SolutionSize::Valid(10); - let mapped = valid.map(|x| x * 2); - assert_eq!(mapped, SolutionSize::Valid(20)); - - let invalid: SolutionSize = SolutionSize::Invalid; - let mapped_invalid = invalid.map(|x| x * 2); - assert_eq!(mapped_invalid, SolutionSize::Invalid); +fn test_extremum_helpers() { + let max = Extremum::maximize(Some(10)); + assert!(max.is_valid()); + assert_eq!(max.size(), Some(&10)); + assert_eq!(max.sense, ExtremumSense::Maximize); + + let min = Extremum::minimize(Some(5)); + assert!(min.is_valid()); + assert_eq!(min.size(), Some(&5)); + assert_eq!(min.sense, ExtremumSense::Minimize); + + let invalid = Extremum::::minimize(None); + assert!(!invalid.is_valid()); + assert_eq!(invalid.size(), None); } #[test] @@ -141,11 +157,11 @@ fn test_one_json() { #[test] fn test_direction() { - let max_dir = Direction::Maximize; - let min_dir = Direction::Minimize; + let max_dir = ExtremumSense::Maximize; + let min_dir = ExtremumSense::Minimize; - assert_eq!(max_dir, Direction::Maximize); - assert_eq!(min_dir, Direction::Minimize); + assert_eq!(max_dir, ExtremumSense::Maximize); + assert_eq!(min_dir, ExtremumSense::Minimize); assert_ne!(max_dir, min_dir); } @@ -210,50 +226,3 @@ fn test_weight_element_f64() { let neg: f64 = -2.5; assert_eq!(neg.to_sum(), -2.5); } - -#[test] -fn test_is_better_maximize_valid_vs_valid() { - // For maximization: larger is better - let a = SolutionSize::Valid(10); - let b = SolutionSize::Valid(5); - assert!(a.is_better(&b, Direction::Maximize)); - assert!(!b.is_better(&a, Direction::Maximize)); -} - -#[test] -fn test_is_better_minimize_valid_vs_valid() { - // For minimization: smaller is better - let a = SolutionSize::Valid(5); - let b = SolutionSize::Valid(10); - assert!(a.is_better(&b, Direction::Minimize)); - assert!(!b.is_better(&a, Direction::Minimize)); -} - -#[test] -fn test_is_better_valid_vs_invalid() { - // Valid is always better than invalid - let valid = SolutionSize::Valid(0); - let invalid: SolutionSize = SolutionSize::Invalid; - assert!(valid.is_better(&invalid, Direction::Maximize)); - assert!(valid.is_better(&invalid, Direction::Minimize)); - assert!(!invalid.is_better(&valid, Direction::Maximize)); - assert!(!invalid.is_better(&valid, Direction::Minimize)); -} - -#[test] -fn test_is_better_invalid_vs_invalid() { - // Neither invalid is better - let a: SolutionSize = SolutionSize::Invalid; - let b: SolutionSize = SolutionSize::Invalid; - assert!(!a.is_better(&b, Direction::Maximize)); - assert!(!a.is_better(&b, Direction::Minimize)); -} - -#[test] -fn test_is_better_equal_valid() { - // Equal values: neither is better - let a = SolutionSize::Valid(5); - let b = SolutionSize::Valid(5); - assert!(!a.is_better(&b, Direction::Maximize)); - assert!(!a.is_better(&b, Direction::Minimize)); -} diff --git a/tests/suites/integration.rs b/tests/suites/integration.rs index 973333f38..126dae9a1 100644 --- a/tests/suites/integration.rs +++ b/tests/suites/integration.rs @@ -23,7 +23,7 @@ mod all_problems_solvable { vec![1i32; 4], ); let solver = BruteForce::new(); - let solutions = solver.find_all_best(&problem); + let solutions = solver.find_all_witnesses(&problem); assert!(!solutions.is_empty()); for sol in &solutions { assert!(problem.evaluate(sol).is_valid()); @@ -37,7 +37,7 @@ mod all_problems_solvable { vec![1i32; 4], ); let solver = BruteForce::new(); - let solutions = solver.find_all_best(&problem); + let solutions = solver.find_all_witnesses(&problem); assert!(!solutions.is_empty()); for sol in &solutions { assert!(problem.evaluate(sol).is_valid()); @@ -51,7 +51,7 @@ mod all_problems_solvable { vec![1, 2, 1], ); let solver = BruteForce::new(); - let solutions = solver.find_all_best(&problem); + let solutions = solver.find_all_witnesses(&problem); assert!(!solutions.is_empty()); } @@ -60,7 +60,7 @@ mod all_problems_solvable { let problem = KColoring::::new(SimpleGraph::new(3, vec![(0, 1), (1, 2)])); let solver = BruteForce::new(); // KColoring returns bool, so we can use find_all_satisfying - let satisfying = solver.find_all_satisfying(&problem); + let satisfying = solver.find_all_witnesses(&problem); assert!(!satisfying.is_empty()); for sol in &satisfying { assert!(problem.evaluate(sol)); @@ -74,7 +74,7 @@ mod all_problems_solvable { vec![1i32; 4], ); let solver = BruteForce::new(); - let solutions = solver.find_all_best(&problem); + let solutions = solver.find_all_witnesses(&problem); assert!(!solutions.is_empty()); for sol in &solutions { assert!(problem.evaluate(sol).is_valid()); @@ -88,7 +88,7 @@ mod all_problems_solvable { vec![1i32; 4], ); let solver = BruteForce::new(); - let solutions = solver.find_all_best(&problem); + let solutions = solver.find_all_witnesses(&problem); assert!(!solutions.is_empty()); for sol in &solutions { assert!(problem.evaluate(sol).is_valid()); @@ -102,7 +102,7 @@ mod all_problems_solvable { vec![1, 2, 1], ); let solver = BruteForce::new(); - let solutions = solver.find_all_best(&problem); + let solutions = solver.find_all_witnesses(&problem); assert!(!solutions.is_empty()); for sol in &solutions { assert!(problem.evaluate(sol).is_valid()); @@ -133,7 +133,7 @@ mod all_problems_solvable { 8, ); let solver = BruteForce::new(); - let solution = solver.find_satisfying(&problem); + let solution = solver.find_witness(&problem); assert!(solution.is_some()); assert!(problem.evaluate(&solution.unwrap())); } @@ -146,7 +146,7 @@ mod all_problems_solvable { 2, ); let solver = BruteForce::new(); - let satisfying = solver.find_all_satisfying(&problem); + let satisfying = solver.find_all_witnesses(&problem); assert_eq!(satisfying, vec![vec![0, 0, 1]]); assert!(satisfying.iter().all(|config| problem.evaluate(config))); } @@ -175,7 +175,7 @@ mod all_problems_solvable { fn test_spin_glass_solvable() { let problem = SpinGlass::new(3, vec![((0, 1), -1.0), ((1, 2), 1.0)], vec![0.5, -0.5, 0.0]); let solver = BruteForce::new(); - let solutions = solver.find_all_best(&problem); + let solutions = solver.find_all_witnesses(&problem); assert!(!solutions.is_empty()); } @@ -187,7 +187,7 @@ mod all_problems_solvable { vec![0.0, 0.0, 1.0], ]); let solver = BruteForce::new(); - let solutions = solver.find_all_best(&problem); + let solutions = solver.find_all_witnesses(&problem); assert!(!solutions.is_empty()); } @@ -196,7 +196,7 @@ mod all_problems_solvable { let problem = MinimumSetCovering::::new(5, vec![vec![0, 1, 2], vec![2, 3, 4], vec![0, 4]]); let solver = BruteForce::new(); - let solutions = solver.find_all_best(&problem); + let solutions = solver.find_all_witnesses(&problem); assert!(!solutions.is_empty()); for sol in &solutions { assert!(problem.evaluate(sol).is_valid()); @@ -208,7 +208,7 @@ mod all_problems_solvable { let problem = MaximumSetPacking::::new(vec![vec![0, 1], vec![2, 3], vec![1, 2], vec![4]]); let solver = BruteForce::new(); - let solutions = solver.find_all_best(&problem); + let solutions = solver.find_all_witnesses(&problem); assert!(!solutions.is_empty()); for sol in &solutions { assert!(problem.evaluate(sol).is_valid()); @@ -240,7 +240,7 @@ mod all_problems_solvable { fn test_factoring_solvable() { let problem = Factoring::new(15, 2, 2); let solver = BruteForce::new(); - let solutions = solver.find_all_best(&problem); + let solutions = solver.find_all_witnesses(&problem); assert!(!solutions.is_empty()); for sol in &solutions { assert!(problem.evaluate(sol).is_valid()); @@ -255,7 +255,7 @@ mod all_problems_solvable { vec![0, 1, 2], ); let solver = BruteForce::new(); - let solutions = solver.find_all_satisfying(&problem); + let solutions = solver.find_all_witnesses(&problem); assert!(solutions.contains(&vec![1, 0, 0])); } @@ -263,7 +263,7 @@ mod all_problems_solvable { fn test_paintshop_solvable() { let problem = PaintShop::new(vec!["a", "b", "a", "b"]); let solver = BruteForce::new(); - let solutions = solver.find_all_best(&problem); + let solutions = solver.find_all_witnesses(&problem); assert!(!solutions.is_empty()); } @@ -275,7 +275,7 @@ mod all_problems_solvable { 1, ); let solver = BruteForce::new(); - let solutions = solver.find_all_best(&problem); + let solutions = solver.find_all_witnesses(&problem); assert!(!solutions.is_empty()); for sol in &solutions { assert!(problem.evaluate(sol).is_valid()); @@ -286,7 +286,7 @@ mod all_problems_solvable { fn test_bmf_solvable() { let problem = BMF::new(vec![vec![true, true], vec![true, true]], 1); let solver = BruteForce::new(); - let solutions = solver.find_all_best(&problem); + let solutions = solver.find_all_witnesses(&problem); assert!(!solutions.is_empty()); for sol in &solutions { // BMF minimizes Hamming distance, all configs are valid (no invalid marker) @@ -311,8 +311,8 @@ mod problem_relationships { let vc_problem = MinimumVertexCover::new(SimpleGraph::new(n, edges), vec![1i32; n]); let solver = BruteForce::new(); - let is_solutions = solver.find_all_best(&is_problem); - let vc_solutions = solver.find_all_best(&vc_problem); + let is_solutions = solver.find_all_witnesses(&is_problem); + let vc_solutions = solver.find_all_witnesses(&vc_problem); let max_is_size = is_solutions[0].iter().sum::(); let min_vc_size = vc_solutions[0].iter().sum::(); @@ -331,7 +331,7 @@ mod problem_relationships { let is_problem = MaximumIndependentSet::new(SimpleGraph::new(n, edges), vec![1i32; n]); let solver = BruteForce::new(); - let maximal_solutions = solver.find_all_best(&maximal_is); + let maximal_solutions = solver.find_all_witnesses(&maximal_is); // Every maximal IS is also a valid IS for sol in &maximal_solutions { @@ -367,7 +367,7 @@ mod problem_relationships { ); let solver = BruteForce::new(); - let solutions = solver.find_all_best(&problem); + let solutions = solver.find_all_witnesses(&problem); // Optimal should be all same spin (all 0 or all 1) for sol in &solutions { @@ -391,11 +391,11 @@ mod problem_relationships { let solver = BruteForce::new(); // All sets needed for cover - let cover_solutions = solver.find_all_best(&covering); + let cover_solutions = solver.find_all_witnesses(&covering); assert_eq!(cover_solutions[0].iter().sum::(), 3); // All sets can be packed (no overlap) - let pack_solutions = solver.find_all_best(&packing); + let pack_solutions = solver.find_all_witnesses(&packing); assert_eq!(pack_solutions[0].iter().sum::(), 3); } } @@ -408,7 +408,7 @@ mod edge_cases { fn test_empty_graph_independent_set() { let problem = MaximumIndependentSet::new(SimpleGraph::new(3, vec![]), vec![1i32; 3]); let solver = BruteForce::new(); - let solutions = solver.find_all_best(&problem); + let solutions = solver.find_all_witnesses(&problem); // All vertices can be in IS when no edges assert_eq!(solutions[0].iter().sum::(), 3); @@ -420,7 +420,7 @@ mod edge_cases { let edges = vec![(0, 1), (0, 2), (0, 3), (1, 2), (1, 3), (2, 3)]; let problem = MaximumIndependentSet::new(SimpleGraph::new(4, edges), vec![1i32; 4]); let solver = BruteForce::new(); - let solutions = solver.find_all_best(&problem); + let solutions = solver.find_all_witnesses(&problem); // Maximum IS in complete graph is 1 assert_eq!(solutions[0].iter().sum::(), 1); @@ -450,7 +450,7 @@ mod edge_cases { // Factor 4 = 2 * 2 let problem = Factoring::new(4, 2, 2); let solver = BruteForce::new(); - let solutions = solver.find_all_best(&problem); + let solutions = solver.find_all_witnesses(&problem); assert!(!solutions.is_empty()); for sol in &solutions { @@ -462,7 +462,7 @@ mod edge_cases { fn test_single_car_paintshop() { let problem = PaintShop::new(vec!["a", "a"]); let solver = BruteForce::new(); - let solutions = solver.find_all_best(&problem); + let solutions = solver.find_all_witnesses(&problem); // Single car always has 1 switch (color must change) assert_eq!(problem.count_switches(&solutions[0]), 1); @@ -478,7 +478,7 @@ mod weighted_problems { let problem = MaximumIndependentSet::new(SimpleGraph::new(3, vec![(0, 1)]), vec![10, 1, 1]); let solver = BruteForce::new(); - let solutions = solver.find_all_best(&problem); + let solutions = solver.find_all_witnesses(&problem); // Should prefer vertex 0 (weight 10) over vertex 1 (weight 1) // Optimal: {0, 2} with weight 11 @@ -496,7 +496,7 @@ mod weighted_problems { MinimumVertexCover::new(SimpleGraph::new(3, vec![(0, 1), (1, 2)]), vec![1, 10, 1]); let solver = BruteForce::new(); - let solutions = solver.find_all_best(&problem); + let solutions = solver.find_all_witnesses(&problem); // Prefer {0, 2} over {1} because {0,2} has weight 2 vs {1} has weight 10 let best_weight: i32 = solutions[0] @@ -511,7 +511,7 @@ mod weighted_problems { fn test_weighted_max_cut() { let problem = MaxCut::new(SimpleGraph::new(3, vec![(0, 1), (1, 2)]), vec![10, 1]); let solver = BruteForce::new(); - let solutions = solver.find_all_best(&problem); + let solutions = solver.find_all_witnesses(&problem); // Maximum cut should include the heavy edge (0,1) let cut_value = problem.evaluate(&solutions[0]); diff --git a/tests/suites/reductions.rs b/tests/suites/reductions.rs index 66e81ae6e..5ea6e9ecd 100644 --- a/tests/suites/reductions.rs +++ b/tests/suites/reductions.rs @@ -31,7 +31,7 @@ mod is_vc_reductions { // Solve the target VC problem let solver = BruteForce::new(); - let vc_solutions = solver.find_all_best(vc_problem); + let vc_solutions = solver.find_all_witnesses(vc_problem); // Extract back to IS solution let is_solution = result.extract_solution(&vc_solutions[0]); @@ -58,7 +58,7 @@ mod is_vc_reductions { // Solve the target IS problem let solver = BruteForce::new(); - let is_solutions = solver.find_all_best(is_problem); + let is_solutions = solver.find_all_witnesses(is_problem); // Extract back to VC solution let vc_solution = result.extract_solution(&is_solutions[0]); @@ -91,7 +91,7 @@ mod is_vc_reductions { // Solve the final problem let solver = BruteForce::new(); - let solutions = solver.find_all_best(final_is); + let solutions = solver.find_all_witnesses(final_is); // Extract through the chain let intermediate_sol = back_to_is.extract_solution(&solutions[0]); @@ -126,10 +126,10 @@ mod is_vc_reductions { let solver = BruteForce::new(); // Solve IS, reduce to VC solution - let is_solutions = solver.find_all_best(&is_problem); + let is_solutions = solver.find_all_witnesses(&is_problem); let max_is = is_solutions[0].iter().sum::(); - let vc_solutions = solver.find_all_best(&vc_problem); + let vc_solutions = solver.find_all_witnesses(&vc_problem); let min_vc = vc_solutions[0].iter().sum::(); assert_eq!(max_is + min_vc, n); @@ -156,7 +156,7 @@ mod is_sp_reductions { // Solve let solver = BruteForce::new(); - let sp_solutions = solver.find_all_best(sp_problem); + let sp_solutions = solver.find_all_witnesses(sp_problem); // Extract to IS solution let is_solution = result.extract_solution(&sp_solutions[0]); @@ -178,7 +178,7 @@ mod is_sp_reductions { // Solve let solver = BruteForce::new(); - let is_solutions = solver.find_all_best(is_problem); + let is_solutions = solver.find_all_witnesses(is_problem); // Extract to SP solution let sp_solution = result.extract_solution(&is_solutions[0]); @@ -201,7 +201,7 @@ mod is_sp_reductions { // Solve SP let solver = BruteForce::new(); - let sp_solutions = solver.find_all_best(sp_problem); + let sp_solutions = solver.find_all_witnesses(sp_problem); // Extract to IS solution let is_solution = to_sp.extract_solution(&sp_solutions[0]); @@ -210,7 +210,7 @@ mod is_sp_reductions { assert!(original.evaluate(&is_solution).is_valid()); // Should match directly solving IS - let direct_solutions = solver.find_all_best(&original); + let direct_solutions = solver.find_all_witnesses(&original); let direct_max = direct_solutions[0].iter().sum::(); let reduced_max = is_solution.iter().sum::(); @@ -234,7 +234,7 @@ mod sg_qubo_reductions { // Solve QUBO let solver = BruteForce::new(); - let qubo_solutions = solver.find_all_best(qubo); + let qubo_solutions = solver.find_all_witnesses(qubo); // Extract to SG solution let sg_solution = result.extract_solution(&qubo_solutions[0]); @@ -253,7 +253,7 @@ mod sg_qubo_reductions { // Solve SG let solver = BruteForce::new(); - let sg_solutions = solver.find_all_best(sg); + let sg_solutions = solver.find_all_witnesses(sg); // Extract to QUBO solution let qubo_solution = result.extract_solution(&sg_solutions[0]); @@ -275,8 +275,8 @@ mod sg_qubo_reductions { // Check that ground states correspond let solver = BruteForce::new(); - let sg_solutions = solver.find_all_best(&sg); - let qubo_solutions = solver.find_all_best(qubo); + let sg_solutions = solver.find_all_witnesses(&sg); + let qubo_solutions = solver.find_all_witnesses(qubo); // Extract QUBO solution back to SG let extracted = result.extract_solution(&qubo_solutions[0]); @@ -316,7 +316,7 @@ mod sg_maxcut_reductions { // Solve MaxCut let solver = BruteForce::new(); - let maxcut_solutions = solver.find_all_best(maxcut); + let maxcut_solutions = solver.find_all_witnesses(maxcut); // Extract to SG solution let sg_solution = result.extract_solution(&maxcut_solutions[0]); @@ -338,7 +338,7 @@ mod sg_maxcut_reductions { // Solve SG let solver = BruteForce::new(); - let sg_solutions = solver.find_all_best(sg); + let sg_solutions = solver.find_all_witnesses(sg); // Extract to MaxCut solution let maxcut_solution = result.extract_solution(&sg_solutions[0]); @@ -360,8 +360,8 @@ mod sg_maxcut_reductions { let solver = BruteForce::new(); // Solve both - let sg_solutions = solver.find_all_best(&sg); - let maxcut_solutions = solver.find_all_best(maxcut); + let sg_solutions = solver.find_all_witnesses(&sg); + let maxcut_solutions = solver.find_all_witnesses(maxcut); // Extract MaxCut solution back to SG let extracted = result.extract_solution(&maxcut_solutions[0]); @@ -389,7 +389,7 @@ mod topology_tests { let sp = MaximumSetPacking::::new(vec![vec![0, 1, 2], vec![2, 3], vec![3, 4]]); let solver = BruteForce::new(); - let solutions = solver.find_all_best(&sp); + let solutions = solver.find_all_witnesses(&sp); assert!(sp.evaluate(&solutions[0]).is_valid()); } @@ -410,7 +410,7 @@ mod topology_tests { let is_problem = MaximumIndependentSet::new(SimpleGraph::new(4, edges), vec![1i32; 4]); let solver = BruteForce::new(); - let solutions = solver.find_all_best(&is_problem); + let solutions = solver.find_all_witnesses(&is_problem); // Vertices 0-1 are connected, 2-3 are connected // Max IS: {0, 2} or {0, 3} or {1, 2} or {1, 3} = size 2 @@ -479,7 +479,7 @@ mod qubo_reductions { assert_eq!(qubo.num_variables(), data.qubo_num_vars); let solver = BruteForce::new(); - let solutions = solver.find_all_best(qubo); + let solutions = solver.find_all_witnesses(qubo); // All QUBO optimal solutions should extract to valid IS solutions for sol in &solutions { @@ -524,7 +524,7 @@ mod qubo_reductions { assert_eq!(qubo.num_variables(), data.qubo_num_vars); let solver = BruteForce::new(); - let solutions = solver.find_all_best(qubo); + let solutions = solver.find_all_witnesses(qubo); for sol in &solutions { let extracted = reduction.extract_solution(sol); @@ -561,7 +561,7 @@ mod qubo_reductions { assert_eq!(qubo.num_variables(), data.qubo_num_vars); let solver = BruteForce::new(); - let solutions = solver.find_all_best(qubo); + let solutions = solver.find_all_witnesses(qubo); for sol in &solutions { let extracted = reduction.extract_solution(sol); @@ -626,7 +626,7 @@ mod qubo_reductions { assert_eq!(qubo.num_variables(), data.qubo_num_vars); let solver = BruteForce::new(); - let solutions = solver.find_all_best(qubo); + let solutions = solver.find_all_witnesses(qubo); for sol in &solutions { let extracted = reduction.extract_solution(sol); @@ -710,7 +710,7 @@ mod qubo_reductions { assert!(qubo.num_variables() >= data.qubo_num_vars); let solver = BruteForce::new(); - let solutions = solver.find_all_best(qubo); + let solutions = solver.find_all_witnesses(qubo); for sol in &solutions { let extracted = reduction.extract_solution(sol); @@ -778,7 +778,7 @@ mod qubo_reductions { let qubo: &QUBO = chain.target_problem(); let solver = BruteForce::new(); - let solutions = solver.find_all_best(qubo); + let solutions = solver.find_all_witnesses(qubo); // Extract back through the full chain to get VC solution for sol in &solutions { @@ -866,20 +866,20 @@ mod end_to_end { // Solve directly let solver = BruteForce::new(); - let is_solutions = solver.find_all_best(&is); + let is_solutions = solver.find_all_witnesses(&is); let direct_size = is_solutions[0].iter().sum::(); // Reduce to VC and solve let to_vc = ReduceTo::>::reduce_to(&is); let vc = to_vc.target_problem(); - let vc_solutions = solver.find_all_best(vc); + let vc_solutions = solver.find_all_witnesses(vc); let vc_extracted = to_vc.extract_solution(&vc_solutions[0]); let via_vc_size = vc_extracted.iter().sum::(); // Reduce to MaximumSetPacking and solve let to_sp = ReduceTo::>::reduce_to(&is); let sp = to_sp.target_problem(); - let sp_solutions = solver.find_all_best(sp); + let sp_solutions = solver.find_all_witnesses(sp); let sp_extracted = to_sp.extract_solution(&sp_solutions[0]); let via_sp_size = sp_extracted.iter().sum::(); @@ -899,7 +899,7 @@ mod end_to_end { // Solve directly let solver = BruteForce::new(); - let sg_solutions = solver.find_all_best(&sg); + let sg_solutions = solver.find_all_witnesses(&sg); // Convert usize solution to i32 spin values for compute_energy let direct_spins: Vec = sg_solutions[0].iter().map(|&x| x as i32).collect(); @@ -908,7 +908,7 @@ mod end_to_end { // Reduce to MaxCut and solve let to_maxcut = ReduceTo::>::reduce_to(&sg); let maxcut = to_maxcut.target_problem(); - let maxcut_solutions = solver.find_all_best(maxcut); + let maxcut_solutions = solver.find_all_witnesses(maxcut); let maxcut_extracted = to_maxcut.extract_solution(&maxcut_solutions[0]); // Convert extracted solution to spins for energy computation @@ -935,7 +935,7 @@ mod end_to_end { // Solve VC let solver = BruteForce::new(); - let vc_solutions = solver.find_all_best(vc); + let vc_solutions = solver.find_all_witnesses(vc); // Extract back through chain let is_sol = is_to_vc.extract_solution(&vc_solutions[0]); From 7247dd40b2ec4a03f453dce5e7296542d574e764 Mon Sep 17 00:00:00 2001 From: GiggleLiu Date: Mon, 23 Mar 2026 14:22:35 +0800 Subject: [PATCH 08/18] fix ci --- docs/paper/reductions.typ | 78 +++++++++++++---------- problemreductions-cli/src/test_support.rs | 4 +- problemreductions-macros/src/lib.rs | 4 +- src/registry/dyn_problem.rs | 30 ++++++++- src/registry/mod.rs | 2 +- src/unit_tests/registry/dispatch.rs | 29 +++++++++ 6 files changed, 108 insertions(+), 39 deletions(-) diff --git a/docs/paper/reductions.typ b/docs/paper/reductions.typ index 9d45f6a91..0f2ade4f0 100644 --- a/docs/paper/reductions.typ +++ b/docs/paper/reductions.typ @@ -48,6 +48,20 @@ } } +#let metric-value(metric) = { + if type(metric) == dictionary { + if "Valid" in metric { + metric.Valid + } else if "value" in metric { + metric.value + } else { + metric + } + } else { + metric + } +} + #let graph-num-vertices(instance) = instance.graph.num_vertices #let graph-num-edges(instance) = instance.graph.edges.len() #let spin-num-spins(instance) = instance.fields.len() @@ -494,7 +508,7 @@ In all graph problems below, $G = (V, E)$ denotes an undirected graph with $|V| // Pick optimal config = {v1, v3, v5, v9} to match figure let sol = (config: x.optimal_config, metric: x.optimal_value) let S = sol.config.enumerate().filter(((i, v)) => v == 1).map(((i, _)) => i) - let alpha = sol.metric.Valid + let alpha = metric-value(sol.metric) [ #problem-def("MaximumIndependentSet")[ Given $G = (V, E)$ with vertex weights $w: V -> RR$, find $S subset.eq V$ maximizing $sum_(v in S) w(v)$ such that no two vertices in $S$ are adjacent: $forall u, v in S: (u, v) in.not E$. @@ -527,7 +541,7 @@ In all graph problems below, $G = (V, E)$ denotes an undirected graph with $|V| // Pick optimal config = {v0, v3, v4} to match figure let sol = (config: x.optimal_config, metric: x.optimal_value) let cover = sol.config.enumerate().filter(((i, v)) => v == 1).map(((i, _)) => i) - let wS = sol.metric.Valid + let wS = metric-value(sol.metric) let complement = sol.config.enumerate().filter(((i, v)) => v == 0).map(((i, _)) => i) let alpha = complement.len() [ @@ -566,7 +580,7 @@ In all graph problems below, $G = (V, E)$ denotes an undirected graph with $|V| let sol = (config: x.optimal_config, metric: x.optimal_value) let side-s = sol.config.enumerate().filter(((i, v)) => v == 1).map(((i, _)) => i) let side-sbar = sol.config.enumerate().filter(((i, v)) => v == 0).map(((i, _)) => i) - let cut-val = sol.metric.Valid + let cut-val = metric-value(sol.metric) let cut-edges = edges.filter(e => side-s.contains(e.at(0)) != side-s.contains(e.at(1))) let uncut-edges = edges.filter(e => side-s.contains(e.at(0)) == side-s.contains(e.at(1))) [ @@ -599,7 +613,7 @@ In all graph problems below, $G = (V, E)$ denotes an undirected graph with $|V| let ne = graph-num-edges(x.instance) let edges = x.instance.graph.edges.map(e => (e.at(0), e.at(1))) let config = x.optimal_config - let cut-val = x.optimal_value.Valid + let cut-val = metric-value(x.optimal_value) let side-a = range(nv).filter(i => config.at(i) == 0) let side-b = range(nv).filter(i => config.at(i) == 1) let cut-edges = edges.filter(e => config.at(e.at(0)) != config.at(e.at(1))) @@ -1576,7 +1590,7 @@ is feasible: each set induces a connected subgraph, the component weights are $2 // Pick optimal config = {v2, v3} to match figure let sol = (config: x.optimal_config, metric: x.optimal_value) let S = sol.config.enumerate().filter(((i, v)) => v == 1).map(((i, _)) => i) - let wS = sol.metric.Valid + let wS = metric-value(sol.metric) // Compute neighbors dominated by each vertex in S let dominated = S.map(s => { let nbrs = () @@ -1617,7 +1631,7 @@ is feasible: each set induces a connected subgraph, the component weights are $2 // Pick optimal config [1,0,0,0,1,0] = edges {(0,1),(2,4)} to match figure let sol = (config: x.optimal_config, metric: x.optimal_value) let matched-edges = sol.config.enumerate().filter(((i, v)) => v == 1).map(((i, _)) => edges.at(i)) - let wM = sol.metric.Valid + let wM = metric-value(sol.metric) // Collect matched vertices let matched-verts = () for (u, v) in matched-edges { @@ -1656,7 +1670,7 @@ is feasible: each set induces a connected subgraph, the component weights are $2 let ew = x.instance.edge_weights let sol = (config: x.optimal_config, metric: x.optimal_value) let tour-edges = sol.config.enumerate().filter(((i, v)) => v == 1).map(((i, _)) => edges.at(i)) - let bottleneck = sol.metric.Valid + let bottleneck = metric-value(sol.metric) let tour-weights = tour-edges.map(((u, v)) => { let idx = edges.position(e => e == (u, v) or e == (v, u)) int(ew.at(idx)) @@ -1746,7 +1760,7 @@ is feasible: each set induces a connected subgraph, the component weights are $2 let ew = x.instance.edge_weights let sol = (config: x.optimal_config, metric: x.optimal_value) let tour-edges = sol.config.enumerate().filter(((i, v)) => v == 1).map(((i, _)) => edges.at(i)) - let tour-cost = sol.metric.Valid + let tour-cost = metric-value(sol.metric) // Build ordered tour from tour-edges starting at vertex 0 let tour-order = (0,) let remaining = tour-edges @@ -1816,7 +1830,7 @@ is feasible: each set induces a connected subgraph, the component weights are $2 let sol = (config: x.optimal_config, metric: x.optimal_value) let tree-edge-indices = sol.config.enumerate().filter(((i, v)) => v == 1).map(((i, _)) => i) let tree-edges = tree-edge-indices.map(i => edges.at(i)) - let cost = sol.metric.Valid + let cost = metric-value(sol.metric) // Steiner vertices: in tree but not terminals let tree-verts = tree-edges.map(e => (e.at(0), e.at(1))).fold((), (acc, pair) => { let (u, v) = pair @@ -1964,7 +1978,7 @@ is feasible: each set induces a connected subgraph, the component weights are $2 let sol = (config: x.optimal_config, metric: x.optimal_value) let cut-edge-indices = sol.config.enumerate().filter(((i, v)) => v == 1).map(((i, _)) => i) let cut-edges = cut-edge-indices.map(i => edges.at(i)) - let cost = sol.metric.Valid + let cost = metric-value(sol.metric) [ #problem-def("MinimumMultiwayCut")[ Given an undirected graph $G=(V,E)$ with edge weights $w: E -> RR_(>0)$ and a set of $k$ terminal vertices $T = {t_1, ..., t_k} subset.eq V$, find a minimum-weight set of edges $C subset.eq E$ such that no two terminals remain in the same connected component of $G' = (V, E backslash C)$. @@ -2095,7 +2109,7 @@ is feasible: each set induces a connected subgraph, the component weights are $2 // optimal config = {v2, v3, v4} let sol = (config: x.optimal_config, metric: x.optimal_value) let K = sol.config.enumerate().filter(((i, v)) => v == 1).map(((i, _)) => i) - let omega = sol.metric.Valid + let omega = metric-value(sol.metric) // Edges within the clique let clique-edges = edges.filter(e => K.contains(e.at(0)) and K.contains(e.at(1))) [ @@ -2129,7 +2143,7 @@ is feasible: each set induces a connected subgraph, the component weights are $2 // optimal config = {v0,v2,v4} with w=3 (maximum-weight maximal IS) let opt = (config: x.optimal_config, metric: x.optimal_value) let S-opt = opt.config.enumerate().filter(((i, v)) => v == 1).map(((i, _)) => i) - let w-opt = opt.metric.Valid + let w-opt = metric-value(opt.metric) // Suboptimal maximal IS {v1,v3} with w=2 (hardcoded — no longer in fixture) let S-sub = (1, 3) let w-sub = 2 @@ -2164,7 +2178,7 @@ is feasible: each set induces a connected subgraph, the component weights are $2 let sol = (config: x.optimal_config, metric: x.optimal_value) let merged = arcs.enumerate().filter(((i, _)) => sol.config.at(i) == 1).map(((i, arc)) => arc) let dummy = arcs.enumerate().filter(((i, _)) => sol.config.at(i) == 0).map(((i, arc)) => arc) - let opt = sol.metric.Valid + let opt = metric-value(sol.metric) let blue = graph-colors.at(0) [ #problem-def("MinimumDummyActivitiesPert")[ @@ -2216,7 +2230,7 @@ is feasible: each set induces a connected subgraph, the component weights are $2 // Pick optimal config = {v0} to match figure let sol = (config: x.optimal_config, metric: x.optimal_value) let S = sol.config.enumerate().filter(((i, v)) => v == 1).map(((i, _)) => i) - let wS = sol.metric.Valid + let wS = metric-value(sol.metric) [ #problem-def("MinimumFeedbackVertexSet")[ Given a directed graph $G = (V, A)$ with vertex weights $w: V -> RR$, find $S subset.eq V$ minimizing $sum_(v in S) w(v)$ such that the induced subgraph $G[V backslash S]$ is a directed acyclic graph (DAG). @@ -2269,7 +2283,7 @@ A classical NP-complete problem from Garey and Johnson @garey1979[Ch.~3, p.~76], let terminals = x.instance.terminals let weights = x.instance.edge_weights let sol = (config: x.optimal_config, metric: x.optimal_value) - let opt-weight = sol.metric.Valid + let opt-weight = metric-value(sol.metric) // Derive tree edges from optimal config let tree-edge-indices = sol.config.enumerate().filter(((i, v)) => v == 1).map(((i, _)) => i) let tree-edges = tree-edge-indices.map(i => edges.at(i)) @@ -2324,7 +2338,7 @@ A classical NP-complete problem from Garey and Johnson @garey1979[Ch.~3, p.~76], let nv = graph-num-vertices(x.instance) let edges = x.instance.graph.edges let K = x.instance.k - let opt-cost = x.optimal_value.Valid + let opt-cost = metric-value(x.optimal_value) // Pick optimal config = {v2, v5} to match figure let sol = (config: x.optimal_config, metric: x.optimal_value) let centers = sol.config.enumerate().filter(((i, v)) => v == 1).map(((i, _)) => i) @@ -2514,7 +2528,7 @@ A classical NP-complete problem from Garey and Johnson @garey1979[Ch.~3, p.~76], // Pick optimal config = {S1, S3} (0-indexed: sets 0, 2) to match figure let sol = (config: x.optimal_config, metric: x.optimal_value) let selected = sol.config.enumerate().filter(((i, v)) => v == 1).map(((i, _)) => i) - let wP = sol.metric.Valid + let wP = metric-value(sol.metric) // Format a set as {e1+1, e2+1, ...} (1-indexed) let fmt-set(s) = "${" + s.map(e => str(e + 1)).join(", ") + "}$" [ @@ -2557,7 +2571,7 @@ A classical NP-complete problem from Garey and Johnson @garey1979[Ch.~3, p.~76], let U-size = x.instance.universe_size let sol = (config: x.optimal_config, metric: x.optimal_value) let selected = sol.config.enumerate().filter(((i, v)) => v == 1).map(((i, _)) => i) - let wC = sol.metric.Valid + let wC = metric-value(sol.metric) let fmt-set(s) = "${" + s.map(e => str(e + 1)).join(", ") + "}$" [ #problem-def("MinimumSetCovering")[ @@ -2602,7 +2616,7 @@ A classical NP-complete problem from Garey and Johnson @garey1979[Ch.~3, p.~76], let U-size = x.instance.universe_size let sol = (config: x.optimal_config, metric: x.optimal_value) let selected = sol.config.enumerate().filter(((i, v)) => v == 1).map(((i, _)) => i) - let hit-size = sol.metric.Valid + let hit-size = metric-value(sol.metric) let fmt-set(s) = if s.len() == 0 { $emptyset$ } else { @@ -3041,7 +3055,7 @@ A classical NP-complete problem from Garey and Johnson @garey1979[Ch.~3, p.~76], let sol = (config: x.optimal_config, metric: x.optimal_value) // Convert config (0=+1, 1=-1) to spin values let spins = sol.config.map(v => if v == 0 { 1 } else { -1 }) - let H = sol.metric.Valid + let H = metric-value(sol.metric) let spin-str = spins.map(s => if s > 0 { "+" } else { "-" }).join(", ") // Count satisfied and frustrated edges let sat-count = edges.filter(((u, v)) => spins.at(u) * spins.at(v) < 0).len() @@ -3088,7 +3102,7 @@ A classical NP-complete problem from Garey and Johnson @garey1979[Ch.~3, p.~76], let Q = x.instance.matrix let sol = (config: x.optimal_config, metric: x.optimal_value) let xstar = sol.config - let fstar = sol.metric.Valid + let fstar = metric-value(sol.metric) // Format the Q matrix as semicolon-separated rows let mat-rows = Q.map(row => row.map(v => { let vi = int(v) @@ -3128,7 +3142,7 @@ A classical NP-complete problem from Garey and Johnson @garey1979[Ch.~3, p.~76], let constraints = x.instance.constraints let sol = (config: x.optimal_config, metric: x.optimal_value) let xstar = sol.config - let fstar = sol.metric.Valid + let fstar = metric-value(sol.metric) // Format objective: c1*x1 + c2*x2 + ... let fmt-obj = obj.map(((i, c)) => { let ci = int(c) @@ -3217,7 +3231,7 @@ A classical NP-complete problem from Garey and Johnson @garey1979[Ch.~3, p.~76], let m = D.len() let sol = (config: x.optimal_config, metric: x.optimal_value) let fstar = sol.config - let cost-star = sol.metric.Valid + let cost-star = metric-value(sol.metric) // Convert integer matrix to math.mat content let to-mat(m) = math.mat(..m.map(row => row.map(v => $#v$))) // Compute identity assignment cost @@ -3308,7 +3322,7 @@ A classical NP-complete problem from Garey and Johnson @garey1979[Ch.~3, p.~76], let target = x.instance.target let bounds = x.instance.bounds let sol = (config: x.optimal_config, metric: x.optimal_value) - let dist = sol.metric.Valid + let dist = metric-value(sol.metric) // Config encodes offset from lower bound; recover actual integer coordinates let coords = sol.config.enumerate().map(((i, v)) => v + bounds.at(i).lower) // Compute B*x: sum over j of coords[j] * basis[j] @@ -3674,7 +3688,7 @@ A classical NP-complete problem from Garey and Johnson @garey1979[Ch.~3, p.~76], let nc = x.instance.n let k = x.instance.k let A = x.instance.matrix - let dH = x.optimal_value.Valid + let dH = metric-value(x.optimal_value) // Decode B and C from optimal config // Config layout: B is m*k values, then C is k*n values let cfg = x.optimal_config @@ -3773,7 +3787,7 @@ A classical NP-complete problem from Garey and Johnson @garey1979[Ch.~3, p.~76], let is-first = x.instance.is_first let sol = (config: x.optimal_config, metric: x.optimal_value) let assign = sol.config // color assignment per car - let num-changes = sol.metric.Valid + let num-changes = metric-value(sol.metric) // Build the full sequence of car labels let seq-labels = seq-indices.map(i => labels.at(i)) // Build color sequence: for each position, if is_first[pos] then color = assign[car], else 1-assign[car] @@ -3827,7 +3841,7 @@ A classical NP-complete problem from Garey and Johnson @garey1979[Ch.~3, p.~76], let bip-edges = x.instance.graph.edges // (li, rj) pairs let ne = bip-edges.len() let sol = (config: x.optimal_config, metric: x.optimal_value) - let total-size = sol.metric.Valid + let total-size = metric-value(sol.metric) [ #problem-def("BicliqueCover")[ Given a bipartite graph $G = (L, R, E)$ and integer $k$, find $k$ bicliques $(L_1, R_1), dots, (L_k, R_k)$ that cover all edges ($E subset.eq union.big_i L_i times R_i$) while minimizing the total size $sum_i (|L_i| + |R_i|)$. @@ -3990,7 +4004,7 @@ A classical NP-complete problem from Garey and Johnson @garey1979[Ch.~3, p.~76], let n = sizes.len() let C = x.instance.capacity let config = x.optimal_config - let num-bins = x.optimal_value.Valid + let num-bins = metric-value(x.optimal_value) // Group items by bin let bins-contents = range(num-bins).map(b => range(n).filter(i => config.at(i) == b) @@ -4048,7 +4062,7 @@ A classical NP-complete problem from Garey and Johnson @garey1979[Ch.~3, p.~76], let C = x.instance.capacity let n = weights.len() let config = x.optimal_config - let opt-val = x.optimal_value.Valid + let opt-val = metric-value(x.optimal_value) let selected = range(n).filter(i => config.at(i) == 1) let total-w = selected.map(i => weights.at(i)).sum() let total-v = selected.map(i => values.at(i)).sum() @@ -4785,7 +4799,7 @@ A classical NP-complete problem from Garey and Johnson @garey1979[Ch.~3, p.~76], let na = arcs.len() let weights = x.instance.weights let config = x.optimal_config - let opt-val = x.optimal_value.Valid + let opt-val = metric-value(x.optimal_value) let removed = range(na).filter(i => config.at(i) == 1) [ #problem-def("MinimumFeedbackArcSet")[ @@ -5389,7 +5403,7 @@ A classical NP-complete problem from Garey and Johnson @garey1979[Ch.~3, p.~76], let deadlines = x.instance.deadlines let precs = x.instance.precedences let sol = (config: x.optimal_config, metric: x.optimal_value) - let tardy-count = sol.metric.Valid + let tardy-count = metric-value(sol.metric) // Decode Lehmer code to permutation (schedule order) let lehmer = sol.config let schedule = { @@ -5472,7 +5486,7 @@ A classical NP-complete problem from Garey and Johnson @garey1979[Ch.~3, p.~76], let precs = x.instance.precedences let ntasks = lengths.len() let sol = (config: x.optimal_config, metric: x.optimal_value) - let opt = sol.metric.Valid + let opt = metric-value(sol.metric) let lehmer = sol.config let schedule = { let avail = range(ntasks) diff --git a/problemreductions-cli/src/test_support.rs b/problemreductions-cli/src/test_support.rs index 0528ee124..2a5b971ac 100644 --- a/problemreductions-cli/src/test_support.rs +++ b/problemreductions-cli/src/test_support.rs @@ -104,7 +104,7 @@ where .downcast_ref::

() .expect("test solve_value downcast failed"); let solver = BruteForce::new(); - format!("{:?}", solver.solve(problem)) + problemreductions::registry::format_metric(&solver.solve(problem)) } fn solve_witness

(any: &dyn Any) -> Option<(Vec, String)> @@ -115,7 +115,7 @@ where let problem = any.downcast_ref::

()?; let solver = BruteForce::new(); let config = solver.find_witness(problem)?; - let evaluation = format!("{:?}", problem.evaluate(&config)); + let evaluation = problemreductions::registry::format_metric(&problem.evaluate(&config)); Some((config, evaluation)) } diff --git a/problemreductions-macros/src/lib.rs b/problemreductions-macros/src/lib.rs index e3e9351ab..ad4a8baf8 100644 --- a/problemreductions-macros/src/lib.rs +++ b/problemreductions-macros/src/lib.rs @@ -565,7 +565,7 @@ fn generate_declare_variants(input: &DeclareVariantsInput) -> syn::Result::solve(&solver, p); - format!("{:?}", total) + crate::registry::format_metric(&total) }; let solve_witness_body = quote! { @@ -592,7 +592,7 @@ fn generate_declare_variants(input: &DeclareVariantsInput) -> syn::Result()?; let solver = crate::solvers::BruteForce::new(); #solve_witness_body - let evaluation = format!("{:?}", crate::traits::Problem::evaluate(p, &config)); + let evaluation = crate::registry::format_metric(&crate::traits::Problem::evaluate(p, &config)); Some((config, evaluation)) }, }; diff --git a/src/registry/dyn_problem.rs b/src/registry/dyn_problem.rs index 0f3acf1c3..00faf39f3 100644 --- a/src/registry/dyn_problem.rs +++ b/src/registry/dyn_problem.rs @@ -6,11 +6,37 @@ use std::fmt; use crate::traits::Problem; +/// Format a metric for CLI- and registry-facing dynamic dispatch. +/// +/// Optimization aggregates migrated from `Valid/Invalid` to `Max/Min`, but the +/// dynamic CLI surface still exposes the legacy `Valid(...)` / `Invalid` +/// strings. Preserve that presentation here so higher layers do not leak the +/// aggregate internals. +pub fn format_metric(metric: &T) -> String +where + T: fmt::Debug + Serialize, +{ + let debug = format!("{metric:?}"); + + match debug.as_str() { + "Max(None)" | "Min(None)" => return "Invalid".to_string(), + _ => {} + } + + if debug.starts_with("Max(Some(") || debug.starts_with("Min(Some(") { + let value = serde_json::to_value(metric).expect("serialize metric failed"); + let inner = serde_json::to_string(&value).expect("serialize metric inner value failed"); + return format!("Valid({inner})"); + } + + debug +} + /// Type-erased problem interface for dynamic dispatch. /// /// Implemented via blanket impl for any `T: Problem + Serialize + 'static`. pub trait DynProblem: Any { - /// Evaluate a configuration and return the result as a debug string. + /// Evaluate a configuration and return the CLI-facing metric string. fn evaluate_dyn(&self, config: &[usize]) -> String; /// Evaluate a configuration and return the result as a serializable JSON value. fn evaluate_json(&self, config: &[usize]) -> Value; @@ -34,7 +60,7 @@ where T::Value: fmt::Debug + Serialize, { fn evaluate_dyn(&self, config: &[usize]) -> String { - format!("{:?}", self.evaluate(config)) + format_metric(&self.evaluate(config)) } fn evaluate_json(&self, config: &[usize]) -> Value { diff --git a/src/registry/mod.rs b/src/registry/mod.rs index 1c8c64570..24e6271c2 100644 --- a/src/registry/mod.rs +++ b/src/registry/mod.rs @@ -51,7 +51,7 @@ pub mod problem_type; mod schema; pub mod variant; -pub use dyn_problem::{DynProblem, LoadedDynProblem, SolveValueFn, SolveWitnessFn}; +pub use dyn_problem::{format_metric, DynProblem, LoadedDynProblem, SolveValueFn, SolveWitnessFn}; pub use info::{ComplexityClass, FieldInfo, ProblemInfo, ProblemMetadata}; 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}; diff --git a/src/unit_tests/registry/dispatch.rs b/src/unit_tests/registry/dispatch.rs index 689124925..727916a53 100644 --- a/src/unit_tests/registry/dispatch.rs +++ b/src/unit_tests/registry/dispatch.rs @@ -1,4 +1,5 @@ use crate::models::graph::MaximumIndependentSet; +use crate::models::graph::MinimumVertexCover; use crate::models::misc::SubsetSum; use crate::registry::variant::find_variant_entry; use crate::registry::{load_dyn, serialize_any, DynProblem, LoadedDynProblem}; @@ -71,6 +72,15 @@ fn test_dyn_problem_blanket_impl_exposes_problem_metadata() { assert!(dyn_problem.serialize_json().is_object()); } +#[test] +fn test_dyn_problem_formats_optimization_values_as_legacy_valid_invalid() { + let problem = MaximumIndependentSet::new(SimpleGraph::new(3, vec![(0, 1)]), vec![1i32; 3]); + let dyn_problem: &dyn DynProblem = &problem; + + assert_eq!(dyn_problem.evaluate_dyn(&[1, 0, 1]), "Valid(2)"); + assert_eq!(dyn_problem.evaluate_dyn(&[1, 1, 0]), "Invalid"); +} + #[test] fn test_loaded_dyn_problem_delegates_to_value_and_witness_fns() { let problem = SubsetSum::new(vec![3u32, 7u32, 1u32], 4u32); @@ -102,6 +112,25 @@ fn loaded_dyn_problem_returns_none_for_aggregate_only_witness() { assert!(loaded.solve_brute_force_witness().is_none()); } +#[test] +fn test_load_dyn_formats_optimization_solve_values_as_legacy_valid_invalid() { + let problem = MinimumVertexCover::new(SimpleGraph::new(3, vec![(0, 1), (1, 2)]), vec![1i32; 3]); + let variant = BTreeMap::from([ + ("graph".to_string(), "SimpleGraph".to_string()), + ("weight".to_string(), "i32".to_string()), + ]); + let loaded = load_dyn( + "MinimumVertexCover", + &variant, + serde_json::to_value(&problem).unwrap(), + ) + .unwrap(); + + assert_eq!(loaded.solve_brute_force_value(), "Valid(1)"); + let solved = loaded.solve_brute_force_witness().unwrap(); + assert_eq!(solved.1, "Valid(1)"); +} + #[test] fn test_find_variant_entry_requires_exact_variant() { let partial = BTreeMap::from([("graph".to_string(), "SimpleGraph".to_string())]); From f969417bcce0c07d6cfd58939da47eba61b3c9bc Mon Sep 17 00:00:00 2001 From: GiggleLiu Date: Mon, 23 Mar 2026 15:53:48 +0800 Subject: [PATCH 09/18] fix: resolve clippy warnings in CLI crate - Inline string literals in eprintln! format strings (print_literal) - Use saturating_sub instead of manual arithmetic check - Merge identical if-else branches in add_ilp_solver_hint Co-Authored-By: Claude Opus 4.6 (1M context) --- problemreductions-cli/src/commands/create.rs | 31 ++++---------------- problemreductions-cli/src/commands/solve.rs | 8 ++--- 2 files changed, 9 insertions(+), 30 deletions(-) diff --git a/problemreductions-cli/src/commands/create.rs b/problemreductions-cli/src/commands/create.rs index 4abe9ee30..28d845544 100644 --- a/problemreductions-cli/src/commands/create.rs +++ b/problemreductions-cli/src/commands/create.rs @@ -916,27 +916,12 @@ fn print_problem_help(canonical: &str, graph_type: Option<&str>) -> Result<()> { let hint = type_format_hint(&field.type_name, graph_type); eprintln!(" --{:<16} {} ({})", "arcs", field.description, hint); } else if field.type_name == "MixedGraph" { - eprintln!( - " --{:<16} {} ({})", - "graph", "Undirected edges E of the mixed graph", "edge list: 0-1,1-2,2-3" - ); - eprintln!( - " --{:<16} {} ({})", - "arcs", "Directed arcs A of the mixed graph", "directed arcs: 0>1,1>2,2>0" - ); + eprintln!(" --{:<16} Undirected edges E of the mixed graph (edge list: 0-1,1-2,2-3)", "graph"); + eprintln!(" --{:<16} Directed arcs A of the mixed graph (directed arcs: 0>1,1>2,2>0)", "arcs"); } else if field.type_name == "BipartiteGraph" { - eprintln!( - " --{:<16} {} ({})", - "left", "Vertices in the left partition", "integer" - ); - eprintln!( - " --{:<16} {} ({})", - "right", "Vertices in the right partition", "integer" - ); - eprintln!( - " --{:<16} {} ({})", - "biedges", "Bipartite edges as left-right pairs", "edge list: 0-0,0-1,1-2" - ); + eprintln!(" --{:<16} Vertices in the left partition (integer)", "left"); + eprintln!(" --{:<16} Vertices in the right partition (integer)", "right"); + eprintln!(" --{:<16} Bipartite edges as left-right pairs (edge list: 0-0,0-1,1-2)", "biedges"); } else { let hint = help_flag_hint(canonical, &field.name, &field.type_name, graph_type); eprintln!(" --{:<16} {} ({})", flag_name, field.description, hint); @@ -5946,11 +5931,7 @@ fn create_random( let num_edges = graph.num_edges(); let edge_weights = vec![1i32; num_edges]; let source = 0; - let sink = if num_vertices > 1 { - num_vertices - 1 - } else { - 0 - }; + let sink = num_vertices.saturating_sub(1); let size_bound = num_vertices; // no effective size constraint let cut_bound = num_edges as i32; // generous bound let variant = variant_map(&[("graph", "SimpleGraph"), ("weight", "i32")]); diff --git a/problemreductions-cli/src/commands/solve.rs b/problemreductions-cli/src/commands/solve.rs index 66cff99b5..1e467d43a 100644 --- a/problemreductions-cli/src/commands/solve.rs +++ b/problemreductions-cli/src/commands/solve.rs @@ -231,11 +231,9 @@ fn solve_bundle(bundle: ReductionBundle, solver_name: &str, out: &OutputConfig) fn add_ilp_solver_hint(err: anyhow::Error) -> anyhow::Error { let message = err.to_string(); - if message.starts_with("No reduction path from ") && message.ends_with(" to ILP") { - anyhow::anyhow!( - "{message}\n\nHint: try `--solver brute-force` for direct exhaustive search on small instances." - ) - } else if message.contains("witness-capable") { + if (message.starts_with("No reduction path from ") && message.ends_with(" to ILP")) + || message.contains("witness-capable") + { anyhow::anyhow!( "{message}\n\nHint: try `--solver brute-force` for direct exhaustive search on small instances." ) From e0dccdae395eec89ff5dfb07f01c0f73b1ede1c5 Mon Sep 17 00:00:00 2001 From: GiggleLiu Date: Mon, 23 Mar 2026 16:04:04 +0800 Subject: [PATCH 10/18] fix: migrate new models to aggregation trait hierarchy Adapt ConsecutiveOnesMatrixAugmentation, PartialFeedbackEdgeSet, and GroupingBySwapping (merged from main) to the new Problem trait: - type Metric -> type Value - SatisfactionProblem -> WitnessProblem - Remove unused Solver imports from tests Co-Authored-By: Claude Opus 4.6 (1M context) --- .../algebraic/consecutive_ones_matrix_augmentation.rs | 6 +++--- src/models/graph/partial_feedback_edge_set.rs | 9 +++------ src/models/misc/grouping_by_swapping.rs | 6 +++--- .../algebraic/consecutive_ones_matrix_augmentation.rs | 2 +- src/unit_tests/models/graph/partial_feedback_edge_set.rs | 2 +- src/unit_tests/models/misc/grouping_by_swapping.rs | 2 +- 6 files changed, 12 insertions(+), 15 deletions(-) diff --git a/src/models/algebraic/consecutive_ones_matrix_augmentation.rs b/src/models/algebraic/consecutive_ones_matrix_augmentation.rs index aa5d187a0..4ce637e4b 100644 --- a/src/models/algebraic/consecutive_ones_matrix_augmentation.rs +++ b/src/models/algebraic/consecutive_ones_matrix_augmentation.rs @@ -5,7 +5,7 @@ //! augmentations such that every row has consecutive 1s. use crate::registry::{FieldInfo, ProblemSchemaEntry}; -use crate::traits::{Problem, SatisfactionProblem}; +use crate::traits::{Problem, WitnessProblem}; use serde::{Deserialize, Serialize}; inventory::submit! { @@ -114,7 +114,7 @@ impl ConsecutiveOnesMatrixAugmentation { impl Problem for ConsecutiveOnesMatrixAugmentation { const NAME: &'static str = "ConsecutiveOnesMatrixAugmentation"; - type Metric = bool; + type Value = bool; fn dims(&self) -> Vec { vec![self.num_cols(); self.num_cols()] @@ -134,7 +134,7 @@ impl Problem for ConsecutiveOnesMatrixAugmentation { } } -impl SatisfactionProblem for ConsecutiveOnesMatrixAugmentation {} +impl WitnessProblem for ConsecutiveOnesMatrixAugmentation {} crate::declare_variants! { default sat ConsecutiveOnesMatrixAugmentation => "factorial(num_cols) * num_rows * num_cols", diff --git a/src/models/graph/partial_feedback_edge_set.rs b/src/models/graph/partial_feedback_edge_set.rs index 03054578b..9e61941ef 100644 --- a/src/models/graph/partial_feedback_edge_set.rs +++ b/src/models/graph/partial_feedback_edge_set.rs @@ -5,7 +5,7 @@ use crate::registry::{FieldInfo, ProblemSchemaEntry, VariantDimension}; use crate::topology::{Graph, SimpleGraph}; -use crate::traits::{Problem, SatisfactionProblem}; +use crate::traits::{Problem, WitnessProblem}; use serde::{Deserialize, Serialize}; #[cfg(feature = "example-db")] use std::collections::BTreeSet; @@ -102,7 +102,7 @@ where G: Graph + crate::variant::VariantParam, { const NAME: &'static str = "PartialFeedbackEdgeSet"; - type Metric = bool; + type Value = bool; fn variant() -> Vec<(&'static str, &'static str)> { crate::variant_params![G] @@ -117,10 +117,7 @@ where } } -impl SatisfactionProblem for PartialFeedbackEdgeSet where - G: Graph + crate::variant::VariantParam -{ -} +impl WitnessProblem for PartialFeedbackEdgeSet where G: Graph + crate::variant::VariantParam {} fn has_cycle_with_length_at_most( graph: &G, diff --git a/src/models/misc/grouping_by_swapping.rs b/src/models/misc/grouping_by_swapping.rs index 34f9781be..9fd595db1 100644 --- a/src/models/misc/grouping_by_swapping.rs +++ b/src/models/misc/grouping_by_swapping.rs @@ -5,7 +5,7 @@ //! symbol appears in a single contiguous block. use crate::registry::{FieldInfo, ProblemSchemaEntry}; -use crate::traits::{Problem, SatisfactionProblem}; +use crate::traits::{Problem, WitnessProblem}; use serde::{Deserialize, Serialize}; inventory::submit! { @@ -141,7 +141,7 @@ impl GroupingBySwapping { impl Problem for GroupingBySwapping { const NAME: &'static str = "GroupingBySwapping"; - type Metric = bool; + type Value = bool; fn dims(&self) -> Vec { vec![self.string_len(); self.budget] @@ -157,7 +157,7 @@ impl Problem for GroupingBySwapping { } } -impl SatisfactionProblem for GroupingBySwapping {} +impl WitnessProblem for GroupingBySwapping {} crate::declare_variants! { default sat GroupingBySwapping => "string_len ^ budget", 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 4b59c2dc6..0f70c1038 100644 --- a/src/unit_tests/models/algebraic/consecutive_ones_matrix_augmentation.rs +++ b/src/unit_tests/models/algebraic/consecutive_ones_matrix_augmentation.rs @@ -1,5 +1,5 @@ use super::*; -use crate::solvers::{BruteForce, Solver}; +use crate::solvers::BruteForce; use crate::traits::Problem; fn issue_yes_matrix() -> Vec> { 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 b721095f6..81355127e 100644 --- a/src/unit_tests/models/graph/partial_feedback_edge_set.rs +++ b/src/unit_tests/models/graph/partial_feedback_edge_set.rs @@ -1,5 +1,5 @@ use super::*; -use crate::solvers::{BruteForce, Solver}; +use crate::solvers::BruteForce; use crate::topology::{Graph, SimpleGraph}; use crate::traits::Problem; diff --git a/src/unit_tests/models/misc/grouping_by_swapping.rs b/src/unit_tests/models/misc/grouping_by_swapping.rs index f3d2dfecc..0162db136 100644 --- a/src/unit_tests/models/misc/grouping_by_swapping.rs +++ b/src/unit_tests/models/misc/grouping_by_swapping.rs @@ -1,5 +1,5 @@ use super::*; -use crate::solvers::{BruteForce, Solver}; +use crate::solvers::BruteForce; use crate::traits::Problem; fn issue_yes_instance() -> GroupingBySwapping { From 47176b05aaac8103c6a20d43d9344bd49cba9b9f Mon Sep 17 00:00:00 2001 From: GiggleLiu Date: Mon, 23 Mar 2026 16:13:16 +0800 Subject: [PATCH 11/18] update skills --- .claude/CLAUDE.md | 47 +++++++++++++---------- .claude/skills/add-model/SKILL.md | 41 ++++++++++++-------- .claude/skills/add-rule/SKILL.md | 19 +++++++-- .claude/skills/final-review/SKILL.md | 2 +- .claude/skills/fix-issue/SKILL.md | 2 +- .claude/skills/review-structural/SKILL.md | 8 ++-- 6 files changed, 73 insertions(+), 46 deletions(-) diff --git a/.claude/CLAUDE.md b/.claude/CLAUDE.md index 8a86664ea..a17680376 100644 --- a/.claude/CLAUDE.md +++ b/.claude/CLAUDE.md @@ -87,9 +87,9 @@ make release V=x.y.z # Tag and push a new release (CI publishes to crates.io) - `misc/` - Unique input structures - 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/solvers/` - BruteForce solver, ILP solver (feature-gated). To check if a problem supports ILP solving (via reduction path), run `pred path ILP` -- `src/traits.rs` - `Problem`, `OptimizationProblem`, `SatisfactionProblem` traits -- `src/rules/traits.rs` - `ReduceTo`, `ReductionResult` traits +- `src/solvers/` - BruteForce solver for aggregate values plus witness recovery when supported, ILP solver (feature-gated, witness-only). To check if a problem supports ILP solving via a witness-capable reduction path, run `pred path ILP` +- `src/traits.rs` - `Problem`, `ObjectiveProblem`, `WitnessProblem` traits +- `src/rules/traits.rs` - `ReduceTo`, `ReduceToAggregate`, `ReductionResult`, `AggregateReductionResult` traits - `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]`) @@ -104,36 +104,41 @@ make release V=x.y.z # Tag and push a new release (CI publishes to crates.io) Problem (core trait — all problems must implement) │ ├── const NAME: &'static str // e.g., "MaximumIndependentSet" -├── type Metric: Clone // SolutionSize for optimization, bool for satisfaction +├── type Value: Clone // aggregate value: Max/Min/Sum/bool/Extremum/... ├── fn dims(&self) -> Vec // config space: [2, 2, 2] for 3 binary variables -├── fn evaluate(&self, config) -> Metric +├── fn evaluate(&self, config) -> Value ├── fn variant() -> Vec<(&str, &str)> // e.g., [("graph","SimpleGraph"), ("weight","i32")] ├── fn num_variables(&self) -> usize // default: dims().len() └── fn problem_type() -> ProblemType // catalog bridge: registry lookup by NAME -OptimizationProblem : Problem> (extension for optimization) +ObjectiveProblem : Problem (temporary compatibility trait for optimization-style models) │ -├── type Value: PartialOrd + Clone // inner objective type (i32, f64, etc.) -└── fn direction(&self) -> Direction // Maximize or Minimize +├── type Objective: PartialOrd + Clone // inner objective type (i32, f64, etc.) +└── fn direction(&self) -> ExtremumSense -SatisfactionProblem : Problem (marker trait for decision problems) +WitnessProblem : Problem (temporary compatibility trait for witness-oriented feasibility problems) ``` -**Satisfaction problems** (e.g., `Satisfiability`) use `Metric = bool` and implement `SatisfactionProblem`. +**Witness-capable objective problems** (e.g., `MaximumIndependentSet`) typically use `Value = Max` or `Min` and implement `ObjectiveProblem`. -**Optimization problems** (e.g., `MaximumIndependentSet`) use `Metric = SolutionSize` where: +**Witness-capable feasibility problems** (e.g., `Satisfiability`) typically use `Value = bool` and implement `WitnessProblem`. `bool` currently acts as an existential aggregate in solver code. + +**Aggregate-only problems** use fold values such as `Sum` or `And`; these solve to a value but have no representative witness configuration. + +Common aggregate wrappers live in `src/types.rs`: ```rust -enum SolutionSize { Valid(T), Invalid } // Invalid = infeasible config -enum Direction { Maximize, Minimize } +Max, Min, Sum, Or, And, Extremum, ExtremumSense ``` ### Key Patterns - `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 dynamic dispatch metadata — every entry must specify `opt` or `sat`, and one entry per problem may be marked `default` (see `src/models/graph/maximum_independent_set.rs`). Variable names in complexity strings are validated at compile time against actual getter methods. +- `declare_variants!` proc macro registers concrete type instantiations with best-known complexity and registry-backed load/serialize/value-solve/witness-solve metadata — the syntax still requires `opt` or `sat`, and one entry per problem may be marked `default` (see `src/models/graph/maximum_independent_set.rs`). Variable names in complexity strings are validated at compile time against actual getter methods. - Problems parameterized by graph type `G` and optionally weight type `W` (problem-dependent) -- `ReductionResult` provides `target_problem()` and `extract_solution()` -- `Solver::find_best()` → `Option>` for optimization problems; `Solver::find_satisfying()` → `Option>` for `Metric = bool` -- `BruteForce::find_all_best()` / `find_all_satisfying()` return `Vec>` for all optimal/satisfying solutions +- `Solver::solve()` computes the aggregate value for any `Problem` whose `Value` implements `Aggregate` +- `BruteForce::find_witness()` / `find_all_witnesses()` recover witnesses only when `P::Value::supports_witnesses()` +- Compatibility helpers remain: `Solver::find_best()` / `find_satisfying()` and `BruteForce::find_all_best()` / `find_all_satisfying()` delegate to the witness APIs for `ObjectiveProblem` / `WitnessProblem` +- `ReductionResult` provides `target_problem()` and `extract_solution()` for witness/config workflows; `AggregateReductionResult` provides `extract_value()` for aggregate/value workflows +- CLI-facing dynamic formatting preserves legacy `Valid(...)` / `Invalid` strings for `Max` / `Min` aggregates; value-only aggregates such as `Sum(56)` keep their aggregate wrapper names - Graph types: SimpleGraph, PlanarGraph, BipartiteGraph, UnitDiskGraph, KingsSubgraph, TriangularSubgraph - Weight types: `One` (unit weight marker), `i32`, `f64` — all implement `WeightElement` trait - `WeightElement` trait: `type Sum: NumericSize` + `fn to_sum(&self)` — converts weight to a summable numeric type @@ -170,10 +175,12 @@ Reduction graph nodes use variant key-value pairs from `Problem::variant()`: - Default variant ranking: `SimpleGraph`, `One`, `KN` are considered default values; variants with the most default values sort first - Nodes come exclusively from `#[reduction]` registrations; natural edges between same-name variants are inferred from the graph/weight subtype partial order - Each primitive reduction is determined by the exact `(source_variant, target_variant)` endpoint pair -- `#[reduction]` accepts only `overhead = { ... }` +- Reduction edges carry `EdgeCapabilities { witness, aggregate }`; graph search defaults to witness mode, and aggregate mode is available through `ReductionMode::Aggregate` +- `#[reduction]` accepts only `overhead = { ... }` and currently registers witness/config reductions; aggregate-only edges require manual `ReductionEntry` registration with `reduce_aggregate_fn` ### Extension Points - New models register dynamic load/serialize/brute-force dispatch through `declare_variants!` in the model file, not by adding manual match arms in the CLI +- Aggregate-only experiments may need manual registry wiring today because `declare_variants!` still uses transitional `opt` / `sat` syntax - Exact registry dispatch lives in `src/registry/`; alias resolution and partial/default variant resolution live in `problemreductions-cli/src/problem_name.rs` - `pred create` UX lives in `problemreductions-cli/src/commands/create.rs` - Canonical paper and CLI examples live in `src/example_db/model_builders.rs` and `src/example_db/rule_builders.rs` @@ -199,8 +206,8 @@ Reduction graph nodes use variant key-value pairs from `Problem::variant()`: **Reference implementations — read these first:** - **Reduction test:** `src/unit_tests/rules/minimumvertexcover_maximumindependentset.rs` — closed-loop pattern - **Model test:** `src/unit_tests/models/graph/maximum_independent_set.rs` — evaluation, serialization -- **Solver test:** `src/unit_tests/solvers/brute_force.rs` — `find_best` + `find_satisfying` -- **Trait definitions:** `src/traits.rs` (`Problem`, `OptimizationProblem`), `src/solvers/mod.rs` (`Solver`) +- **Solver test:** `src/unit_tests/solvers/brute_force.rs` — aggregate `solve()` plus witness recovery helpers +- **Trait definitions:** `src/traits.rs` (`Problem`, `ObjectiveProblem`, `WitnessProblem`), `src/solvers/mod.rs` (`Solver`) ### Coverage diff --git a/.claude/skills/add-model/SKILL.md b/.claude/skills/add-model/SKILL.md index 38fa0bad7..d0fc29607 100644 --- a/.claude/skills/add-model/SKILL.md +++ b/.claude/skills/add-model/SKILL.md @@ -17,16 +17,16 @@ 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** | Optimization (maximize/minimize) or satisfaction | Optimization (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** | What `dims()` returns | `vec![2; num_vertices]` for binary vertex selection | | 7 | **Feasibility check** | How to validate a configuration | "All selected vertices must be pairwise adjacent" | -| 8 | **Objective function** | How to compute the metric | "Sum of weights of selected vertices" | +| 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\|" | | 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 | Optimization: one optimal solution + optimal value. Satisfaction: 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. @@ -61,7 +61,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:** `src/traits.rs` (`Problem`, `OptimizationProblem`, `SatisfactionProblem`) +- **Trait definitions / aggregate types:** `src/traits.rs` (`Problem`, `ObjectiveProblem`, `WitnessProblem`), `src/types.rs` (`Max`, `Min`, `Sum`, `And`, `Extremum`) - **Registry dispatch boundary:** `src/registry/mod.rs`, `src/registry/variant.rs` - **CLI aliases:** `problemreductions-cli/src/problem_name.rs` - **CLI creation:** `problemreductions-cli/src/commands/create.rs` @@ -71,7 +71,8 @@ Read these first to understand the patterns: Before implementing, make sure the plan explicitly covers these items that structural review checks later: - `ProblemSchemaEntry` metadata is complete for the current schema shape (`display_name`, `aliases`, `dimensions`, and constructor-facing `fields`) -- `declare_variants!` is present with the correct `opt`/`sat` marker and exactly one `default` variant when multiple concrete variants exist +- `Problem::Value` uses the correct aggregate wrapper and any `ObjectiveProblem` / `WitnessProblem` compatibility marker is intentional +- `declare_variants!` is present with the current transitional `opt`/`sat` syntax and 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` - `docs/paper/reductions.typ` adds both the display-name dictionary entry and the `problem-def(...)` @@ -110,19 +111,21 @@ Create `src/models//.rs`: // 1. inventory::submit! for ProblemSchemaEntry // 2. Struct definition with #[derive(Debug, Clone, Serialize, Deserialize)] // 3. Constructor (new) + accessor methods -// 4. Problem trait impl (NAME, Metric, dims, evaluate, variant) -// 5. OptimizationProblem or SatisfactionProblem impl +// 4. Problem trait impl (NAME, Value, dims, evaluate, variant) +// 5. Optional ObjectiveProblem or WitnessProblem impl // 6. #[cfg(test)] #[path = "..."] mod tests; ``` Key decisions: - **Schema metadata:** `ProblemSchemaEntry` must reflect the current registry schema shape, including `display_name`, `aliases`, `dimensions`, and constructor-facing `fields` -- **Optimization problems:** `type Metric = SolutionSize`, implement `OptimizationProblem` with `direction()` -- **Satisfaction problems:** `type Metric = bool`, implement `SatisfactionProblem` (marker trait) +- **Objective problems:** use `type Value = Max<_>`, `Min<_>`, or `Extremum<_>` and implement `ObjectiveProblem` when the model should expose optimization-style witness helpers +- **Witness problems:** use `type Value = bool` and implement `WitnessProblem` for existential feasibility problems +- **Aggregate-only problems:** use a value-only aggregate such as `Sum<_>`, `And`, or a custom `Aggregate`; do not implement `ObjectiveProblem` / `WitnessProblem` unless witnesses are 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 check feasibility first, then compute objective +- **`evaluate()`:** must return the per-configuration aggregate value. For models with invalid configs, check feasibility first and return the appropriate invalid/false contribution - **`variant()`:** use the `variant_params!` macro — e.g., `crate::variant_params![G, W]` for `Problem`, or `crate::variant_params![]` for problems with no type parameters. Each type parameter must implement `VariantParam` (already done for standard types like `SimpleGraph`, `i32`, `One`). See `src/variant.rs`. +- **Solve surface:** `Solver::solve()` always computes the aggregate value. `pred solve problem.json` prints a `Solution` only when a witness exists; `pred solve bundle.json` and `--solver ilp` remain witness-only workflows ## Step 2.5: Register variant complexity @@ -136,8 +139,8 @@ crate::declare_variants! { ``` - Each entry must include an explicit solver kind: - - `opt` for optimization problems (`BruteForce::find_best`) - - `sat` for satisfaction problems (`BruteForce::find_satisfying`) + - `opt` for objective models + - `sat` for witness/feasibility models - Mark exactly one concrete variant `default` when the problem has multiple registered variants - The complexity string references the getter method names from Step 1.5 (e.g., `num_vertices`) — variable names are validated at compile time against actual getters, so typos cause compile errors - One entry per supported `(graph, weight)` combination @@ -146,6 +149,8 @@ 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 +Today `declare_variants!` still requires `opt` / `sat` syntax even though runtime solving is aggregate-based. Aggregate-only models are not first-class in that macro yet; coordinate before introducing one, and be prepared to wire `VariantEntry` manually if needed. + ## Step 3: Register the model Update these files to register the new problem type: @@ -160,7 +165,7 @@ The CLI now loads, serializes, and brute-force solves problems through the core 1. **Registry-backed dispatch comes from `declare_variants!`:** - Make sure every concrete variant you want the CLI to load is listed in `declare_variants!` - - Use the correct `opt`/`sat` marker per entry + - Use the current `opt`/`sat` marker syntax intentionally (`opt` for objective models, `sat` for witness models) - Mark the intended default variant with `default` when applicable 2. **`problemreductions-cli/src/problem_name.rs`:** @@ -200,9 +205,9 @@ 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: - **Creation/basic** — exercise constructor inputs, key accessors, `dims()` / `num_variables()`. -- **Evaluation** — valid and invalid configs so the feasibility boundary is explicit. -- **Direction** — verify optimization direction (optimization problems only). -- **Solver** — brute-force solver finds correct solutions (when the model is small enough). +- **Evaluation** — valid and invalid configs so the feasibility boundary or aggregate contribution is explicit. +- **Direction** — verify optimization direction (`ObjectiveProblem` models only). +- **Solver** — brute-force `solve()` returns the correct aggregate value; if witnesses are supported, verify `find_witness()` / `find_all_witnesses()` as well. - **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). @@ -280,9 +285,11 @@ Structural and quality review is handled by the `review-pipeline` stage, not her | Forgetting `inventory::submit!` | Every problem needs a `ProblemSchemaEntry` registration | | 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` | | 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 `declare_variants!` syntax | Every entry now needs `opt` or `sat`; one entry per problem may be marked `default` | +| Wrong compatibility marker | Implement `ObjectiveProblem` / `WitnessProblem` only when the model genuinely supports those witness semantics | +| Wrong `declare_variants!` syntax | Every entry still needs `opt` or `sat`; one entry per problem may be marked `default` | | Forgetting CLI alias | Must add lowercase entry in `problem_name.rs` `resolve_alias()` | | Inventing short aliases | Only use well-established literature abbreviations (MIS, SAT, TSP); do NOT invent new ones | | Forgetting CLI create | Must add creation handler in `commands/create.rs` and flags in `cli.rs` | diff --git a/.claude/skills/add-rule/SKILL.md b/.claude/skills/add-rule/SKILL.md index 96dfedb73..9bff1abcc 100644 --- a/.claude/skills/add-rule/SKILL.md +++ b/.claude/skills/add-rule/SKILL.md @@ -33,7 +33,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`, `ReductionResult`) +- **Traits:** `src/rules/traits.rs` (`ReduceTo`, `ReduceToAggregate`, `ReductionResult`, `AggregateReductionResult`) ## Step 1: Implement the reduction @@ -84,6 +84,8 @@ impl ReduceTo for SourceType { 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]`. +**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. + ## Step 2: Register in mod.rs Add to `src/rules/mod.rs`: @@ -98,7 +100,7 @@ Create `src/unit_tests/rules/_.rs`: ```rust // 1. Create source problem instance // 2. Reduce: let reduction = ReduceTo::::reduce_to(&source); -// 3. Solve target: solver.find_all_best(reduction.target_problem()) +// 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 ``` @@ -108,6 +110,11 @@ Additional recommended tests: - Edge cases (empty graph, single vertex, etc.) - Weight preservation (if applicable) +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(...)` + Link via `#[cfg(test)] #[path = "..."] mod tests;` at the bottom of the rule file. ## Step 4: Add canonical example to example_db @@ -190,7 +197,12 @@ Structural and quality review is handled by the `review-pipeline` stage, not her ## CLI Impact -Adding a reduction rule does NOT require CLI changes -- the reduction graph is auto-generated from `#[reduction]` macros and the CLI discovers paths dynamically. However, both source and target models must already be fully registered through their model files (`declare_variants!`), aliases as needed in `problem_name.rs`, and `pred create` support where applicable (see `add-model` skill). +Adding a witness-preserving reduction rule does NOT require CLI changes -- the reduction graph is auto-generated from `#[reduction]` macros and the CLI discovers paths dynamically. However, both source and target models must already be fully registered through their model files (`declare_variants!`), aliases as needed in `problem_name.rs`, and `pred create` support where applicable (see `add-model` skill). + +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 @@ -204,6 +216,7 @@ Adding a reduction rule does NOT require CLI changes -- the reduction graph is a | 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` | | 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 | diff --git a/.claude/skills/final-review/SKILL.md b/.claude/skills/final-review/SKILL.md index bcb34d1cf..885c41167 100644 --- a/.claude/skills/final-review/SKILL.md +++ b/.claude/skills/final-review/SKILL.md @@ -217,7 +217,7 @@ Verify the PR includes all required components: **For [Model] PRs:** - [ ] Model implementation (`src/models/...`) - [ ] Unit tests (`src/unit_tests/models/...`) -- [ ] `declare_variants!` macro with explicit `opt`/`sat` solver-kind markers and intended default variant +- [ ] Variant registration exists: usually `declare_variants!` with explicit `opt`/`sat` syntax and intended default variant, or justified manual `VariantEntry` wiring for aggregate-only work - [ ] Schema / registry entry for CLI-facing model creation (`ProblemSchemaEntry`) - [ ] `canonical_model_example_specs()` function in the model file (gated by `#[cfg(feature = "example-db")]`) and registered in the category `mod.rs` example chain - [ ] Paper section in `docs/paper/reductions.typ` (`problem-def` entry) diff --git a/.claude/skills/fix-issue/SKILL.md b/.claude/skills/fix-issue/SKILL.md index 0a2aa60f9..4dee06d39 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 optimization** unless evidence points otherwise. The project prefers `OptimizationProblem` (like MIS, SpinGlass, TSP) because optimization subsumes decision. Check associated `[Rule]` issues (`gh issue list --search " in:title label:rule"`) to see how rules use the model — if rules only need the decision version (e.g., reducing to SAT with a bound), optimization still works since you can extract the bound from the optimal value. Only use `SatisfactionProblem` for inherently decision/feasibility problems (SAT, KColoring) where there is no natural optimization objective. If switching to optimization, 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<_>` or `Min<_>` (or `Extremum<_>` when the sense is runtime data) plus `ObjectiveProblem`. 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 `WitnessProblem` with `type Value = bool` 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 60daf1f45..12fc48f6c 100644 --- a/.claude/skills/review-structural/SKILL.md +++ b/.claude/skills/review-structural/SKILL.md @@ -54,13 +54,13 @@ Only run if review type includes "model". Given: problem name `P`, category `C`, | 2 | `inventory::submit!` present | `Grep("inventory::submit", file)` | | 3 | `#[derive(...Serialize, Deserialize)]` on struct | `Grep("Serialize.*Deserialize", file)` | | 4 | `Problem` trait impl | `Grep("impl.*Problem for.*{P}", file)` | -| 5 | `OptimizationProblem` or `SatisfactionProblem` impl | `Grep("(OptimizationProblem|SatisfactionProblem).*for.*{P}", file)` | +| 5 | Aggregate value and compatibility markers are present | `Grep("type Value =|(ObjectiveProblem|WitnessProblem).*for.*{P}", 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 | | 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 | `declare_variants!` entry exists | `Grep("declare_variants!|default opt|default sat|opt {P}|sat {P}", file)` | +| 11 | Variant registration exists | `Grep("declare_variants!|default opt|default sat|opt {P}|sat {P}|VariantEntry", file)` | | 12 | CLI `resolve_alias` entry | `Grep("{P}", "problemreductions-cli/src/problem_name.rs")` | | 13 | CLI `create` support | `Grep('"{P}"', "problemreductions-cli/src/commands/create.rs")` | | 14 | Canonical model example registered | `Grep("{P}", "src/example_db/model_builders.rs")` | @@ -107,7 +107,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? Does it return `SolutionSize::Invalid` / `false` for infeasible configs? +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? @@ -127,7 +127,7 @@ Only if a linked issue was provided. |---|-------| | 1 | Problem name matches issue | | 2 | Mathematical definition matches | -| 3 | Problem type (opt/sat) matches | +| 3 | Problem framing (objective / witness / aggregate-only) matches | | 4 | Type parameters match | | 5 | Configuration space matches | | 6 | Feasibility check matches | From 675a4d98370fa250867f9d5d1758d47656be6923 Mon Sep 17 00:00:00 2001 From: GiggleLiu Date: Mon, 23 Mar 2026 17:04:39 +0800 Subject: [PATCH 12/18] clean up --- .claude/CLAUDE.md | 24 ++-- .claude/skills/add-model/SKILL.md | 31 ++-- .claude/skills/final-review/SKILL.md | 2 +- .claude/skills/fix-issue/SKILL.md | 2 +- .claude/skills/review-structural/SKILL.md | 4 +- benches/solver_benchmarks.rs | 26 ++-- problemreductions-cli/src/commands/create.rs | 25 +++- problemreductions-macros/src/lib.rs | 119 +++------------- src/lib.rs | 6 +- src/models/algebraic/bmf.rs | 14 +- .../algebraic/closest_vector_problem.rs | 25 +--- .../consecutive_block_minimization.rs | 20 +-- .../consecutive_ones_matrix_augmentation.rs | 16 +-- .../algebraic/consecutive_ones_submatrix.rs | 46 +++--- src/models/algebraic/ilp.rs | 19 +-- src/models/algebraic/quadratic_assignment.rs | 14 +- src/models/algebraic/qubo.rs | 24 +--- .../algebraic/sparse_matrix_compression.rs | 12 +- src/models/formula/circuit.rs | 12 +- src/models/formula/ksat.rs | 20 +-- src/models/formula/nae_satisfiability.rs | 18 +-- src/models/formula/qbf.rs | 20 +-- src/models/formula/sat.rs | 18 +-- src/models/graph/acyclic_partition.rs | 28 ++-- .../balanced_complete_bipartite_subgraph.rs | 34 ++--- src/models/graph/biclique_cover.rs | 14 +- .../graph/biconnectivity_augmentation.rs | 21 ++- .../graph/bottleneck_traveling_salesman.rs | 14 +- .../bounded_component_spanning_forest.rs | 17 +-- .../directed_two_commodity_integral_flow.rs | 12 +- src/models/graph/disjoint_connecting_paths.rs | 12 +- src/models/graph/generalized_hex.rs | 28 ++-- src/models/graph/graph_partitioning.rs | 17 +-- src/models/graph/hamiltonian_circuit.rs | 12 +- src/models/graph/hamiltonian_path.rs | 12 +- src/models/graph/integral_flow_bundles.rs | 80 +++++------ .../graph/integral_flow_homologous_arcs.rs | 72 +++++----- .../graph/integral_flow_with_multipliers.rs | 12 +- src/models/graph/isomorphic_spanning_tree.rs | 48 +++---- src/models/graph/kclique.rs | 12 +- src/models/graph/kcoloring.rs | 20 ++- src/models/graph/kth_best_spanning_tree.rs | 15 +- .../graph/length_bounded_disjoint_paths.rs | 12 +- src/models/graph/longest_circuit.rs | 17 +-- src/models/graph/longest_path.rs | 20 +-- src/models/graph/max_cut.rs | 18 +-- src/models/graph/maximal_is.rs | 18 +-- src/models/graph/maximum_clique.rs | 18 +-- src/models/graph/maximum_independent_set.rs | 30 ++-- src/models/graph/maximum_matching.rs | 18 +-- src/models/graph/min_max_multicenter.rs | 64 ++++----- .../graph/minimum_cut_into_bounded_sets.rs | 67 ++++----- src/models/graph/minimum_dominating_set.rs | 18 +-- .../graph/minimum_dummy_activities_pert.rs | 14 +- src/models/graph/minimum_feedback_arc_set.rs | 17 +-- .../graph/minimum_feedback_vertex_set.rs | 17 +-- src/models/graph/minimum_multiway_cut.rs | 18 +-- src/models/graph/minimum_sum_multicenter.rs | 18 +-- src/models/graph/minimum_vertex_cover.rs | 18 +-- src/models/graph/mixed_chinese_postman.rs | 67 +++++---- src/models/graph/multiple_choice_branching.rs | 29 ++-- .../graph/multiple_copy_file_allocation.rs | 12 +- .../graph/optimal_linear_arrangement.rs | 12 +- src/models/graph/partial_feedback_edge_set.rs | 12 +- .../graph/partition_into_paths_of_length_2.rs | 12 +- src/models/graph/partition_into_triangles.rs | 100 ++++++------- .../graph/path_constrained_network_flow.rs | 12 +- src/models/graph/rooted_tree_arrangement.rs | 12 +- src/models/graph/rural_postman.rs | 17 +-- .../graph/shortest_weight_constrained_path.rs | 17 +-- src/models/graph/spin_glass.rs | 28 +--- src/models/graph/steiner_tree.rs | 20 +-- src/models/graph/steiner_tree_in_graphs.rs | 20 +-- .../graph/strong_connectivity_augmentation.rs | 15 +- src/models/graph/subgraph_isomorphism.rs | 66 ++++----- src/models/graph/traveling_salesman.rs | 18 +-- .../graph/undirected_flow_lower_bounds.rs | 14 +- .../undirected_two_commodity_integral_flow.rs | 104 +++++++------- src/models/misc/additional_key.rs | 94 ++++++------- src/models/misc/bin_packing.rs | 20 +-- .../misc/boyce_codd_normal_form_violation.rs | 54 +++---- src/models/misc/capacity_assignment.rs | 20 +-- src/models/misc/conjunctive_boolean_query.rs | 42 +++--- .../misc/conjunctive_query_foldability.rs | 57 ++++---- ...onsistency_of_database_frequency_tables.rs | 66 ++++----- src/models/misc/ensemble_computation.rs | 68 ++++----- src/models/misc/expected_retrieval_cost.rs | 12 +- src/models/misc/factoring.rs | 14 +- src/models/misc/flow_shop_scheduling.rs | 42 +++--- src/models/misc/grouping_by_swapping.rs | 16 +-- src/models/misc/knapsack.rs | 14 +- src/models/misc/longest_common_subsequence.rs | 26 ++-- .../misc/minimum_tardiness_sequencing.rs | 14 +- src/models/misc/multiprocessor_scheduling.rs | 36 ++--- src/models/misc/paintshop.rs | 14 +- src/models/misc/partially_ordered_knapsack.rs | 14 +- src/models/misc/partition.rs | 38 ++--- .../misc/precedence_constrained_scheduling.rs | 52 +++---- .../misc/rectilinear_picture_compression.rs | 70 +++++----- .../misc/resource_constrained_scheduling.rs | 86 ++++++------ .../scheduling_with_individual_deadlines.rs | 50 +++---- ...ing_to_minimize_maximum_cumulative_cost.rs | 48 +++---- ...ng_to_minimize_weighted_completion_time.rs | 14 +- ...quencing_to_minimize_weighted_tardiness.rs | 16 +-- ...encing_with_release_times_and_deadlines.rs | 56 ++++---- .../misc/sequencing_within_intervals.rs | 63 +++++---- .../misc/shortest_common_supersequence.rs | 26 ++-- src/models/misc/stacker_crane.rs | 14 +- src/models/misc/staff_scheduling.rs | 24 ++-- .../misc/string_to_string_correction.rs | 74 +++++----- src/models/misc/subset_sum.rs | 36 ++--- src/models/misc/sum_of_squares_partition.rs | 20 +-- src/models/misc/timetable_design.rs | 72 +++++----- src/models/set/comparative_containment.rs | 19 +-- src/models/set/consecutive_sets.rs | 132 +++++++++--------- src/models/set/exact_cover_by_3_sets.rs | 56 ++++---- src/models/set/maximum_set_packing.rs | 21 +-- src/models/set/minimum_cardinality_key.rs | 30 ++-- src/models/set/minimum_hitting_set.rs | 14 +- src/models/set/minimum_set_covering.rs | 17 +-- src/models/set/prime_attribute_name.rs | 64 ++++----- .../set/rooted_tree_storage_assignment.rs | 56 ++++---- src/models/set/set_basis.rs | 26 ++-- .../set/two_dimensional_consecutive_sets.rs | 82 +++++------ src/rules/test_helpers.rs | 104 +++++++------- src/solvers/brute_force.rs | 54 +------ src/solvers/mod.rs | 14 +- src/traits.rs | 11 -- src/types.rs | 38 +++-- src/unit_tests/graph_models.rs | 10 +- src/unit_tests/models/algebraic/bmf.rs | 15 +- .../algebraic/closest_vector_problem.rs | 13 +- .../consecutive_ones_matrix_augmentation.rs | 2 +- src/unit_tests/models/algebraic/ilp.rs | 27 +--- .../models/algebraic/quadratic_assignment.rs | 10 +- src/unit_tests/models/algebraic/qubo.rs | 10 +- src/unit_tests/models/graph/biclique_cover.rs | 15 +- .../graph/bottleneck_traveling_salesman.rs | 14 +- .../models/graph/graph_partitioning.rs | 9 +- .../graph/integral_flow_homologous_arcs.rs | 4 +- src/unit_tests/models/graph/kcoloring.rs | 2 +- .../models/graph/kth_best_spanning_tree.rs | 4 +- .../graph/length_bounded_disjoint_paths.rs | 2 +- src/unit_tests/models/graph/longest_path.rs | 5 +- src/unit_tests/models/graph/max_cut.rs | 9 -- src/unit_tests/models/graph/maximal_is.rs | 9 -- src/unit_tests/models/graph/maximum_clique.rs | 13 +- .../models/graph/maximum_independent_set.rs | 9 +- .../models/graph/maximum_matching.rs | 10 +- .../graph/minimum_cut_into_bounded_sets.rs | 9 +- .../models/graph/minimum_dominating_set.rs | 9 +- .../graph/minimum_dummy_activities_pert.rs | 5 +- .../models/graph/minimum_feedback_arc_set.rs | 10 +- .../graph/minimum_feedback_vertex_set.rs | 10 +- .../models/graph/minimum_multiway_cut.rs | 11 +- .../models/graph/minimum_sum_multicenter.rs | 10 +- .../models/graph/minimum_vertex_cover.rs | 9 +- .../models/graph/partial_feedback_edge_set.rs | 6 +- .../graph/path_constrained_network_flow.rs | 2 +- src/unit_tests/models/graph/rural_postman.rs | 2 +- src/unit_tests/models/graph/spin_glass.rs | 9 +- src/unit_tests/models/graph/steiner_tree.rs | 13 +- .../models/graph/steiner_tree_in_graphs.rs | 10 +- .../models/graph/traveling_salesman.rs | 13 +- src/unit_tests/models/misc/bin_packing.rs | 9 +- src/unit_tests/models/misc/factoring.rs | 9 +- .../models/misc/flow_shop_scheduling.rs | 2 +- .../models/misc/grouping_by_swapping.rs | 12 +- src/unit_tests/models/misc/knapsack.rs | 4 +- .../models/misc/longest_common_subsequence.rs | 2 +- .../misc/minimum_tardiness_sequencing.rs | 4 +- .../models/misc/multiprocessor_scheduling.rs | 2 +- src/unit_tests/models/misc/paintshop.rs | 9 +- .../models/misc/partially_ordered_knapsack.rs | 4 +- .../scheduling_with_individual_deadlines.rs | 2 +- ...ng_to_minimize_weighted_completion_time.rs | 5 +- .../misc/sequencing_within_intervals.rs | 2 +- .../misc/shortest_common_supersequence.rs | 2 +- .../models/set/maximum_set_packing.rs | 10 +- .../models/set/minimum_cardinality_key.rs | 4 +- .../models/set/minimum_hitting_set.rs | 10 +- .../models/set/minimum_set_covering.rs | 10 +- src/unit_tests/models/set/set_basis.rs | 4 +- src/unit_tests/reduction_graph.rs | 109 ++++++++------- src/unit_tests/registry/dispatch.rs | 8 +- src/unit_tests/rules/coloring_ilp.rs | 2 +- src/unit_tests/rules/graph.rs | 26 ---- src/unit_tests/rules/registry.rs | 30 ++++ src/unit_tests/rules/sat_coloring.rs | 2 +- src/unit_tests/rules/sat_ksat.rs | 8 +- src/unit_tests/solvers/brute_force.rs | 34 +++++ src/unit_tests/trait_consistency.rs | 77 ---------- src/unit_tests/types.rs | 105 ++++++++++++-- tests/suites/integration.rs | 16 +-- 194 files changed, 2065 insertions(+), 2884 deletions(-) diff --git a/.claude/CLAUDE.md b/.claude/CLAUDE.md index a17680376..3212b9fbd 100644 --- a/.claude/CLAUDE.md +++ b/.claude/CLAUDE.md @@ -88,7 +88,7 @@ make release V=x.y.z # Tag and push a new release (CI publishes to crates.io) - 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/solvers/` - BruteForce solver for aggregate values plus witness recovery when supported, ILP solver (feature-gated, witness-only). To check if a problem supports ILP solving via a witness-capable reduction path, run `pred path ILP` -- `src/traits.rs` - `Problem`, `ObjectiveProblem`, `WitnessProblem` traits +- `src/traits.rs` - `Problem` trait - `src/rules/traits.rs` - `ReduceTo`, `ReduceToAggregate`, `ReductionResult`, `AggregateReductionResult` traits - `src/registry/` - Compile-time reduction metadata collection - `problemreductions-cli/` - `pred` CLI tool (separate crate in workspace) @@ -104,24 +104,17 @@ make release V=x.y.z # Tag and push a new release (CI publishes to crates.io) Problem (core trait — all problems must implement) │ ├── const NAME: &'static str // e.g., "MaximumIndependentSet" -├── type Value: Clone // aggregate value: Max/Min/Sum/bool/Extremum/... +├── type Value: Clone // aggregate value: Max/Min/Sum/Or/And/Extremum/... ├── fn dims(&self) -> Vec // config space: [2, 2, 2] for 3 binary variables ├── fn evaluate(&self, config) -> Value ├── fn variant() -> Vec<(&str, &str)> // e.g., [("graph","SimpleGraph"), ("weight","i32")] ├── fn num_variables(&self) -> usize // default: dims().len() └── fn problem_type() -> ProblemType // catalog bridge: registry lookup by NAME - -ObjectiveProblem : Problem (temporary compatibility trait for optimization-style models) -│ -├── type Objective: PartialOrd + Clone // inner objective type (i32, f64, etc.) -└── fn direction(&self) -> ExtremumSense - -WitnessProblem : Problem (temporary compatibility trait for witness-oriented feasibility problems) ``` -**Witness-capable objective problems** (e.g., `MaximumIndependentSet`) typically use `Value = Max` or `Min` and implement `ObjectiveProblem`. +**Witness-capable objective problems** (e.g., `MaximumIndependentSet`) typically use `Value = Max`, `Min`, or `Extremum`. -**Witness-capable feasibility problems** (e.g., `Satisfiability`) typically use `Value = bool` and implement `WitnessProblem`. `bool` currently acts as an existential aggregate in solver code. +**Witness-capable feasibility problems** (e.g., `Satisfiability`) typically use `Value = Or`. **Aggregate-only problems** use fold values such as `Sum` or `And`; these solve to a value but have no representative witness configuration. @@ -132,13 +125,12 @@ Max, Min, Sum, Or, And, Extremum, ExtremumSense ### Key Patterns - `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/value-solve/witness-solve metadata — the syntax still requires `opt` or `sat`, and one entry per problem may be marked `default` (see `src/models/graph/maximum_independent_set.rs`). Variable names in complexity strings are validated at compile time against actual getter methods. +- `declare_variants!` proc macro registers concrete type instantiations with best-known complexity and registry-backed load/serialize/value-solve/witness-solve metadata. One entry per problem may be marked `default`, and variable names in complexity strings are validated at compile time against actual getter methods. - Problems parameterized by graph type `G` and optionally weight type `W` (problem-dependent) - `Solver::solve()` computes the aggregate value for any `Problem` whose `Value` implements `Aggregate` - `BruteForce::find_witness()` / `find_all_witnesses()` recover witnesses only when `P::Value::supports_witnesses()` -- Compatibility helpers remain: `Solver::find_best()` / `find_satisfying()` and `BruteForce::find_all_best()` / `find_all_satisfying()` delegate to the witness APIs for `ObjectiveProblem` / `WitnessProblem` - `ReductionResult` provides `target_problem()` and `extract_solution()` for witness/config workflows; `AggregateReductionResult` provides `extract_value()` for aggregate/value workflows -- CLI-facing dynamic formatting preserves legacy `Valid(...)` / `Invalid` strings for `Max` / `Min` aggregates; value-only aggregates such as `Sum(56)` keep their aggregate wrapper names +- CLI-facing dynamic formatting preserves legacy `Valid(...)` / `Invalid` strings for `Max` / `Min` aggregates; other aggregates keep their wrapper names (for example `Or(true)` or `Sum(56)`) - Graph types: SimpleGraph, PlanarGraph, BipartiteGraph, UnitDiskGraph, KingsSubgraph, TriangularSubgraph - Weight types: `One` (unit weight marker), `i32`, `f64` — all implement `WeightElement` trait - `WeightElement` trait: `type Sum: NumericSize` + `fn to_sum(&self)` — converts weight to a summable numeric type @@ -180,7 +172,7 @@ Reduction graph nodes use variant key-value pairs from `Problem::variant()`: ### Extension Points - New models register dynamic load/serialize/brute-force dispatch through `declare_variants!` in the model file, not by adding manual match arms in the CLI -- Aggregate-only experiments may need manual registry wiring today because `declare_variants!` still uses transitional `opt` / `sat` syntax +- Aggregate-only models are first-class in `declare_variants!`; aggregate-only reduction edges still need manual `ReductionEntry` wiring because `#[reduction]` only registers witness/config reductions today - Exact registry dispatch lives in `src/registry/`; alias resolution and partial/default variant resolution live in `problemreductions-cli/src/problem_name.rs` - `pred create` UX lives in `problemreductions-cli/src/commands/create.rs` - Canonical paper and CLI examples live in `src/example_db/model_builders.rs` and `src/example_db/rule_builders.rs` @@ -207,7 +199,7 @@ Reduction graph nodes use variant key-value pairs from `Problem::variant()`: - **Reduction test:** `src/unit_tests/rules/minimumvertexcover_maximumindependentset.rs` — closed-loop pattern - **Model test:** `src/unit_tests/models/graph/maximum_independent_set.rs` — evaluation, serialization - **Solver test:** `src/unit_tests/solvers/brute_force.rs` — aggregate `solve()` plus witness recovery helpers -- **Trait definitions:** `src/traits.rs` (`Problem`, `ObjectiveProblem`, `WitnessProblem`), `src/solvers/mod.rs` (`Solver`) +- **Trait definitions:** `src/traits.rs` (`Problem`), `src/solvers/mod.rs` (`Solver`) ### Coverage diff --git a/.claude/skills/add-model/SKILL.md b/.claude/skills/add-model/SKILL.md index d0fc29607..960b8b821 100644 --- a/.claude/skills/add-model/SKILL.md +++ b/.claude/skills/add-model/SKILL.md @@ -61,7 +61,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`, `ObjectiveProblem`, `WitnessProblem`), `src/types.rs` (`Max`, `Min`, `Sum`, `And`, `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 aliases:** `problemreductions-cli/src/problem_name.rs` - **CLI creation:** `problemreductions-cli/src/commands/create.rs` @@ -71,8 +71,8 @@ Read these first to understand the patterns: Before implementing, make sure the plan explicitly covers these items that structural review checks later: - `ProblemSchemaEntry` metadata is complete for the current schema shape (`display_name`, `aliases`, `dimensions`, and constructor-facing `fields`) -- `Problem::Value` uses the correct aggregate wrapper and any `ObjectiveProblem` / `WitnessProblem` compatibility marker is intentional -- `declare_variants!` is present with the current transitional `opt`/`sat` syntax and exactly one `default` variant when multiple concrete variants exist +- `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` - `docs/paper/reductions.typ` adds both the display-name dictionary entry and the `problem-def(...)` @@ -112,15 +112,14 @@ Create `src/models//.rs`: // 2. Struct definition with #[derive(Debug, Clone, Serialize, Deserialize)] // 3. Constructor (new) + accessor methods // 4. Problem trait impl (NAME, Value, dims, evaluate, variant) -// 5. Optional ObjectiveProblem or WitnessProblem impl -// 6. #[cfg(test)] #[path = "..."] mod tests; +// 5. #[cfg(test)] #[path = "..."] mod tests; ``` Key decisions: - **Schema metadata:** `ProblemSchemaEntry` must reflect the current registry schema shape, including `display_name`, `aliases`, `dimensions`, and constructor-facing `fields` -- **Objective problems:** use `type Value = Max<_>`, `Min<_>`, or `Extremum<_>` and implement `ObjectiveProblem` when the model should expose optimization-style witness helpers -- **Witness problems:** use `type Value = bool` and implement `WitnessProblem` for existential feasibility problems -- **Aggregate-only problems:** use a value-only aggregate such as `Sum<_>`, `And`, or a custom `Aggregate`; do not implement `ObjectiveProblem` / `WitnessProblem` unless witnesses are meaningful +- **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 - **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 the per-configuration aggregate value. For models with invalid configs, check feasibility first and return the appropriate invalid/false contribution @@ -133,14 +132,11 @@ Add `declare_variants!` at the bottom of the model file (after the trait impls, ```rust crate::declare_variants! { - opt ProblemName => "1.1996^num_vertices", - default opt ProblemName => "1.1996^num_vertices", + ProblemName => "1.1996^num_vertices", + default ProblemName => "1.1996^num_vertices", } ``` -- Each entry must include an explicit solver kind: - - `opt` for objective models - - `sat` for witness/feasibility models - Mark exactly one concrete variant `default` when the problem has multiple registered variants - The complexity string references the getter method names from Step 1.5 (e.g., `num_vertices`) — variable names are validated at compile time against actual getters, so typos cause compile errors - One entry per supported `(graph, weight)` combination @@ -149,7 +145,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 -Today `declare_variants!` still requires `opt` / `sat` syntax even though runtime solving is aggregate-based. Aggregate-only models are not first-class in that macro yet; coordinate before introducing one, and be prepared to wire `VariantEntry` manually if needed. +`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 @@ -165,7 +161,6 @@ The CLI now loads, serializes, and brute-force solves problems through the core 1. **Registry-backed dispatch comes from `declare_variants!`:** - Make sure every concrete variant you want the CLI to load is listed in `declare_variants!` - - Use the current `opt`/`sat` marker syntax intentionally (`opt` for objective models, `sat` for witness models) - Mark the intended default variant with `default` when applicable 2. **`problemreductions-cli/src/problem_name.rs`:** @@ -206,7 +201,7 @@ Every model needs **at least 3 test functions** (the structural reviewer enforce - **Creation/basic** — exercise constructor inputs, key accessors, `dims()` / `num_variables()`. - **Evaluation** — valid and invalid configs so the feasibility boundary or aggregate contribution is explicit. -- **Direction** — verify optimization direction (`ObjectiveProblem` models only). +- **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. - **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). @@ -288,8 +283,8 @@ Structural and quality review is handled by the `review-pipeline` stage, not her | 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 compatibility marker | Implement `ObjectiveProblem` / `WitnessProblem` only when the model genuinely supports those witness semantics | -| Wrong `declare_variants!` syntax | Every entry still needs `opt` or `sat`; one entry per problem may be marked `default` | +| 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` | | Forgetting CLI alias | Must add lowercase entry in `problem_name.rs` `resolve_alias()` | | Inventing short aliases | Only use well-established literature abbreviations (MIS, SAT, TSP); do NOT invent new ones | | Forgetting CLI create | Must add creation handler in `commands/create.rs` and flags in `cli.rs` | diff --git a/.claude/skills/final-review/SKILL.md b/.claude/skills/final-review/SKILL.md index 885c41167..633d86ad8 100644 --- a/.claude/skills/final-review/SKILL.md +++ b/.claude/skills/final-review/SKILL.md @@ -217,7 +217,7 @@ Verify the PR includes all required components: **For [Model] PRs:** - [ ] Model implementation (`src/models/...`) - [ ] Unit tests (`src/unit_tests/models/...`) -- [ ] Variant registration exists: usually `declare_variants!` with explicit `opt`/`sat` syntax and intended default variant, or justified manual `VariantEntry` wiring for aggregate-only work +- [ ] Variant registration exists: usually `declare_variants!` with the intended default variant, or justified manual `VariantEntry` wiring for unusual dynamic-registration work - [ ] Schema / registry entry for CLI-facing model creation (`ProblemSchemaEntry`) - [ ] `canonical_model_example_specs()` function in the model file (gated by `#[cfg(feature = "example-db")]`) and registered in the category `mod.rs` example chain - [ ] Paper section in `docs/paper/reductions.typ` (`problem-def` entry) diff --git a/.claude/skills/fix-issue/SKILL.md b/.claude/skills/fix-issue/SKILL.md index 4dee06d39..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<_>` or `Min<_>` (or `Extremum<_>` when the sense is runtime data) plus `ObjectiveProblem`. 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 `WitnessProblem` with `type Value = bool` 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. 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 12fc48f6c..c169ebcbd 100644 --- a/.claude/skills/review-structural/SKILL.md +++ b/.claude/skills/review-structural/SKILL.md @@ -54,13 +54,13 @@ Only run if review type includes "model". Given: problem name `P`, category `C`, | 2 | `inventory::submit!` present | `Grep("inventory::submit", file)` | | 3 | `#[derive(...Serialize, Deserialize)]` on struct | `Grep("Serialize.*Deserialize", file)` | | 4 | `Problem` trait impl | `Grep("impl.*Problem for.*{P}", file)` | -| 5 | Aggregate value and compatibility markers are present | `Grep("type Value =|(ObjectiveProblem|WitnessProblem).*for.*{P}", file)` | +| 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 | | 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!|default opt|default sat|opt {P}|sat {P}|VariantEntry", file)` | +| 11 | Variant registration exists | `Grep("declare_variants!|VariantEntry", file)` | | 12 | CLI `resolve_alias` entry | `Grep("{P}", "problemreductions-cli/src/problem_name.rs")` | | 13 | CLI `create` support | `Grep('"{P}"', "problemreductions-cli/src/commands/create.rs")` | | 14 | Canonical model example registered | `Grep("{P}", "src/example_db/model_builders.rs")` | diff --git a/benches/solver_benchmarks.rs b/benches/solver_benchmarks.rs index 0ee082fc3..69572e1d5 100644 --- a/benches/solver_benchmarks.rs +++ b/benches/solver_benchmarks.rs @@ -21,7 +21,7 @@ fn bench_independent_set(c: &mut Criterion) { let solver = BruteForce::new(); group.bench_with_input(BenchmarkId::new("path", n), n, |b, _| { - b.iter(|| solver.find_best(black_box(&problem))) + b.iter(|| solver.find_witness(black_box(&problem))) }); } @@ -38,7 +38,7 @@ fn bench_vertex_covering(c: &mut Criterion) { let solver = BruteForce::new(); group.bench_with_input(BenchmarkId::new("path", n), n, |b, _| { - b.iter(|| solver.find_best(black_box(&problem))) + b.iter(|| solver.find_witness(black_box(&problem))) }); } @@ -56,7 +56,7 @@ fn bench_max_cut(c: &mut Criterion) { let solver = BruteForce::new(); group.bench_with_input(BenchmarkId::new("path", n), n, |b, _| { - b.iter(|| solver.find_best(black_box(&problem))) + b.iter(|| solver.find_witness(black_box(&problem))) }); } @@ -83,7 +83,7 @@ fn bench_satisfiability(c: &mut Criterion) { let solver = BruteForce::new(); group.bench_with_input(BenchmarkId::new("3-sat", num_vars), num_vars, |b, _| { - b.iter(|| solver.find_all_satisfying(black_box(&problem))) + b.iter(|| solver.find_all_witnesses(black_box(&problem))) }); } @@ -104,7 +104,7 @@ fn bench_spin_glass(c: &mut Criterion) { let solver = BruteForce::new(); group.bench_with_input(BenchmarkId::new("chain", n), n, |b, _| { - b.iter(|| solver.find_best(black_box(&problem))) + b.iter(|| solver.find_witness(black_box(&problem))) }); } @@ -126,7 +126,7 @@ fn bench_set_covering(c: &mut Criterion) { group.bench_with_input( BenchmarkId::new("overlapping", num_sets), num_sets, - |b, _| b.iter(|| solver.find_best(black_box(&problem))), + |b, _| b.iter(|| solver.find_witness(black_box(&problem))), ); } @@ -143,7 +143,7 @@ fn bench_coloring(c: &mut Criterion) { let solver = BruteForce::new(); group.bench_with_input(BenchmarkId::new("path_3colors", n), n, |b, _| { - b.iter(|| solver.find_all_satisfying(black_box(&problem))) + b.iter(|| solver.find_all_witnesses(black_box(&problem))) }); } @@ -161,7 +161,7 @@ fn bench_matching(c: &mut Criterion) { let solver = BruteForce::new(); group.bench_with_input(BenchmarkId::new("path", n), n, |b, _| { - b.iter(|| solver.find_best(black_box(&problem))) + b.iter(|| solver.find_witness(black_box(&problem))) }); } @@ -182,7 +182,7 @@ fn bench_paintshop(c: &mut Criterion) { let solver = BruteForce::new(); group.bench_with_input(BenchmarkId::new("sequential", n), n, |b, _| { - b.iter(|| solver.find_best(black_box(&problem))) + b.iter(|| solver.find_witness(black_box(&problem))) }); } @@ -201,7 +201,7 @@ fn bench_comparison(c: &mut Criterion) { vec![1i32; 8], ); group.bench_function("MaximumIndependentSet", |b| { - b.iter(|| solver.find_best(black_box(&is_problem))) + b.iter(|| solver.find_witness(black_box(&is_problem))) }); // SAT with 8 variables @@ -215,7 +215,7 @@ fn bench_comparison(c: &mut Criterion) { ], ); group.bench_function("Satisfiability", |b| { - b.iter(|| solver.find_all_satisfying(black_box(&sat_problem))) + b.iter(|| solver.find_all_witnesses(black_box(&sat_problem))) }); // SpinGlass with 8 spins @@ -225,7 +225,7 @@ fn bench_comparison(c: &mut Criterion) { vec![0.0; 8], ); group.bench_function("SpinGlass", |b| { - b.iter(|| solver.find_best(black_box(&sg_problem))) + b.iter(|| solver.find_witness(black_box(&sg_problem))) }); // MaxCut with 8 vertices @@ -234,7 +234,7 @@ fn bench_comparison(c: &mut Criterion) { vec![1, 1, 1, 1], ); group.bench_function("MaxCut", |b| { - b.iter(|| solver.find_best(black_box(&mc_problem))) + b.iter(|| solver.find_witness(black_box(&mc_problem))) }); group.finish(); diff --git a/problemreductions-cli/src/commands/create.rs b/problemreductions-cli/src/commands/create.rs index c831d9d92..21183fb7d 100644 --- a/problemreductions-cli/src/commands/create.rs +++ b/problemreductions-cli/src/commands/create.rs @@ -930,12 +930,27 @@ fn print_problem_help(canonical: &str, graph_type: Option<&str>) -> Result<()> { let hint = type_format_hint(&field.type_name, graph_type); eprintln!(" --{:<16} {} ({})", "arcs", field.description, hint); } else if field.type_name == "MixedGraph" { - eprintln!(" --{:<16} Undirected edges E of the mixed graph (edge list: 0-1,1-2,2-3)", "graph"); - eprintln!(" --{:<16} Directed arcs A of the mixed graph (directed arcs: 0>1,1>2,2>0)", "arcs"); + eprintln!( + " --{:<16} Undirected edges E of the mixed graph (edge list: 0-1,1-2,2-3)", + "graph" + ); + eprintln!( + " --{:<16} Directed arcs A of the mixed graph (directed arcs: 0>1,1>2,2>0)", + "arcs" + ); } else if field.type_name == "BipartiteGraph" { - eprintln!(" --{:<16} Vertices in the left partition (integer)", "left"); - eprintln!(" --{:<16} Vertices in the right partition (integer)", "right"); - eprintln!(" --{:<16} Bipartite edges as left-right pairs (edge list: 0-0,0-1,1-2)", "biedges"); + eprintln!( + " --{:<16} Vertices in the left partition (integer)", + "left" + ); + eprintln!( + " --{:<16} Vertices in the right partition (integer)", + "right" + ); + eprintln!( + " --{:<16} Bipartite edges as left-right pairs (edge list: 0-0,0-1,1-2)", + "biedges" + ); } else { let hint = help_flag_hint(canonical, &field.name, &field.type_name, graph_type); eprintln!(" --{:<16} {} ({})", flag_name, field.description, hint); diff --git a/problemreductions-macros/src/lib.rs b/problemreductions-macros/src/lib.rs index ad4a8baf8..0cbdf482c 100644 --- a/problemreductions-macros/src/lib.rs +++ b/problemreductions-macros/src/lib.rs @@ -377,21 +377,12 @@ fn extract_target_from_trait(path: &Path) -> syn::Result { // --- declare_variants! proc macro --- -/// Solver kind for dispatch generation. -#[derive(Debug, Clone, Copy)] -enum SolverKind { - /// Optimization problem marker. - Opt, - /// Satisfaction problem marker. - Sat, -} - /// Input for the `declare_variants!` proc macro. struct DeclareVariantsInput { entries: Vec, } -/// A single entry: `[default] opt|sat Type => "complexity_string"`. +/// A single entry: `[default] Type => "complexity_string"`. struct DeclareVariantEntry { is_default: bool, ty: Type, @@ -408,33 +399,6 @@ impl syn::parse::Parse for DeclareVariantsInput { input.parse::()?; } - // Require `opt` or `sat` keyword - let _solver_kind = if input.peek(syn::Ident) { - let fork = input.fork(); - if let Ok(ident) = fork.parse::() { - match ident.to_string().as_str() { - "opt" => { - input.parse::()?; // consume - SolverKind::Opt - } - "sat" => { - input.parse::()?; // consume - SolverKind::Sat - } - _ => { - return Err(syn::Error::new( - ident.span(), - "expected `opt` or `sat` before type name", - )); - } - } - } else { - return Err(input.error("expected `opt` or `sat` before type name")); - } - } else { - return Err(input.error("expected `opt` or `sat` before type name")); - }; - let ty: Type = input.parse()?; input.parse::]>()?; let complexity: syn::LitStr = input.parse()?; @@ -644,7 +608,7 @@ mod tests { #[test] fn declare_variants_accepts_single_default() { let input: DeclareVariantsInput = syn::parse_quote! { - default opt Foo => "1", + default Foo => "1", }; assert!(generate_declare_variants(&input).is_ok()); } @@ -652,7 +616,7 @@ mod tests { #[test] fn declare_variants_requires_one_default_per_problem() { let input: DeclareVariantsInput = syn::parse_quote! { - opt Foo => "1", + Foo => "1", }; let err = generate_declare_variants(&input).unwrap_err(); assert!( @@ -665,8 +629,8 @@ mod tests { #[test] fn declare_variants_rejects_multiple_defaults_for_one_problem() { let input: DeclareVariantsInput = syn::parse_quote! { - default opt Foo => "1", - default opt Foo => "2", + default Foo => "1", + default Foo => "2", }; let err = generate_declare_variants(&input).unwrap_err(); assert!( @@ -679,7 +643,7 @@ mod tests { #[test] fn declare_variants_rejects_missing_default_marker() { let input: DeclareVariantsInput = syn::parse_quote! { - opt Foo => "1", + Foo => "1", }; let err = generate_declare_variants(&input).unwrap_err(); assert!( @@ -692,8 +656,8 @@ mod tests { #[test] fn declare_variants_marks_only_explicit_default() { let input: DeclareVariantsInput = syn::parse_quote! { - opt Foo => "1", - default opt Foo => "2", + Foo => "1", + default Foo => "2", }; let result = generate_declare_variants(&input); assert!(result.is_ok()); @@ -705,27 +669,27 @@ mod tests { } #[test] - fn declare_variants_accepts_solver_kind_markers() { + fn declare_variants_accepts_entries_without_solver_kind_markers() { let input: DeclareVariantsInput = syn::parse_quote! { - default opt Foo => "1", - default sat Bar => "2", + default Foo => "1", + default Bar => "2", }; assert!(generate_declare_variants(&input).is_ok()); } #[test] - fn declare_variants_rejects_missing_solver_kind() { - let result = syn::parse_str::("Foo => \"1\""); + fn declare_variants_rejects_legacy_solver_kind_markers() { + let result = syn::parse_str::("default opt Foo => \"1\""); assert!( result.is_err(), - "expected parse error for missing solver kind" + "expected parse error for legacy solver kind marker" ); } #[test] - fn declare_variants_generates_aggregate_value_and_witness_dispatch_for_opt_entries() { + fn declare_variants_generates_aggregate_value_and_witness_dispatch() { let input: DeclareVariantsInput = syn::parse_quote! { - default opt Foo => "1", + default Foo => "1", }; let tokens = generate_declare_variants(&input).unwrap().to_string(); assert!(tokens.contains("factory :"), "expected factory field"); @@ -775,55 +739,6 @@ mod tests { ); } - #[test] - fn declare_variants_generates_aggregate_value_and_witness_dispatch_for_sat_entries() { - let input: DeclareVariantsInput = syn::parse_quote! { - default sat Foo => "1", - }; - let tokens = generate_declare_variants(&input).unwrap().to_string(); - assert!(tokens.contains("factory :"), "expected factory field"); - assert!( - tokens.contains("serialize_fn :"), - "expected serialize_fn field" - ); - assert!( - tokens.contains("solve_value_fn :"), - "expected solve_value_fn field" - ); - assert!( - tokens.contains("solve_witness_fn :"), - "expected solve_witness_fn field" - ); - assert!( - !tokens.contains("factory : None"), - "factory should not be None" - ); - assert!( - !tokens.contains("serialize_fn : None"), - "serialize_fn should not be None" - ); - assert!( - !tokens.contains("solve_value_fn : None"), - "solve_value_fn should not be None" - ); - assert!( - !tokens.contains("solve_witness_fn : None"), - "solve_witness_fn should not be None" - ); - assert!( - tokens.contains("let total ="), - "expected aggregate value solve" - ); - assert!( - tokens.contains("find_witness"), - "expected find_witness in tokens" - ); - assert!( - !tokens.contains("find_satisfying"), - "did not expect legacy find_satisfying in tokens" - ); - } - #[test] fn reduction_rejects_unexpected_attribute() { let extra_attr = syn::Ident::new("extra", proc_macro2::Span::call_site()); @@ -848,7 +763,7 @@ mod tests { #[test] fn declare_variants_codegen_uses_required_dispatch_fields() { let input: DeclareVariantsInput = syn::parse_quote! { - default opt Foo => "1", + default Foo => "1", }; let tokens = generate_declare_variants(&input).unwrap().to_string(); assert!(tokens.contains("factory :")); diff --git a/src/lib.rs b/src/lib.rs index 1c1c9ef65..2eca06109 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -11,7 +11,7 @@ //! | [`rules`] | Reduction rules, [`ReductionGraph`](rules::ReductionGraph) for path search | //! | [`solvers`] | [`BruteForce`] and [`ILPSolver`](solvers::ILPSolver) | //! | [`topology`] | Graph types — [`SimpleGraph`](topology::SimpleGraph), [`UnitDiskGraph`](topology::UnitDiskGraph), etc. | -//! | [`traits`] | Core traits — [`Problem`], [`ObjectiveProblem`], [`WitnessProblem`] | +//! | [`traits`] | Core traits — [`Problem`] | //! | [`types`] | [`Max`], [`Min`], [`Extremum`], [`ExtremumSense`], [`ProblemSize`], [`WeightElement`] | //! | [`variant`] | Variant parameter system for problem type parameterization | //! @@ -89,7 +89,7 @@ pub mod prelude { // Core traits pub use crate::rules::{ReduceTo, ReductionResult}; pub use crate::solvers::{BruteForce, Solver}; - pub use crate::traits::{ObjectiveProblem, Problem, WitnessProblem}; + pub use crate::traits::Problem; // Types pub use crate::error::{ProblemError, Result}; @@ -105,7 +105,7 @@ pub use error::{ProblemError, Result}; pub use expr::{asymptotic_normal_form, AsymptoticAnalysisError, CanonicalizationError, Expr}; pub use registry::{ComplexityClass, ProblemInfo}; pub use solvers::{BruteForce, Solver}; -pub use traits::{ObjectiveProblem, Problem, WitnessProblem}; +pub use traits::Problem; pub use types::{ And, Extremum, ExtremumSense, Max, Min, NumericSize, One, Or, ProblemSize, Sum, Unweighted, WeightElement, diff --git a/src/models/algebraic/bmf.rs b/src/models/algebraic/bmf.rs index 8f0c424ef..8e3bc1f81 100644 --- a/src/models/algebraic/bmf.rs +++ b/src/models/algebraic/bmf.rs @@ -5,8 +5,8 @@ //! The boolean product `(B * C)[i,j] = OR_k (B[i,k] AND C[k,j])`. use crate::registry::{FieldInfo, ProblemSchemaEntry}; -use crate::traits::{ObjectiveProblem, Problem}; -use crate::types::{ExtremumSense, Min}; +use crate::traits::Problem; +use crate::types::Min; use serde::{Deserialize, Serialize}; inventory::submit! { @@ -223,16 +223,8 @@ impl Problem for BMF { } } -impl ObjectiveProblem for BMF { - type Objective = i32; - - fn direction(&self) -> ExtremumSense { - ExtremumSense::Minimize - } -} - crate::declare_variants! { - default opt BMF => "2^(rows * rank + rank * cols)", + default BMF => "2^(rows * rank + rank * cols)", } #[cfg(feature = "example-db")] diff --git a/src/models/algebraic/closest_vector_problem.rs b/src/models/algebraic/closest_vector_problem.rs index 1b5d7d580..3e3410538 100644 --- a/src/models/algebraic/closest_vector_problem.rs +++ b/src/models/algebraic/closest_vector_problem.rs @@ -4,8 +4,8 @@ //! minimizing ‖Bx - t‖₂. use crate::registry::{FieldInfo, ProblemSchemaEntry, VariantDimension}; -use crate::traits::{ObjectiveProblem, Problem}; -use crate::types::{ExtremumSense, Min}; +use crate::traits::Problem; +use crate::types::Min; use serde::{Deserialize, Serialize}; inventory::submit! { @@ -274,26 +274,9 @@ where } } -impl ObjectiveProblem for ClosestVectorProblem -where - T: Clone - + Into - + crate::variant::VariantParam - + Serialize - + for<'de> Deserialize<'de> - + std::fmt::Debug - + 'static, -{ - type Objective = f64; - - fn direction(&self) -> ExtremumSense { - ExtremumSense::Minimize - } -} - crate::declare_variants! { - default opt ClosestVectorProblem => "2^num_basis_vectors", - opt ClosestVectorProblem => "2^num_basis_vectors", + default ClosestVectorProblem => "2^num_basis_vectors", + ClosestVectorProblem => "2^num_basis_vectors", } #[cfg(feature = "example-db")] diff --git a/src/models/algebraic/consecutive_block_minimization.rs b/src/models/algebraic/consecutive_block_minimization.rs index 844939f33..53785c485 100644 --- a/src/models/algebraic/consecutive_block_minimization.rs +++ b/src/models/algebraic/consecutive_block_minimization.rs @@ -9,7 +9,7 @@ //! This is problem SR17 in Garey & Johnson. use crate::registry::{FieldInfo, ProblemSchemaEntry}; -use crate::traits::{Problem, WitnessProblem}; +use crate::traits::Problem; use serde::{Deserialize, Serialize}; inventory::submit! { @@ -156,17 +156,19 @@ impl ConsecutiveBlockMinimization { impl Problem for ConsecutiveBlockMinimization { const NAME: &'static str = "ConsecutiveBlockMinimization"; - type Value = bool; + type Value = crate::types::Or; fn dims(&self) -> Vec { vec![self.num_cols; self.num_cols] } - fn evaluate(&self, config: &[usize]) -> bool { - match self.count_consecutive_blocks(config) { - Some(total) => (total as i64) <= self.bound, - None => false, - } + fn evaluate(&self, config: &[usize]) -> crate::types::Or { + crate::types::Or({ + match self.count_consecutive_blocks(config) { + Some(total) => (total as i64) <= self.bound, + None => false, + } + }) } fn variant() -> Vec<(&'static str, &'static str)> { @@ -178,10 +180,8 @@ impl Problem for ConsecutiveBlockMinimization { } } -impl WitnessProblem for ConsecutiveBlockMinimization {} - crate::declare_variants! { - default sat ConsecutiveBlockMinimization => "factorial(num_cols) * num_rows * num_cols", + default ConsecutiveBlockMinimization => "factorial(num_cols) * num_rows * num_cols", } #[derive(Debug, Clone, Deserialize)] diff --git a/src/models/algebraic/consecutive_ones_matrix_augmentation.rs b/src/models/algebraic/consecutive_ones_matrix_augmentation.rs index 4ce637e4b..079d7bcd6 100644 --- a/src/models/algebraic/consecutive_ones_matrix_augmentation.rs +++ b/src/models/algebraic/consecutive_ones_matrix_augmentation.rs @@ -5,7 +5,7 @@ //! augmentations such that every row has consecutive 1s. use crate::registry::{FieldInfo, ProblemSchemaEntry}; -use crate::traits::{Problem, WitnessProblem}; +use crate::traits::Problem; use serde::{Deserialize, Serialize}; inventory::submit! { @@ -114,15 +114,17 @@ impl ConsecutiveOnesMatrixAugmentation { impl Problem for ConsecutiveOnesMatrixAugmentation { const NAME: &'static str = "ConsecutiveOnesMatrixAugmentation"; - type Value = bool; + type Value = crate::types::Or; fn dims(&self) -> Vec { vec![self.num_cols(); self.num_cols()] } - fn evaluate(&self, config: &[usize]) -> bool { - self.total_augmentation_cost(config) - .is_some_and(|cost| cost <= self.bound as usize) + fn evaluate(&self, config: &[usize]) -> crate::types::Or { + crate::types::Or({ + self.total_augmentation_cost(config) + .is_some_and(|cost| cost <= self.bound as usize) + }) } fn variant() -> Vec<(&'static str, &'static str)> { @@ -134,10 +136,8 @@ impl Problem for ConsecutiveOnesMatrixAugmentation { } } -impl WitnessProblem for ConsecutiveOnesMatrixAugmentation {} - crate::declare_variants! { - default sat ConsecutiveOnesMatrixAugmentation => "factorial(num_cols) * num_rows * num_cols", + default ConsecutiveOnesMatrixAugmentation => "factorial(num_cols) * num_rows * num_cols", } #[cfg(feature = "example-db")] diff --git a/src/models/algebraic/consecutive_ones_submatrix.rs b/src/models/algebraic/consecutive_ones_submatrix.rs index 0b012dc8e..3b7308ddc 100644 --- a/src/models/algebraic/consecutive_ones_submatrix.rs +++ b/src/models/algebraic/consecutive_ones_submatrix.rs @@ -7,7 +7,7 @@ //! transformation from Hamiltonian Path. use crate::registry::{FieldInfo, ProblemSchemaEntry}; -use crate::traits::{Problem, WitnessProblem}; +use crate::traits::Problem; use serde::{Deserialize, Serialize}; inventory::submit! { @@ -173,7 +173,7 @@ impl ConsecutiveOnesSubmatrix { impl Problem for ConsecutiveOnesSubmatrix { const NAME: &'static str = "ConsecutiveOnesSubmatrix"; - type Value = bool; + type Value = crate::types::Or; fn variant() -> Vec<(&'static str, &'static str)> { crate::variant_params![] @@ -183,31 +183,31 @@ impl Problem for ConsecutiveOnesSubmatrix { vec![2; self.num_cols()] } - fn evaluate(&self, config: &[usize]) -> bool { - if config.len() != self.num_cols() { - return false; - } - if config.iter().any(|&v| v >= 2) { - return false; - } - // Collect selected column indices - let selected: Vec = config - .iter() - .enumerate() - .filter(|(_, &v)| v == 1) - .map(|(i, _)| i) - .collect(); - if (selected.len() as i64) != self.bound { - return false; - } - self.any_permutation_has_c1p(&selected) + fn evaluate(&self, config: &[usize]) -> crate::types::Or { + crate::types::Or({ + if config.len() != self.num_cols() { + return crate::types::Or(false); + } + if config.iter().any(|&v| v >= 2) { + return crate::types::Or(false); + } + // Collect selected column indices + let selected: Vec = config + .iter() + .enumerate() + .filter(|(_, &v)| v == 1) + .map(|(i, _)| i) + .collect(); + if (selected.len() as i64) != self.bound { + return crate::types::Or(false); + } + self.any_permutation_has_c1p(&selected) + }) } } -impl WitnessProblem for ConsecutiveOnesSubmatrix {} - crate::declare_variants! { - default sat ConsecutiveOnesSubmatrix => "2^(num_cols) * (num_rows + num_cols)", + default ConsecutiveOnesSubmatrix => "2^(num_cols) * (num_rows + num_cols)", } #[cfg(feature = "example-db")] diff --git a/src/models/algebraic/ilp.rs b/src/models/algebraic/ilp.rs index 285ee3946..adcb80d15 100644 --- a/src/models/algebraic/ilp.rs +++ b/src/models/algebraic/ilp.rs @@ -8,8 +8,8 @@ //! - `ILP`: non-negative integer variables (0..2^31-1) use crate::registry::{FieldInfo, ProblemSchemaEntry, VariantDimension}; -use crate::traits::{ObjectiveProblem, Problem}; -use crate::types::{Extremum, ExtremumSense}; +use crate::traits::Problem; +use crate::types::Extremum; use serde::{Deserialize, Serialize}; use std::marker::PhantomData; @@ -270,20 +270,9 @@ impl Problem for ILP { } } -impl ObjectiveProblem for ILP { - type Objective = f64; - - fn direction(&self) -> ExtremumSense { - match self.sense { - ObjectiveSense::Maximize => ExtremumSense::Maximize, - ObjectiveSense::Minimize => ExtremumSense::Minimize, - } - } -} - crate::declare_variants! { - default opt ILP => "2^num_vars", - opt ILP => "num_vars^num_vars", + default ILP => "2^num_vars", + ILP => "num_vars^num_vars", } #[cfg(feature = "example-db")] diff --git a/src/models/algebraic/quadratic_assignment.rs b/src/models/algebraic/quadratic_assignment.rs index 9ac37abe4..2582d3106 100644 --- a/src/models/algebraic/quadratic_assignment.rs +++ b/src/models/algebraic/quadratic_assignment.rs @@ -4,8 +4,8 @@ //! where cost depends on both inter-facility flows and inter-location distances. use crate::registry::{FieldInfo, ProblemSchemaEntry}; -use crate::traits::{ObjectiveProblem, Problem}; -use crate::types::{ExtremumSense, Min}; +use crate::traits::Problem; +use crate::types::Min; use serde::{Deserialize, Serialize}; inventory::submit! { @@ -164,16 +164,8 @@ impl Problem for QuadraticAssignment { } } -impl ObjectiveProblem for QuadraticAssignment { - type Objective = i64; - - fn direction(&self) -> ExtremumSense { - ExtremumSense::Minimize - } -} - crate::declare_variants! { - default opt QuadraticAssignment => "factorial(num_facilities)", + default QuadraticAssignment => "factorial(num_facilities)", } #[cfg(feature = "example-db")] diff --git a/src/models/algebraic/qubo.rs b/src/models/algebraic/qubo.rs index 32fc8b73c..a36fd7bc4 100644 --- a/src/models/algebraic/qubo.rs +++ b/src/models/algebraic/qubo.rs @@ -3,8 +3,8 @@ //! QUBO minimizes a quadratic function over binary variables. use crate::registry::{FieldInfo, ProblemSchemaEntry, VariantDimension}; -use crate::traits::{ObjectiveProblem, Problem}; -use crate::types::{ExtremumSense, Min, WeightElement}; +use crate::traits::Problem; +use crate::types::{Min, WeightElement}; use serde::{Deserialize, Serialize}; inventory::submit! { @@ -173,26 +173,8 @@ where } } -impl ObjectiveProblem for QUBO -where - W: WeightElement - + crate::variant::VariantParam - + PartialOrd - + num_traits::Num - + num_traits::Zero - + num_traits::Bounded - + std::ops::AddAssign - + std::ops::Mul, -{ - type Objective = W::Sum; - - fn direction(&self) -> ExtremumSense { - ExtremumSense::Minimize - } -} - crate::declare_variants! { - default opt QUBO => "2^num_vars", + default QUBO => "2^num_vars", } #[cfg(feature = "example-db")] diff --git a/src/models/algebraic/sparse_matrix_compression.rs b/src/models/algebraic/sparse_matrix_compression.rs index 4d253e9c4..7f4f5e9c5 100644 --- a/src/models/algebraic/sparse_matrix_compression.rs +++ b/src/models/algebraic/sparse_matrix_compression.rs @@ -5,7 +5,7 @@ //! by assigning each row a shift in `{1, ..., K}` without collisions. use crate::registry::{FieldInfo, ProblemSchemaEntry}; -use crate::traits::{Problem, WitnessProblem}; +use crate::traits::Problem; use serde::{Deserialize, Serialize}; inventory::submit! { @@ -119,14 +119,14 @@ impl SparseMatrixCompression { impl Problem for SparseMatrixCompression { const NAME: &'static str = "SparseMatrixCompression"; - type Value = bool; + type Value = crate::types::Or; fn dims(&self) -> Vec { vec![self.bound_k; self.num_rows()] } - fn evaluate(&self, config: &[usize]) -> bool { - self.storage_vector(config).is_some() + fn evaluate(&self, config: &[usize]) -> crate::types::Or { + crate::types::Or(self.storage_vector(config).is_some()) } fn variant() -> Vec<(&'static str, &'static str)> { @@ -134,10 +134,8 @@ impl Problem for SparseMatrixCompression { } } -impl WitnessProblem for SparseMatrixCompression {} - crate::declare_variants! { - default sat SparseMatrixCompression => "(bound_k ^ num_rows) * num_rows * num_cols", + default SparseMatrixCompression => "(bound_k ^ num_rows) * num_rows * num_cols", } #[cfg(feature = "example-db")] diff --git a/src/models/formula/circuit.rs b/src/models/formula/circuit.rs index 6c764f82a..1a951265c 100644 --- a/src/models/formula/circuit.rs +++ b/src/models/formula/circuit.rs @@ -4,7 +4,7 @@ //! The goal is to find variable assignments that satisfy the circuit constraints. use crate::registry::{FieldInfo, ProblemSchemaEntry}; -use crate::traits::{Problem, WitnessProblem}; +use crate::traits::Problem; use serde::{Deserialize, Serialize}; use std::collections::HashMap; @@ -289,14 +289,14 @@ pub(crate) fn is_circuit_satisfying( impl Problem for CircuitSAT { const NAME: &'static str = "CircuitSAT"; - type Value = bool; + type Value = crate::types::Or; fn dims(&self) -> Vec { vec![2; self.variables.len()] } - fn evaluate(&self, config: &[usize]) -> bool { - self.count_satisfied(config) == self.circuit.num_assignments() + fn evaluate(&self, config: &[usize]) -> crate::types::Or { + crate::types::Or(self.count_satisfied(config) == self.circuit.num_assignments()) } fn variant() -> Vec<(&'static str, &'static str)> { @@ -304,10 +304,8 @@ impl Problem for CircuitSAT { } } -impl WitnessProblem for CircuitSAT {} - crate::declare_variants! { - default sat CircuitSAT => "2^num_variables", + default CircuitSAT => "2^num_variables", } #[cfg(feature = "example-db")] diff --git a/src/models/formula/ksat.rs b/src/models/formula/ksat.rs index 043a00055..047c902e5 100644 --- a/src/models/formula/ksat.rs +++ b/src/models/formula/ksat.rs @@ -6,7 +6,7 @@ //! MaxKSatisfiability type (if available). use crate::registry::{FieldInfo, ProblemSchemaEntry, VariantDimension}; -use crate::traits::{Problem, WitnessProblem}; +use crate::traits::Problem; use crate::variant::{KValue, K2, K3, KN}; use serde::{Deserialize, Serialize}; @@ -168,15 +168,17 @@ impl KSatisfiability { impl Problem for KSatisfiability { const NAME: &'static str = "KSatisfiability"; - type Value = bool; + type Value = crate::types::Or; fn dims(&self) -> Vec { vec![2; self.num_vars] } - fn evaluate(&self, config: &[usize]) -> bool { - let assignment = Self::config_to_assignment(config); - self.is_satisfying(&assignment) + fn evaluate(&self, config: &[usize]) -> crate::types::Or { + crate::types::Or({ + let assignment = Self::config_to_assignment(config); + self.is_satisfying(&assignment) + }) } fn variant() -> Vec<(&'static str, &'static str)> { @@ -184,12 +186,10 @@ impl Problem for KSatisfiability { } } -impl WitnessProblem for KSatisfiability {} - crate::declare_variants! { - default sat KSatisfiability => "2^num_variables", - sat KSatisfiability => "num_variables + num_clauses", - sat KSatisfiability => "1.307^num_variables", + default KSatisfiability => "2^num_variables", + KSatisfiability => "num_variables + num_clauses", + KSatisfiability => "1.307^num_variables", } #[cfg(feature = "example-db")] diff --git a/src/models/formula/nae_satisfiability.rs b/src/models/formula/nae_satisfiability.rs index 95721c243..cbc53b327 100644 --- a/src/models/formula/nae_satisfiability.rs +++ b/src/models/formula/nae_satisfiability.rs @@ -4,7 +4,7 @@ //! contains at least one true literal and at least one false literal. use crate::registry::{FieldInfo, ProblemSchemaEntry}; -use crate::traits::{Problem, WitnessProblem}; +use crate::traits::Problem; use serde::{Deserialize, Serialize}; use super::CNFClause; @@ -96,7 +96,7 @@ impl NAESatisfiability { /// Check if a solution (config) is valid. pub fn is_valid_solution(&self, config: &[usize]) -> bool { - self.evaluate(config) + self.evaluate(config).0 } fn config_to_assignment(config: &[usize]) -> Vec { @@ -135,15 +135,17 @@ impl NAESatisfiability { impl Problem for NAESatisfiability { const NAME: &'static str = "NAESatisfiability"; - type Value = bool; + type Value = crate::types::Or; fn dims(&self) -> Vec { vec![2; self.num_vars] } - fn evaluate(&self, config: &[usize]) -> bool { - let assignment = Self::config_to_assignment(config); - self.is_nae_satisfying(&assignment) + fn evaluate(&self, config: &[usize]) -> crate::types::Or { + crate::types::Or({ + let assignment = Self::config_to_assignment(config); + self.is_nae_satisfying(&assignment) + }) } fn variant() -> Vec<(&'static str, &'static str)> { @@ -151,10 +153,8 @@ impl Problem for NAESatisfiability { } } -impl WitnessProblem for NAESatisfiability {} - crate::declare_variants! { - default sat NAESatisfiability => "2^num_variables", + default NAESatisfiability => "2^num_variables", } #[derive(Debug, Clone, Deserialize)] diff --git a/src/models/formula/qbf.rs b/src/models/formula/qbf.rs index 69f30a48d..d202e9f17 100644 --- a/src/models/formula/qbf.rs +++ b/src/models/formula/qbf.rs @@ -10,7 +10,7 @@ use crate::models::formula::CNFClause; use crate::registry::{FieldInfo, ProblemSchemaEntry}; -use crate::traits::{Problem, WitnessProblem}; +use crate::traits::Problem; use serde::{Deserialize, Serialize}; inventory::submit! { @@ -157,17 +157,19 @@ impl QuantifiedBooleanFormulas { impl Problem for QuantifiedBooleanFormulas { const NAME: &'static str = "QuantifiedBooleanFormulas"; - type Value = bool; + type Value = crate::types::Or; fn dims(&self) -> Vec { vec![] } - fn evaluate(&self, config: &[usize]) -> bool { - if !config.is_empty() { - return false; - } - self.is_true() + fn evaluate(&self, config: &[usize]) -> crate::types::Or { + crate::types::Or({ + if !config.is_empty() { + return crate::types::Or(false); + } + self.is_true() + }) } fn variant() -> Vec<(&'static str, &'static str)> { @@ -175,10 +177,8 @@ impl Problem for QuantifiedBooleanFormulas { } } -impl WitnessProblem for QuantifiedBooleanFormulas {} - crate::declare_variants! { - default sat QuantifiedBooleanFormulas => "2^num_vars", + default QuantifiedBooleanFormulas => "2^num_vars", } #[cfg(feature = "example-db")] diff --git a/src/models/formula/sat.rs b/src/models/formula/sat.rs index a021c65ff..d84f35940 100644 --- a/src/models/formula/sat.rs +++ b/src/models/formula/sat.rs @@ -6,7 +6,7 @@ //! the separate MaxSatisfiability type (if available). use crate::registry::{FieldInfo, ProblemSchemaEntry}; -use crate::traits::{Problem, WitnessProblem}; +use crate::traits::Problem; use serde::{Deserialize, Serialize}; inventory::submit! { @@ -169,7 +169,7 @@ impl Satisfiability { /// /// For SAT, a valid solution is one that satisfies all clauses. pub fn is_valid_solution(&self, config: &[usize]) -> bool { - self.evaluate(config) + self.evaluate(config).0 } /// Convert a usize config to boolean assignment. @@ -180,15 +180,17 @@ impl Satisfiability { impl Problem for Satisfiability { const NAME: &'static str = "Satisfiability"; - type Value = bool; + type Value = crate::types::Or; fn dims(&self) -> Vec { vec![2; self.num_vars] } - fn evaluate(&self, config: &[usize]) -> bool { - let assignment = Self::config_to_assignment(config); - self.is_satisfying(&assignment) + fn evaluate(&self, config: &[usize]) -> crate::types::Or { + crate::types::Or({ + let assignment = Self::config_to_assignment(config); + self.is_satisfying(&assignment) + }) } fn variant() -> Vec<(&'static str, &'static str)> { @@ -196,10 +198,8 @@ impl Problem for Satisfiability { } } -impl WitnessProblem for Satisfiability {} - crate::declare_variants! { - default sat Satisfiability => "2^num_variables", + default Satisfiability => "2^num_variables", } /// Check if an assignment satisfies a SAT formula. diff --git a/src/models/graph/acyclic_partition.rs b/src/models/graph/acyclic_partition.rs index 5620d61ac..4bb5f4935 100644 --- a/src/models/graph/acyclic_partition.rs +++ b/src/models/graph/acyclic_partition.rs @@ -7,7 +7,7 @@ use crate::registry::{FieldInfo, ProblemSchemaEntry, ProblemSizeFieldEntry, VariantDimension}; use crate::topology::DirectedGraph; -use crate::traits::{Problem, WitnessProblem}; +use crate::traits::Problem; use crate::types::WeightElement; use num_traits::Zero; use serde::{Deserialize, Serialize}; @@ -156,7 +156,7 @@ where W: WeightElement + crate::variant::VariantParam, { const NAME: &'static str = "AcyclicPartition"; - type Value = bool; + type Value = crate::types::Or; fn variant() -> Vec<(&'static str, &'static str)> { crate::variant_params![W] @@ -166,20 +166,20 @@ where vec![self.graph.num_vertices(); self.graph.num_vertices()] } - fn evaluate(&self, config: &[usize]) -> bool { - is_valid_acyclic_partition( - &self.graph, - &self.vertex_weights, - &self.arc_costs, - &self.weight_bound, - &self.cost_bound, - config, - ) + fn evaluate(&self, config: &[usize]) -> crate::types::Or { + crate::types::Or({ + is_valid_acyclic_partition( + &self.graph, + &self.vertex_weights, + &self.arc_costs, + &self.weight_bound, + &self.cost_bound, + config, + ) + }) } } -impl WitnessProblem for AcyclicPartition where W: WeightElement + crate::variant::VariantParam {} - fn is_valid_acyclic_partition( graph: &DirectedGraph, vertex_weights: &[W], @@ -237,7 +237,7 @@ fn is_valid_acyclic_partition( } crate::declare_variants! { - default sat AcyclicPartition => "num_vertices^num_vertices", + default AcyclicPartition => "num_vertices^num_vertices", } #[cfg(feature = "example-db")] diff --git a/src/models/graph/balanced_complete_bipartite_subgraph.rs b/src/models/graph/balanced_complete_bipartite_subgraph.rs index 578596b79..fe2ce502f 100644 --- a/src/models/graph/balanced_complete_bipartite_subgraph.rs +++ b/src/models/graph/balanced_complete_bipartite_subgraph.rs @@ -1,6 +1,6 @@ use crate::registry::{FieldInfo, ProblemSchemaEntry}; use crate::topology::BipartiteGraph; -use crate::traits::{Problem, WitnessProblem}; +use crate::traits::Problem; use serde::{Deserialize, Serialize}; use std::collections::HashSet; @@ -96,31 +96,33 @@ impl BalancedCompleteBipartiteSubgraph { } pub fn is_valid_solution(&self, config: &[usize]) -> bool { - self.evaluate(config) + self.evaluate(config).0 } } impl Problem for BalancedCompleteBipartiteSubgraph { const NAME: &'static str = "BalancedCompleteBipartiteSubgraph"; - type Value = bool; + type Value = crate::types::Or; fn dims(&self) -> Vec { vec![2; self.num_vertices()] } - fn evaluate(&self, config: &[usize]) -> bool { - let Some((selected_left, selected_right)) = self.selected_vertices(config) else { - return false; - }; + fn evaluate(&self, config: &[usize]) -> crate::types::Or { + crate::types::Or({ + let Some((selected_left, selected_right)) = self.selected_vertices(config) else { + return crate::types::Or(false); + }; - if selected_left.len() != self.k || selected_right.len() != self.k { - return false; - } + if selected_left.len() != self.k || selected_right.len() != self.k { + return crate::types::Or(false); + } - selected_left.iter().all(|&left| { - selected_right - .iter() - .all(|&right| self.has_selected_edge(left, right)) + selected_left.iter().all(|&left| { + selected_right + .iter() + .all(|&right| self.has_selected_edge(left, right)) + }) }) } @@ -129,8 +131,6 @@ impl Problem for BalancedCompleteBipartiteSubgraph { } } -impl WitnessProblem for BalancedCompleteBipartiteSubgraph {} - #[derive(Deserialize)] struct BalancedCompleteBipartiteSubgraphRepr { graph: BipartiteGraph, @@ -144,7 +144,7 @@ impl From for BalancedCompleteBipartiteSu } crate::declare_variants! { - default sat BalancedCompleteBipartiteSubgraph => "1.3803^num_vertices", + default BalancedCompleteBipartiteSubgraph => "1.3803^num_vertices", } #[cfg(feature = "example-db")] diff --git a/src/models/graph/biclique_cover.rs b/src/models/graph/biclique_cover.rs index 8779ecf1e..868649c02 100644 --- a/src/models/graph/biclique_cover.rs +++ b/src/models/graph/biclique_cover.rs @@ -5,8 +5,8 @@ use crate::registry::{FieldInfo, ProblemSchemaEntry}; use crate::topology::BipartiteGraph; -use crate::traits::{ObjectiveProblem, Problem}; -use crate::types::{ExtremumSense, Min}; +use crate::traits::Problem; +use crate::types::Min; use serde::{Deserialize, Serialize}; use std::collections::HashSet; @@ -238,16 +238,8 @@ impl Problem for BicliqueCover { } } -impl ObjectiveProblem for BicliqueCover { - type Objective = i32; - - fn direction(&self) -> ExtremumSense { - ExtremumSense::Minimize - } -} - crate::declare_variants! { - default opt BicliqueCover => "2^num_vertices", + default BicliqueCover => "2^num_vertices", } #[cfg(feature = "example-db")] diff --git a/src/models/graph/biconnectivity_augmentation.rs b/src/models/graph/biconnectivity_augmentation.rs index 9e545ecf6..d923c2ce9 100644 --- a/src/models/graph/biconnectivity_augmentation.rs +++ b/src/models/graph/biconnectivity_augmentation.rs @@ -6,7 +6,7 @@ use crate::registry::{FieldInfo, ProblemSchemaEntry, VariantDimension}; use crate::topology::{Graph, SimpleGraph}; -use crate::traits::{Problem, WitnessProblem}; +use crate::traits::Problem; use crate::types::WeightElement; use num_traits::Zero; use serde::{Deserialize, Serialize}; @@ -165,7 +165,7 @@ where W: WeightElement + crate::variant::VariantParam, { const NAME: &'static str = "BiconnectivityAugmentation"; - type Value = bool; + type Value = crate::types::Or; fn variant() -> Vec<(&'static str, &'static str)> { crate::variant_params![G, W] @@ -175,19 +175,14 @@ where vec![2; self.num_potential_edges()] } - fn evaluate(&self, config: &[usize]) -> bool { - self.augmented_graph(config) - .is_some_and(|graph| is_biconnected(&graph)) + fn evaluate(&self, config: &[usize]) -> crate::types::Or { + crate::types::Or({ + self.augmented_graph(config) + .is_some_and(|graph| is_biconnected(&graph)) + }) } } -impl WitnessProblem for BiconnectivityAugmentation -where - G: Graph + crate::variant::VariantParam, - W: WeightElement + crate::variant::VariantParam, -{ -} - fn normalize_edge(u: usize, v: usize) -> (usize, usize) { if u <= v { (u, v) @@ -260,7 +255,7 @@ fn is_biconnected(graph: &G) -> bool { } crate::declare_variants! { - default sat BiconnectivityAugmentation => "2^num_potential_edges", + default BiconnectivityAugmentation => "2^num_potential_edges", } #[cfg(feature = "example-db")] diff --git a/src/models/graph/bottleneck_traveling_salesman.rs b/src/models/graph/bottleneck_traveling_salesman.rs index 03e5e18df..3badad9cb 100644 --- a/src/models/graph/bottleneck_traveling_salesman.rs +++ b/src/models/graph/bottleneck_traveling_salesman.rs @@ -5,8 +5,8 @@ use crate::registry::{FieldInfo, ProblemSchemaEntry}; use crate::topology::{Graph, SimpleGraph}; -use crate::traits::{ObjectiveProblem, Problem}; -use crate::types::{ExtremumSense, Min}; +use crate::traits::Problem; +use crate::types::Min; use serde::{Deserialize, Serialize}; inventory::submit! { @@ -129,14 +129,6 @@ impl Problem for BottleneckTravelingSalesman { } } -impl ObjectiveProblem for BottleneckTravelingSalesman { - type Objective = i32; - - fn direction(&self) -> ExtremumSense { - ExtremumSense::Minimize - } -} - #[cfg(feature = "example-db")] pub(crate) fn canonical_model_example_specs() -> Vec { vec![crate::example_db::specs::ModelExampleSpec { @@ -165,7 +157,7 @@ pub(crate) fn canonical_model_example_specs() -> Vec "num_vertices^2 * 2^num_vertices", + default BottleneckTravelingSalesman => "num_vertices^2 * 2^num_vertices", } #[cfg(test)] diff --git a/src/models/graph/bounded_component_spanning_forest.rs b/src/models/graph/bounded_component_spanning_forest.rs index bffc3219c..32f229a1a 100644 --- a/src/models/graph/bounded_component_spanning_forest.rs +++ b/src/models/graph/bounded_component_spanning_forest.rs @@ -6,7 +6,7 @@ use crate::registry::{FieldInfo, ProblemSchemaEntry, VariantDimension}; use crate::topology::{Graph, SimpleGraph}; -use crate::traits::{Problem, WitnessProblem}; +use crate::traits::Problem; use crate::types::WeightElement; use num_traits::Zero; use serde::{Deserialize, Serialize}; @@ -185,7 +185,7 @@ where W: WeightElement + crate::variant::VariantParam, { const NAME: &'static str = "BoundedComponentSpanningForest"; - type Value = bool; + type Value = crate::types::Or; fn variant() -> Vec<(&'static str, &'static str)> { crate::variant_params![G, W] @@ -195,18 +195,11 @@ where vec![self.max_components; self.graph.num_vertices()] } - fn evaluate(&self, config: &[usize]) -> bool { - self.is_valid_solution(config) + fn evaluate(&self, config: &[usize]) -> crate::types::Or { + crate::types::Or(self.is_valid_solution(config)) } } -impl WitnessProblem for BoundedComponentSpanningForest -where - G: Graph + crate::variant::VariantParam, - W: WeightElement + crate::variant::VariantParam, -{ -} - #[cfg(feature = "example-db")] pub(crate) fn canonical_model_example_specs() -> Vec { vec![crate::example_db::specs::ModelExampleSpec { @@ -237,7 +230,7 @@ pub(crate) fn canonical_model_example_specs() -> Vec => "3^num_vertices", + default BoundedComponentSpanningForest => "3^num_vertices", } #[cfg(test)] diff --git a/src/models/graph/directed_two_commodity_integral_flow.rs b/src/models/graph/directed_two_commodity_integral_flow.rs index 0dd3f0ed2..97b4dd49b 100644 --- a/src/models/graph/directed_two_commodity_integral_flow.rs +++ b/src/models/graph/directed_two_commodity_integral_flow.rs @@ -8,7 +8,7 @@ use crate::registry::{FieldInfo, ProblemSchemaEntry}; use crate::topology::DirectedGraph; -use crate::traits::{Problem, WitnessProblem}; +use crate::traits::Problem; use serde::{Deserialize, Serialize}; inventory::submit! { @@ -244,7 +244,7 @@ impl DirectedTwoCommodityIntegralFlow { impl Problem for DirectedTwoCommodityIntegralFlow { const NAME: &'static str = "DirectedTwoCommodityIntegralFlow"; - type Value = bool; + type Value = crate::types::Or; fn dims(&self) -> Vec { self.capacities @@ -254,8 +254,8 @@ impl Problem for DirectedTwoCommodityIntegralFlow { .collect() } - fn evaluate(&self, config: &[usize]) -> bool { - self.is_feasible(config) + fn evaluate(&self, config: &[usize]) -> crate::types::Or { + crate::types::Or(self.is_feasible(config)) } fn variant() -> Vec<(&'static str, &'static str)> { @@ -263,10 +263,8 @@ impl Problem for DirectedTwoCommodityIntegralFlow { } } -impl WitnessProblem for DirectedTwoCommodityIntegralFlow {} - crate::declare_variants! { - default sat DirectedTwoCommodityIntegralFlow => "(max_capacity + 1)^(2 * num_arcs)", + default DirectedTwoCommodityIntegralFlow => "(max_capacity + 1)^(2 * num_arcs)", } #[cfg(feature = "example-db")] diff --git a/src/models/graph/disjoint_connecting_paths.rs b/src/models/graph/disjoint_connecting_paths.rs index e1dd76e84..d565fa123 100644 --- a/src/models/graph/disjoint_connecting_paths.rs +++ b/src/models/graph/disjoint_connecting_paths.rs @@ -5,7 +5,7 @@ use crate::registry::{FieldInfo, ProblemSchemaEntry, VariantDimension}; use crate::topology::{Graph, SimpleGraph}; -use crate::traits::{Problem, WitnessProblem}; +use crate::traits::Problem; use crate::variant::VariantParam; use serde::{Deserialize, Serialize}; use std::collections::BTreeSet; @@ -117,7 +117,7 @@ where G: Graph + VariantParam, { const NAME: &'static str = "DisjointConnectingPaths"; - type Value = bool; + type Value = crate::types::Or; fn variant() -> Vec<(&'static str, &'static str)> { crate::variant_params![G] @@ -127,13 +127,11 @@ where vec![2; self.num_edges()] } - fn evaluate(&self, config: &[usize]) -> bool { - self.is_valid_solution(config) + fn evaluate(&self, config: &[usize]) -> crate::types::Or { + crate::types::Or(self.is_valid_solution(config)) } } -impl WitnessProblem for DisjointConnectingPaths {} - fn canonical_edges(graph: &G) -> Vec<(usize, usize)> { let mut edges = graph .edges() @@ -245,7 +243,7 @@ fn is_valid_disjoint_connecting_paths( } crate::declare_variants! { - default sat DisjointConnectingPaths => "2^num_edges", + default DisjointConnectingPaths => "2^num_edges", } #[cfg(feature = "example-db")] diff --git a/src/models/graph/generalized_hex.rs b/src/models/graph/generalized_hex.rs index 79b016fa2..ef7252aae 100644 --- a/src/models/graph/generalized_hex.rs +++ b/src/models/graph/generalized_hex.rs @@ -9,7 +9,7 @@ use serde::{Deserialize, Serialize}; use crate::registry::{FieldInfo, ProblemSchemaEntry, VariantDimension}; use crate::topology::{Graph, SimpleGraph}; -use crate::traits::{Problem, WitnessProblem}; +use crate::traits::Problem; use crate::variant::VariantParam; inventory::submit! { @@ -239,7 +239,7 @@ where G: Graph + VariantParam, { const NAME: &'static str = "GeneralizedHex"; - type Value = bool; + type Value = crate::types::Or; fn variant() -> Vec<(&'static str, &'static str)> { crate::variant_params![G] @@ -249,22 +249,22 @@ where vec![] } - fn evaluate(&self, config: &[usize]) -> bool { - if !config.is_empty() { - return false; - } - let playable_vertices = self.playable_vertices(); - let vertex_to_state_index = self.vertex_to_state_index(&playable_vertices); - let mut state = vec![ClaimState::Unclaimed; playable_vertices.len()]; - let mut memo = HashMap::new(); - self.first_player_wins(&mut state, &vertex_to_state_index, &mut memo) + fn evaluate(&self, config: &[usize]) -> crate::types::Or { + crate::types::Or({ + if !config.is_empty() { + return crate::types::Or(false); + } + let playable_vertices = self.playable_vertices(); + let vertex_to_state_index = self.vertex_to_state_index(&playable_vertices); + let mut state = vec![ClaimState::Unclaimed; playable_vertices.len()]; + let mut memo = HashMap::new(); + self.first_player_wins(&mut state, &vertex_to_state_index, &mut memo) + }) } } -impl WitnessProblem for GeneralizedHex where G: Graph + VariantParam {} - crate::declare_variants! { - default sat GeneralizedHex => "3^num_playable_vertices", + default GeneralizedHex => "3^num_playable_vertices", } #[cfg(feature = "example-db")] diff --git a/src/models/graph/graph_partitioning.rs b/src/models/graph/graph_partitioning.rs index 694420868..a2ea1f0f6 100644 --- a/src/models/graph/graph_partitioning.rs +++ b/src/models/graph/graph_partitioning.rs @@ -5,8 +5,8 @@ use crate::registry::{FieldInfo, ProblemSchemaEntry, VariantDimension}; use crate::topology::{Graph, SimpleGraph}; -use crate::traits::{ObjectiveProblem, Problem}; -use crate::types::{ExtremumSense, Min}; +use crate::traits::Problem; +use crate::types::Min; use serde::{Deserialize, Serialize}; inventory::submit! { @@ -127,19 +127,8 @@ where } } -impl ObjectiveProblem for GraphPartitioning -where - G: Graph + crate::variant::VariantParam, -{ - type Objective = i32; - - fn direction(&self) -> ExtremumSense { - ExtremumSense::Minimize - } -} - crate::declare_variants! { - default opt GraphPartitioning => "2^num_vertices", + default GraphPartitioning => "2^num_vertices", } #[cfg(feature = "example-db")] diff --git a/src/models/graph/hamiltonian_circuit.rs b/src/models/graph/hamiltonian_circuit.rs index c24c8fa8f..471c15af0 100644 --- a/src/models/graph/hamiltonian_circuit.rs +++ b/src/models/graph/hamiltonian_circuit.rs @@ -5,7 +5,7 @@ use crate::registry::{FieldInfo, ProblemSchemaEntry, VariantDimension}; use crate::topology::{Graph, SimpleGraph}; -use crate::traits::{Problem, WitnessProblem}; +use crate::traits::Problem; use crate::variant::VariantParam; use serde::{Deserialize, Serialize}; @@ -92,7 +92,7 @@ where G: Graph + VariantParam, { const NAME: &'static str = "HamiltonianCircuit"; - type Value = bool; + type Value = crate::types::Or; fn variant() -> Vec<(&'static str, &'static str)> { crate::variant_params![G] @@ -103,8 +103,8 @@ where vec![n; n] } - fn evaluate(&self, config: &[usize]) -> bool { - is_valid_hamiltonian_circuit(&self.graph, config) + fn evaluate(&self, config: &[usize]) -> crate::types::Or { + crate::types::Or(is_valid_hamiltonian_circuit(&self.graph, config)) } } @@ -140,8 +140,6 @@ pub(crate) fn is_valid_hamiltonian_circuit(graph: &G, config: &[usize] true } -impl WitnessProblem for HamiltonianCircuit {} - #[cfg(feature = "example-db")] pub(crate) fn canonical_model_example_specs() -> Vec { vec![crate::example_db::specs::ModelExampleSpec { @@ -167,7 +165,7 @@ pub(crate) fn canonical_model_example_specs() -> Vec => "1.657^num_vertices", + default HamiltonianCircuit => "1.657^num_vertices", } #[cfg(test)] diff --git a/src/models/graph/hamiltonian_path.rs b/src/models/graph/hamiltonian_path.rs index 29b446045..ddc39ffa1 100644 --- a/src/models/graph/hamiltonian_path.rs +++ b/src/models/graph/hamiltonian_path.rs @@ -5,7 +5,7 @@ use crate::registry::{FieldInfo, ProblemSchemaEntry, VariantDimension}; use crate::topology::{Graph, SimpleGraph}; -use crate::traits::{Problem, WitnessProblem}; +use crate::traits::Problem; use crate::variant::VariantParam; use serde::{Deserialize, Serialize}; @@ -99,7 +99,7 @@ where G: Graph + VariantParam, { const NAME: &'static str = "HamiltonianPath"; - type Value = bool; + type Value = crate::types::Or; fn variant() -> Vec<(&'static str, &'static str)> { crate::variant_params![G] @@ -110,13 +110,11 @@ where vec![n; n] } - fn evaluate(&self, config: &[usize]) -> bool { - is_valid_hamiltonian_path(&self.graph, config) + fn evaluate(&self, config: &[usize]) -> crate::types::Or { + crate::types::Or(is_valid_hamiltonian_path(&self.graph, config)) } } -impl WitnessProblem for HamiltonianPath {} - /// Check if a configuration represents a valid Hamiltonian path in the graph. /// /// A valid Hamiltonian path is a permutation of the vertices such that @@ -170,7 +168,7 @@ pub(crate) fn canonical_model_example_specs() -> Vec => "1.657^num_vertices", + default HamiltonianPath => "1.657^num_vertices", } #[cfg(test)] diff --git a/src/models/graph/integral_flow_bundles.rs b/src/models/graph/integral_flow_bundles.rs index 69e39f4c6..893cb1da3 100644 --- a/src/models/graph/integral_flow_bundles.rs +++ b/src/models/graph/integral_flow_bundles.rs @@ -5,7 +5,7 @@ use crate::registry::{FieldInfo, ProblemSchemaEntry, ProblemSizeFieldEntry}; use crate::topology::DirectedGraph; -use crate::traits::{Problem, WitnessProblem}; +use crate::traits::Problem; use serde::{Deserialize, Serialize}; use std::collections::BTreeSet; @@ -172,7 +172,7 @@ impl IntegralFlowBundles { /// Check whether a configuration is feasible. pub fn is_valid_solution(&self, config: &[usize]) -> bool { - self.evaluate(config) + self.evaluate(config).0 } fn arc_upper_bounds(&self) -> Vec { @@ -202,7 +202,7 @@ impl IntegralFlowBundles { impl Problem for IntegralFlowBundles { const NAME: &'static str = "IntegralFlowBundles"; - type Value = bool; + type Value = crate::types::Or; fn dims(&self) -> Vec { self.arc_upper_bounds() @@ -216,47 +216,49 @@ impl Problem for IntegralFlowBundles { .collect() } - fn evaluate(&self, config: &[usize]) -> bool { - if config.len() != self.num_arcs() { - return false; - } - - let upper_bounds = self.arc_upper_bounds(); - for (&value, &upper_bound) in config.iter().zip(&upper_bounds) { - if u64::try_from(value).map_or(true, |value| value > upper_bound) { - return false; + fn evaluate(&self, config: &[usize]) -> crate::types::Or { + crate::types::Or({ + if config.len() != self.num_arcs() { + return crate::types::Or(false); } - } - for (bundle, &capacity) in self.bundles.iter().zip(&self.bundle_capacities) { - let mut total = 0u64; - for &arc_index in bundle { - let Ok(flow) = u64::try_from(config[arc_index]) else { - return false; - }; - let Some(next_total) = total.checked_add(flow) else { - return false; - }; - total = next_total; + let upper_bounds = self.arc_upper_bounds(); + for (&value, &upper_bound) in config.iter().zip(&upper_bounds) { + if u64::try_from(value).map_or(true, |value| value > upper_bound) { + return crate::types::Or(false); + } } - if total > capacity { - return false; - } - } - for vertex in 0..self.num_vertices() { - if vertex == self.source || vertex == self.sink { - continue; + for (bundle, &capacity) in self.bundles.iter().zip(&self.bundle_capacities) { + let mut total = 0u64; + for &arc_index in bundle { + let Ok(flow) = u64::try_from(config[arc_index]) else { + return crate::types::Or(false); + }; + let Some(next_total) = total.checked_add(flow) else { + return crate::types::Or(false); + }; + total = next_total; + } + if total > capacity { + return crate::types::Or(false); + } } - if self.vertex_balance(config, vertex) != Some(0) { - return false; + + for vertex in 0..self.num_vertices() { + if vertex == self.source || vertex == self.sink { + continue; + } + if self.vertex_balance(config, vertex) != Some(0) { + return crate::types::Or(false); + } } - } - matches!( - self.vertex_balance(config, self.sink), - Some(balance) if balance >= i128::from(self.requirement) - ) + matches!( + self.vertex_balance(config, self.sink), + Some(balance) if balance >= i128::from(self.requirement) + ) + }) } fn variant() -> Vec<(&'static str, &'static str)> { @@ -264,10 +266,8 @@ impl Problem for IntegralFlowBundles { } } -impl WitnessProblem for IntegralFlowBundles {} - crate::declare_variants! { - default sat IntegralFlowBundles => "2^num_arcs", + default IntegralFlowBundles => "2^num_arcs", } #[cfg(feature = "example-db")] diff --git a/src/models/graph/integral_flow_homologous_arcs.rs b/src/models/graph/integral_flow_homologous_arcs.rs index ece6bb465..0798cd834 100644 --- a/src/models/graph/integral_flow_homologous_arcs.rs +++ b/src/models/graph/integral_flow_homologous_arcs.rs @@ -6,7 +6,7 @@ use crate::registry::{FieldInfo, ProblemSchemaEntry, ProblemSizeFieldEntry}; use crate::topology::DirectedGraph; -use crate::traits::{Problem, WitnessProblem}; +use crate::traits::Problem; use serde::{Deserialize, Serialize}; inventory::submit! { @@ -139,7 +139,7 @@ impl IntegralFlowHomologousArcs { } pub fn is_valid_solution(&self, config: &[usize]) -> bool { - self.evaluate(config) + self.evaluate(config).0 } fn domain_size(capacity: u64) -> usize { @@ -152,7 +152,7 @@ impl IntegralFlowHomologousArcs { impl Problem for IntegralFlowHomologousArcs { const NAME: &'static str = "IntegralFlowHomologousArcs"; - type Value = bool; + type Value = crate::types::Or; fn variant() -> Vec<(&'static str, &'static str)> { crate::variant_params![] @@ -165,50 +165,50 @@ impl Problem for IntegralFlowHomologousArcs { .collect() } - fn evaluate(&self, config: &[usize]) -> bool { - if config.len() != self.num_arcs() { - return false; - } + fn evaluate(&self, config: &[usize]) -> crate::types::Or { + crate::types::Or({ + if config.len() != self.num_arcs() { + return crate::types::Or(false); + } - for &(a, b) in &self.homologous_pairs { - if config[a] != config[b] { - return false; + for &(a, b) in &self.homologous_pairs { + if config[a] != config[b] { + return crate::types::Or(false); + } } - } - let mut balances = vec![0_i128; self.num_vertices()]; - for (arc_index, ((u, v), &capacity)) in self - .graph - .arcs() - .into_iter() - .zip(self.capacities.iter()) - .enumerate() - { - let Ok(flow) = u64::try_from(config[arc_index]) else { - return false; - }; - if flow > capacity { - return false; + let mut balances = vec![0_i128; self.num_vertices()]; + for (arc_index, ((u, v), &capacity)) in self + .graph + .arcs() + .into_iter() + .zip(self.capacities.iter()) + .enumerate() + { + let Ok(flow) = u64::try_from(config[arc_index]) else { + return crate::types::Or(false); + }; + if flow > capacity { + return crate::types::Or(false); + } + let flow = i128::from(flow); + balances[u] -= flow; + balances[v] += flow; } - let flow = i128::from(flow); - balances[u] -= flow; - balances[v] += flow; - } - for (vertex, &balance) in balances.iter().enumerate() { - if vertex != self.source && vertex != self.sink && balance != 0 { - return false; + for (vertex, &balance) in balances.iter().enumerate() { + if vertex != self.source && vertex != self.sink && balance != 0 { + return crate::types::Or(false); + } } - } - balances[self.sink] >= i128::from(self.requirement) + balances[self.sink] >= i128::from(self.requirement) + }) } } -impl WitnessProblem for IntegralFlowHomologousArcs {} - crate::declare_variants! { - default sat IntegralFlowHomologousArcs => "(max_capacity + 1)^num_arcs", + default IntegralFlowHomologousArcs => "(max_capacity + 1)^num_arcs", } #[cfg(feature = "example-db")] diff --git a/src/models/graph/integral_flow_with_multipliers.rs b/src/models/graph/integral_flow_with_multipliers.rs index 325f8f6be..d620d4b24 100644 --- a/src/models/graph/integral_flow_with_multipliers.rs +++ b/src/models/graph/integral_flow_with_multipliers.rs @@ -6,7 +6,7 @@ use crate::registry::{FieldInfo, ProblemSchemaEntry, ProblemSizeFieldEntry}; use crate::topology::DirectedGraph; -use crate::traits::{Problem, WitnessProblem}; +use crate::traits::Problem; use serde::{Deserialize, Serialize}; inventory::submit! { @@ -195,7 +195,7 @@ impl IntegralFlowWithMultipliers { impl Problem for IntegralFlowWithMultipliers { const NAME: &'static str = "IntegralFlowWithMultipliers"; - type Value = bool; + type Value = crate::types::Or; fn dims(&self) -> Vec { self.capacities @@ -204,8 +204,8 @@ impl Problem for IntegralFlowWithMultipliers { .collect() } - fn evaluate(&self, config: &[usize]) -> bool { - self.is_feasible(config) + fn evaluate(&self, config: &[usize]) -> crate::types::Or { + crate::types::Or(self.is_feasible(config)) } fn variant() -> Vec<(&'static str, &'static str)> { @@ -213,10 +213,8 @@ impl Problem for IntegralFlowWithMultipliers { } } -impl WitnessProblem for IntegralFlowWithMultipliers {} - crate::declare_variants! { - default sat IntegralFlowWithMultipliers => "(max_capacity + 1)^num_arcs", + default IntegralFlowWithMultipliers => "(max_capacity + 1)^num_arcs", } #[cfg(feature = "example-db")] diff --git a/src/models/graph/isomorphic_spanning_tree.rs b/src/models/graph/isomorphic_spanning_tree.rs index 33fb6ebb6..b340b6c59 100644 --- a/src/models/graph/isomorphic_spanning_tree.rs +++ b/src/models/graph/isomorphic_spanning_tree.rs @@ -6,7 +6,7 @@ use crate::registry::{FieldInfo, ProblemSchemaEntry}; use crate::topology::{Graph, SimpleGraph}; -use crate::traits::{Problem, WitnessProblem}; +use crate::traits::Problem; use serde::{Deserialize, Serialize}; inventory::submit! { @@ -125,37 +125,39 @@ impl IsomorphicSpanningTree { impl Problem for IsomorphicSpanningTree { const NAME: &'static str = "IsomorphicSpanningTree"; - type Value = bool; + type Value = crate::types::Or; fn dims(&self) -> Vec { let n = self.graph.num_vertices(); vec![n; n] } - fn evaluate(&self, config: &[usize]) -> bool { - let n = self.graph.num_vertices(); - if config.len() != n { - return false; - } + fn evaluate(&self, config: &[usize]) -> crate::types::Or { + crate::types::Or({ + let n = self.graph.num_vertices(); + if config.len() != n { + return crate::types::Or(false); + } - // Check that config is a valid permutation: all values in 0..n, all distinct - let mut seen = vec![false; n]; - for &v in config { - if v >= n || seen[v] { - return false; + // Check that config is a valid permutation: all values in 0..n, all distinct + let mut seen = vec![false; n]; + for &v in config { + if v >= n || seen[v] { + return crate::types::Or(false); + } + seen[v] = true; } - seen[v] = true; - } - // Check that every tree edge maps to a graph edge under the permutation - // config[i] = π(i): tree vertex i maps to graph vertex config[i] - for (u, v) in self.tree.edges() { - if !self.graph.has_edge(config[u], config[v]) { - return false; + // Check that every tree edge maps to a graph edge under the permutation + // config[i] = π(i): tree vertex i maps to graph vertex config[i] + for (u, v) in self.tree.edges() { + if !self.graph.has_edge(config[u], config[v]) { + return crate::types::Or(false); + } } - } - true + true + }) } fn variant() -> Vec<(&'static str, &'static str)> { @@ -163,8 +165,6 @@ impl Problem for IsomorphicSpanningTree { } } -impl WitnessProblem for IsomorphicSpanningTree {} - #[cfg(feature = "example-db")] pub(crate) fn canonical_model_example_specs() -> Vec { vec![crate::example_db::specs::ModelExampleSpec { @@ -179,7 +179,7 @@ pub(crate) fn canonical_model_example_specs() -> Vec "factorial(num_vertices)", + default IsomorphicSpanningTree => "factorial(num_vertices)", } #[cfg(test)] diff --git a/src/models/graph/kclique.rs b/src/models/graph/kclique.rs index 930bc8385..d0b84cbcf 100644 --- a/src/models/graph/kclique.rs +++ b/src/models/graph/kclique.rs @@ -5,7 +5,7 @@ use crate::registry::{FieldInfo, ProblemSchemaEntry, VariantDimension}; use crate::topology::{Graph, SimpleGraph}; -use crate::traits::{Problem, WitnessProblem}; +use crate::traits::Problem; use serde::{Deserialize, Serialize}; inventory::submit! { @@ -73,7 +73,7 @@ where G: Graph + crate::variant::VariantParam, { const NAME: &'static str = "KClique"; - type Value = bool; + type Value = crate::types::Or; fn variant() -> Vec<(&'static str, &'static str)> { crate::variant_params![G] @@ -83,13 +83,11 @@ where vec![2; self.graph.num_vertices()] } - fn evaluate(&self, config: &[usize]) -> bool { - is_kclique_config(&self.graph, config, self.k) + fn evaluate(&self, config: &[usize]) -> crate::types::Or { + crate::types::Or(is_kclique_config(&self.graph, config, self.k)) } } -impl WitnessProblem for KClique where G: Graph + crate::variant::VariantParam {} - fn is_kclique_config(graph: &G, config: &[usize], k: usize) -> bool { if config.len() != graph.num_vertices() { return false; @@ -124,7 +122,7 @@ fn is_kclique_config(graph: &G, config: &[usize], k: usize) -> bool { } crate::declare_variants! { - default sat KClique => "1.1996^num_vertices", + default KClique => "1.1996^num_vertices", } #[cfg(feature = "example-db")] diff --git a/src/models/graph/kcoloring.rs b/src/models/graph/kcoloring.rs index 4c9a8f679..9e810dfc9 100644 --- a/src/models/graph/kcoloring.rs +++ b/src/models/graph/kcoloring.rs @@ -5,7 +5,7 @@ use crate::registry::{FieldInfo, ProblemSchemaEntry, VariantDimension}; use crate::topology::{Graph, SimpleGraph}; -use crate::traits::{Problem, WitnessProblem}; +use crate::traits::Problem; use crate::variant::{KValue, VariantParam, K2, K3, K4, K5, KN}; use serde::{Deserialize, Serialize}; @@ -145,7 +145,7 @@ where G: Graph + VariantParam, { const NAME: &'static str = "KColoring"; - type Value = bool; + type Value = crate::types::Or; fn variant() -> Vec<(&'static str, &'static str)> { crate::variant_params![K, G] @@ -155,13 +155,11 @@ where vec![self.num_colors; self.graph.num_vertices()] } - fn evaluate(&self, config: &[usize]) -> bool { - self.is_valid_coloring(config) + fn evaluate(&self, config: &[usize]) -> crate::types::Or { + crate::types::Or(self.is_valid_coloring(config)) } } -impl WitnessProblem for KColoring {} - /// Check if a coloring is valid for a graph. /// /// # Panics @@ -203,12 +201,12 @@ pub(crate) fn canonical_model_example_specs() -> Vec => "2^num_vertices", - sat KColoring => "num_vertices + num_edges", - sat KColoring => "1.3289^num_vertices", - sat KColoring => "1.7159^num_vertices", + default KColoring => "2^num_vertices", + KColoring => "num_vertices + num_edges", + KColoring => "1.3289^num_vertices", + KColoring => "1.7159^num_vertices", // Best known: O*((2-ε)^n) for some ε > 0 (Zamir 2021), concrete ε unknown - sat KColoring => "2^num_vertices", + KColoring => "2^num_vertices", } #[cfg(test)] diff --git a/src/models/graph/kth_best_spanning_tree.rs b/src/models/graph/kth_best_spanning_tree.rs index 476566cad..f26c26fcb 100644 --- a/src/models/graph/kth_best_spanning_tree.rs +++ b/src/models/graph/kth_best_spanning_tree.rs @@ -5,7 +5,7 @@ use crate::registry::{FieldInfo, ProblemSchemaEntry, VariantDimension}; use crate::topology::{Graph, SimpleGraph}; -use crate::traits::{Problem, WitnessProblem}; +use crate::traits::Problem; use crate::types::WeightElement; use num_traits::Zero; use serde::{Deserialize, Serialize}; @@ -207,7 +207,7 @@ where W: WeightElement + crate::variant::VariantParam, { const NAME: &'static str = "KthBestSpanningTree"; - type Value = bool; + type Value = crate::types::Or; fn variant() -> Vec<(&'static str, &'static str)> { crate::variant_params![W] @@ -217,16 +217,11 @@ where vec![2; self.k * self.graph.num_edges()] } - fn evaluate(&self, config: &[usize]) -> bool { - self.evaluate_config(config) + fn evaluate(&self, config: &[usize]) -> crate::types::Or { + crate::types::Or(self.evaluate_config(config)) } } -impl WitnessProblem for KthBestSpanningTree where - W: WeightElement + crate::variant::VariantParam -{ -} - #[cfg(feature = "example-db")] pub(crate) fn canonical_model_example_specs() -> Vec { // K4 with weights [1,1,2,2,2,3], k=2, B=4. @@ -245,7 +240,7 @@ pub(crate) fn canonical_model_example_specs() -> Vec => "2^(num_edges * k)", + default KthBestSpanningTree => "2^(num_edges * k)", } #[cfg(test)] diff --git a/src/models/graph/length_bounded_disjoint_paths.rs b/src/models/graph/length_bounded_disjoint_paths.rs index 692e9d163..339ec25eb 100644 --- a/src/models/graph/length_bounded_disjoint_paths.rs +++ b/src/models/graph/length_bounded_disjoint_paths.rs @@ -5,7 +5,7 @@ use crate::registry::{FieldInfo, ProblemSchemaEntry, VariantDimension}; use crate::topology::{Graph, SimpleGraph}; -use crate::traits::{Problem, WitnessProblem}; +use crate::traits::Problem; use crate::variant::VariantParam; use serde::{Deserialize, Serialize}; @@ -135,7 +135,7 @@ where G: Graph + VariantParam, { const NAME: &'static str = "LengthBoundedDisjointPaths"; - type Value = bool; + type Value = crate::types::Or; fn variant() -> Vec<(&'static str, &'static str)> { crate::variant_params![G] @@ -145,13 +145,11 @@ where vec![2; self.num_paths_required * self.graph.num_vertices()] } - fn evaluate(&self, config: &[usize]) -> bool { - self.is_valid_solution(config) + fn evaluate(&self, config: &[usize]) -> crate::types::Or { + crate::types::Or(self.is_valid_solution(config)) } } -impl WitnessProblem for LengthBoundedDisjointPaths {} - fn is_valid_path_collection( graph: &G, source: usize, @@ -315,7 +313,7 @@ pub(crate) fn canonical_model_example_specs() -> Vec => "2^(num_paths_required * num_vertices)", + default LengthBoundedDisjointPaths => "2^(num_paths_required * num_vertices)", } #[cfg(test)] diff --git a/src/models/graph/longest_circuit.rs b/src/models/graph/longest_circuit.rs index d4ede94d0..db22d76fd 100644 --- a/src/models/graph/longest_circuit.rs +++ b/src/models/graph/longest_circuit.rs @@ -5,7 +5,7 @@ use crate::registry::{FieldInfo, ProblemSchemaEntry, VariantDimension}; use crate::topology::{Graph, SimpleGraph}; -use crate::traits::{Problem, WitnessProblem}; +use crate::traits::Problem; use crate::types::WeightElement; use num_traits::Zero; use serde::{Deserialize, Serialize}; @@ -161,7 +161,7 @@ where W: WeightElement + crate::variant::VariantParam, { const NAME: &'static str = "LongestCircuit"; - type Value = bool; + type Value = crate::types::Or; fn variant() -> Vec<(&'static str, &'static str)> { crate::variant_params![G, W] @@ -171,8 +171,8 @@ where vec![2; self.graph.num_edges()] } - fn evaluate(&self, config: &[usize]) -> bool { - self.is_valid_solution(config) + fn evaluate(&self, config: &[usize]) -> crate::types::Or { + crate::types::Or(self.is_valid_solution(config)) } } @@ -242,13 +242,6 @@ pub(crate) fn is_simple_circuit(graph: &G, config: &[usize]) -> bool { visited_selected_vertices == selected_vertices.len() } -impl WitnessProblem for LongestCircuit -where - G: Graph + crate::variant::VariantParam, - W: WeightElement + crate::variant::VariantParam, -{ -} - #[cfg(feature = "example-db")] pub(crate) fn canonical_model_example_specs() -> Vec { vec![crate::example_db::specs::ModelExampleSpec { @@ -278,7 +271,7 @@ pub(crate) fn canonical_model_example_specs() -> Vec => "2^num_vertices * num_vertices^2", + default LongestCircuit => "2^num_vertices * num_vertices^2", } #[cfg(test)] diff --git a/src/models/graph/longest_path.rs b/src/models/graph/longest_path.rs index 12bf7f5c9..fd70dbeb3 100644 --- a/src/models/graph/longest_path.rs +++ b/src/models/graph/longest_path.rs @@ -5,8 +5,8 @@ use crate::registry::{FieldInfo, ProblemSchemaEntry, VariantDimension}; use crate::topology::{Graph, SimpleGraph}; -use crate::traits::{ObjectiveProblem, Problem}; -use crate::types::{ExtremumSense, Max, One, WeightElement}; +use crate::traits::Problem; +use crate::types::{Max, One, WeightElement}; use num_traits::Zero; use serde::{Deserialize, Serialize}; use std::collections::VecDeque; @@ -175,18 +175,6 @@ where } } -impl ObjectiveProblem for LongestPath -where - G: Graph + crate::variant::VariantParam, - W: WeightElement + crate::variant::VariantParam, -{ - type Objective = W::Sum; - - fn direction(&self) -> ExtremumSense { - ExtremumSense::Maximize - } -} - fn is_simple_st_path( graph: &G, source_vertex: usize, @@ -265,8 +253,8 @@ fn is_simple_st_path( } crate::declare_variants! { - default opt LongestPath => "num_vertices * 2^num_vertices", - opt LongestPath => "num_vertices * 2^num_vertices", + default LongestPath => "num_vertices * 2^num_vertices", + LongestPath => "num_vertices * 2^num_vertices", } #[cfg(feature = "example-db")] diff --git a/src/models/graph/max_cut.rs b/src/models/graph/max_cut.rs index 8355538cc..b3c22b0db 100644 --- a/src/models/graph/max_cut.rs +++ b/src/models/graph/max_cut.rs @@ -5,8 +5,8 @@ use crate::registry::{FieldInfo, ProblemSchemaEntry, VariantDimension}; use crate::topology::{Graph, SimpleGraph}; -use crate::traits::{ObjectiveProblem, Problem}; -use crate::types::{ExtremumSense, Max, WeightElement}; +use crate::traits::Problem; +use crate::types::{Max, WeightElement}; use num_traits::Zero; use serde::{Deserialize, Serialize}; @@ -188,18 +188,6 @@ where } } -impl ObjectiveProblem for MaxCut -where - G: Graph + crate::variant::VariantParam, - W: WeightElement + crate::variant::VariantParam, -{ - type Objective = W::Sum; - - fn direction(&self) -> ExtremumSense { - ExtremumSense::Maximize - } -} - /// Compute the total weight of edges crossing the cut. /// /// # Arguments @@ -221,7 +209,7 @@ where } crate::declare_variants! { - default opt MaxCut => "2^(2.372 * num_vertices / 3)", + default MaxCut => "2^(2.372 * num_vertices / 3)", } #[cfg(feature = "example-db")] diff --git a/src/models/graph/maximal_is.rs b/src/models/graph/maximal_is.rs index bf5893981..b08c37d4b 100644 --- a/src/models/graph/maximal_is.rs +++ b/src/models/graph/maximal_is.rs @@ -5,8 +5,8 @@ use crate::registry::{FieldInfo, ProblemSchemaEntry, VariantDimension}; use crate::topology::{Graph, SimpleGraph}; -use crate::traits::{ObjectiveProblem, Problem}; -use crate::types::{ExtremumSense, Max, WeightElement}; +use crate::traits::Problem; +use crate::types::{Max, WeightElement}; use num_traits::Zero; use serde::{Deserialize, Serialize}; @@ -177,18 +177,6 @@ where } } -impl ObjectiveProblem for MaximalIS -where - G: Graph + crate::variant::VariantParam, - W: WeightElement + crate::variant::VariantParam, -{ - type Objective = W::Sum; - - fn direction(&self) -> ExtremumSense { - ExtremumSense::Maximize - } -} - #[cfg(feature = "example-db")] pub(crate) fn canonical_model_example_specs() -> Vec { vec![crate::example_db::specs::ModelExampleSpec { @@ -235,7 +223,7 @@ pub(crate) fn is_maximal_independent_set(graph: &G, selected: &[bool]) } crate::declare_variants! { - default opt MaximalIS => "3^(num_vertices / 3)", + default MaximalIS => "3^(num_vertices / 3)", } #[cfg(test)] diff --git a/src/models/graph/maximum_clique.rs b/src/models/graph/maximum_clique.rs index 78196e2e6..6dd4cb284 100644 --- a/src/models/graph/maximum_clique.rs +++ b/src/models/graph/maximum_clique.rs @@ -5,8 +5,8 @@ use crate::registry::{FieldInfo, ProblemSchemaEntry, VariantDimension}; use crate::topology::{Graph, SimpleGraph}; -use crate::traits::{ObjectiveProblem, Problem}; -use crate::types::{ExtremumSense, Max, WeightElement}; +use crate::traits::Problem; +use crate::types::{Max, WeightElement}; use num_traits::Zero; use serde::{Deserialize, Serialize}; @@ -143,18 +143,6 @@ where } } -impl ObjectiveProblem for MaximumClique -where - G: Graph + crate::variant::VariantParam, - W: WeightElement + crate::variant::VariantParam, -{ - type Objective = W::Sum; - - fn direction(&self) -> ExtremumSense { - ExtremumSense::Maximize - } -} - /// Check if a configuration forms a valid clique. fn is_clique_config(graph: &G, config: &[usize]) -> bool { // Collect all selected vertices @@ -177,7 +165,7 @@ fn is_clique_config(graph: &G, config: &[usize]) -> bool { } crate::declare_variants! { - default opt MaximumClique => "1.1996^num_vertices", + default MaximumClique => "1.1996^num_vertices", } #[cfg(feature = "example-db")] diff --git a/src/models/graph/maximum_independent_set.rs b/src/models/graph/maximum_independent_set.rs index 892509b6c..f72ab3679 100644 --- a/src/models/graph/maximum_independent_set.rs +++ b/src/models/graph/maximum_independent_set.rs @@ -5,8 +5,8 @@ use crate::registry::{FieldInfo, ProblemSchemaEntry, VariantDimension}; use crate::topology::{Graph, KingsSubgraph, SimpleGraph, TriangularSubgraph, UnitDiskGraph}; -use crate::traits::{ObjectiveProblem, Problem}; -use crate::types::{ExtremumSense, Max, One, WeightElement}; +use crate::traits::Problem; +use crate::types::{Max, One, WeightElement}; use num_traits::Zero; use serde::{Deserialize, Serialize}; @@ -143,18 +143,6 @@ where } } -impl ObjectiveProblem for MaximumIndependentSet -where - G: Graph + crate::variant::VariantParam, - W: WeightElement + crate::variant::VariantParam, -{ - type Objective = W::Sum; - - fn direction(&self) -> ExtremumSense { - ExtremumSense::Maximize - } -} - /// Check if a configuration forms a valid independent set. fn is_independent_set_config(graph: &G, config: &[usize]) -> bool { for (u, v) in graph.edges() { @@ -166,13 +154,13 @@ fn is_independent_set_config(graph: &G, config: &[usize]) -> bool { } crate::declare_variants! { - opt MaximumIndependentSet => "1.1996^num_vertices", - default opt MaximumIndependentSet => "1.1996^num_vertices", - opt MaximumIndependentSet => "2^sqrt(num_vertices)", - opt MaximumIndependentSet => "2^sqrt(num_vertices)", - opt MaximumIndependentSet => "2^sqrt(num_vertices)", - opt MaximumIndependentSet => "2^sqrt(num_vertices)", - opt MaximumIndependentSet => "2^sqrt(num_vertices)", + MaximumIndependentSet => "1.1996^num_vertices", + default MaximumIndependentSet => "1.1996^num_vertices", + MaximumIndependentSet => "2^sqrt(num_vertices)", + MaximumIndependentSet => "2^sqrt(num_vertices)", + MaximumIndependentSet => "2^sqrt(num_vertices)", + MaximumIndependentSet => "2^sqrt(num_vertices)", + MaximumIndependentSet => "2^sqrt(num_vertices)", } #[cfg(feature = "example-db")] diff --git a/src/models/graph/maximum_matching.rs b/src/models/graph/maximum_matching.rs index 4f0585ba0..f2c3d0719 100644 --- a/src/models/graph/maximum_matching.rs +++ b/src/models/graph/maximum_matching.rs @@ -5,8 +5,8 @@ use crate::registry::{FieldInfo, ProblemSchemaEntry, VariantDimension}; use crate::topology::{Graph, SimpleGraph}; -use crate::traits::{ObjectiveProblem, Problem}; -use crate::types::{ExtremumSense, Max, WeightElement}; +use crate::traits::Problem; +use crate::types::{Max, WeightElement}; use num_traits::Zero; use serde::{Deserialize, Serialize}; use std::collections::HashMap; @@ -213,20 +213,8 @@ where } } -impl ObjectiveProblem for MaximumMatching -where - G: Graph + crate::variant::VariantParam, - W: WeightElement + crate::variant::VariantParam, -{ - type Objective = W::Sum; - - fn direction(&self) -> ExtremumSense { - ExtremumSense::Maximize - } -} - crate::declare_variants! { - default opt MaximumMatching => "num_vertices^3", + default MaximumMatching => "num_vertices^3", } #[cfg(feature = "example-db")] diff --git a/src/models/graph/min_max_multicenter.rs b/src/models/graph/min_max_multicenter.rs index fdb959328..98e2c4614 100644 --- a/src/models/graph/min_max_multicenter.rs +++ b/src/models/graph/min_max_multicenter.rs @@ -6,7 +6,7 @@ use crate::registry::{FieldInfo, ProblemSchemaEntry, VariantDimension}; use crate::topology::{Graph, SimpleGraph}; -use crate::traits::{Problem, WitnessProblem}; +use crate::traits::Problem; use crate::types::WeightElement; use num_traits::Zero; use serde::{Deserialize, Serialize}; @@ -246,7 +246,7 @@ where W: WeightElement + crate::variant::VariantParam, { const NAME: &'static str = "MinMaxMulticenter"; - type Value = bool; + type Value = crate::types::Or; fn variant() -> Vec<(&'static str, &'static str)> { crate::variant_params![G, W] @@ -256,46 +256,44 @@ where vec![2; self.graph.num_vertices()] } - fn evaluate(&self, config: &[usize]) -> bool { - if config.len() != self.graph.num_vertices() || config.iter().any(|&selected| selected > 1) - { - return false; - } + fn evaluate(&self, config: &[usize]) -> crate::types::Or { + crate::types::Or({ + if config.len() != self.graph.num_vertices() + || config.iter().any(|&selected| selected > 1) + { + return crate::types::Or(false); + } - // Check exactly K centers are selected - let num_selected = config.iter().filter(|&&selected| selected == 1).count(); - if num_selected != self.k { - return false; - } + // Check exactly K centers are selected + let num_selected = config.iter().filter(|&&selected| selected == 1).count(); + if num_selected != self.k { + return crate::types::Or(false); + } - // Compute shortest distances to nearest center - let distances = match self.shortest_distances(config) { - Some(d) => d, - None => return false, - }; + // Compute shortest distances to nearest center + let distances = match self.shortest_distances(config) { + Some(d) => d, + None => { + return crate::types::Or(false); + } + }; - // Compute max weighted distance: max_{v} w(v) * d(v) - let mut max_wd = W::Sum::zero(); - for (v, dist) in distances.iter().enumerate() { - let wd = self.vertex_weights[v].to_sum() * dist.clone(); - if wd > max_wd { - max_wd = wd; + // Compute max weighted distance: max_{v} w(v) * d(v) + let mut max_wd = W::Sum::zero(); + for (v, dist) in distances.iter().enumerate() { + let wd = self.vertex_weights[v].to_sum() * dist.clone(); + if wd > max_wd { + max_wd = wd; + } } - } - max_wd <= self.bound + max_wd <= self.bound + }) } } -impl WitnessProblem for MinMaxMulticenter -where - G: Graph + crate::variant::VariantParam, - W: WeightElement + crate::variant::VariantParam, -{ -} - crate::declare_variants! { - default sat MinMaxMulticenter => "1.4969^num_vertices", + default MinMaxMulticenter => "1.4969^num_vertices", } #[cfg(feature = "example-db")] diff --git a/src/models/graph/minimum_cut_into_bounded_sets.rs b/src/models/graph/minimum_cut_into_bounded_sets.rs index 9afb02840..ed221d11b 100644 --- a/src/models/graph/minimum_cut_into_bounded_sets.rs +++ b/src/models/graph/minimum_cut_into_bounded_sets.rs @@ -6,7 +6,7 @@ use crate::registry::{FieldInfo, ProblemSchemaEntry, VariantDimension}; use crate::topology::{Graph, SimpleGraph}; -use crate::traits::{Problem, WitnessProblem}; +use crate::traits::Problem; use crate::types::WeightElement; use num_traits::Zero; use serde::{Deserialize, Serialize}; @@ -164,7 +164,7 @@ where W: WeightElement + crate::variant::VariantParam, { const NAME: &'static str = "MinimumCutIntoBoundedSets"; - type Value = bool; + type Value = crate::types::Or; fn variant() -> Vec<(&'static str, &'static str)> { crate::variant_params![G, W] @@ -174,47 +174,42 @@ where vec![2; self.graph.num_vertices()] } - fn evaluate(&self, config: &[usize]) -> bool { - let n = self.graph.num_vertices(); - if config.len() != n { - return false; - } + fn evaluate(&self, config: &[usize]) -> crate::types::Or { + crate::types::Or({ + let n = self.graph.num_vertices(); + if config.len() != n { + return crate::types::Or(false); + } - // Check source is in V1 (config=0) and sink is in V2 (config=1) - if config[self.source] != 0 { - return false; - } - if config[self.sink] != 1 { - return false; - } + // Check source is in V1 (config=0) and sink is in V2 (config=1) + if config[self.source] != 0 { + return crate::types::Or(false); + } + if config[self.sink] != 1 { + return crate::types::Or(false); + } - // Check size bounds - let count_v1 = config.iter().filter(|&&x| x == 0).count(); - let count_v2 = config.iter().filter(|&&x| x == 1).count(); - if count_v1 > self.size_bound || count_v2 > self.size_bound { - return false; - } + // Check size bounds + let count_v1 = config.iter().filter(|&&x| x == 0).count(); + let count_v2 = config.iter().filter(|&&x| x == 1).count(); + if count_v1 > self.size_bound || count_v2 > self.size_bound { + return crate::types::Or(false); + } - // Compute cut weight - let mut cut_weight = W::Sum::zero(); - for ((u, v), weight) in self.graph.edges().iter().zip(self.edge_weights.iter()) { - if config[*u] != config[*v] { - cut_weight += weight.to_sum(); + // Compute cut weight + let mut cut_weight = W::Sum::zero(); + for ((u, v), weight) in self.graph.edges().iter().zip(self.edge_weights.iter()) { + if config[*u] != config[*v] { + cut_weight += weight.to_sum(); + } } - } - // Check cut weight <= K - cut_weight <= self.cut_bound + // Check cut weight <= K + cut_weight <= self.cut_bound + }) } } -impl WitnessProblem for MinimumCutIntoBoundedSets -where - G: Graph + crate::variant::VariantParam, - W: WeightElement + crate::variant::VariantParam, -{ -} - #[cfg(feature = "example-db")] pub(crate) fn canonical_model_example_specs() -> Vec { vec![crate::example_db::specs::ModelExampleSpec { @@ -250,7 +245,7 @@ pub(crate) fn canonical_model_example_specs() -> Vec => "2^num_vertices", + default MinimumCutIntoBoundedSets => "2^num_vertices", } #[cfg(test)] diff --git a/src/models/graph/minimum_dominating_set.rs b/src/models/graph/minimum_dominating_set.rs index 38aa2fd83..8b3f69e52 100644 --- a/src/models/graph/minimum_dominating_set.rs +++ b/src/models/graph/minimum_dominating_set.rs @@ -5,8 +5,8 @@ use crate::registry::{FieldInfo, ProblemSchemaEntry, VariantDimension}; use crate::topology::{Graph, SimpleGraph}; -use crate::traits::{ObjectiveProblem, Problem}; -use crate::types::{ExtremumSense, Min, WeightElement}; +use crate::traits::Problem; +use crate::types::{Min, WeightElement}; use num_traits::Zero; use serde::{Deserialize, Serialize}; use std::collections::HashSet; @@ -163,20 +163,8 @@ where } } -impl ObjectiveProblem for MinimumDominatingSet -where - G: Graph + crate::variant::VariantParam, - W: WeightElement + crate::variant::VariantParam, -{ - type Objective = W::Sum; - - fn direction(&self) -> ExtremumSense { - ExtremumSense::Minimize - } -} - crate::declare_variants! { - default opt MinimumDominatingSet => "1.4969^num_vertices", + default MinimumDominatingSet => "1.4969^num_vertices", } #[cfg(feature = "example-db")] diff --git a/src/models/graph/minimum_dummy_activities_pert.rs b/src/models/graph/minimum_dummy_activities_pert.rs index 9379ce2eb..c4a8dbdb0 100644 --- a/src/models/graph/minimum_dummy_activities_pert.rs +++ b/src/models/graph/minimum_dummy_activities_pert.rs @@ -9,8 +9,8 @@ use crate::registry::{FieldInfo, ProblemSchemaEntry}; use crate::topology::DirectedGraph; -use crate::traits::{ObjectiveProblem, Problem}; -use crate::types::{ExtremumSense, Min}; +use crate::traits::Problem; +use crate::types::Min; use serde::{Deserialize, Deserializer, Serialize}; use std::collections::{BTreeMap, BTreeSet}; @@ -200,16 +200,8 @@ impl Problem for MinimumDummyActivitiesPert { } } -impl ObjectiveProblem for MinimumDummyActivitiesPert { - type Objective = i32; - - fn direction(&self) -> ExtremumSense { - ExtremumSense::Minimize - } -} - crate::declare_variants! { - default opt MinimumDummyActivitiesPert => "2^num_arcs", + default MinimumDummyActivitiesPert => "2^num_arcs", } #[cfg(feature = "example-db")] diff --git a/src/models/graph/minimum_feedback_arc_set.rs b/src/models/graph/minimum_feedback_arc_set.rs index 12933c495..a69fa1aa6 100644 --- a/src/models/graph/minimum_feedback_arc_set.rs +++ b/src/models/graph/minimum_feedback_arc_set.rs @@ -5,8 +5,8 @@ use crate::registry::{FieldInfo, ProblemSchemaEntry, VariantDimension}; use crate::topology::DirectedGraph; -use crate::traits::{ObjectiveProblem, Problem}; -use crate::types::{ExtremumSense, Min, WeightElement}; +use crate::traits::Problem; +use crate::types::{Min, WeightElement}; use num_traits::Zero; use serde::{Deserialize, Serialize}; @@ -150,17 +150,6 @@ where } } -impl ObjectiveProblem for MinimumFeedbackArcSet -where - W: WeightElement + crate::variant::VariantParam, -{ - type Objective = W::Sum; - - fn direction(&self) -> ExtremumSense { - ExtremumSense::Minimize - } -} - /// Check if a configuration forms a valid feedback arc set. /// /// config[i] = 1 means arc i is selected for removal. @@ -176,7 +165,7 @@ fn is_valid_fas(graph: &DirectedGraph, config: &[usize]) -> bool { } crate::declare_variants! { - default opt MinimumFeedbackArcSet => "2^num_vertices", + default MinimumFeedbackArcSet => "2^num_vertices", } #[cfg(feature = "example-db")] diff --git a/src/models/graph/minimum_feedback_vertex_set.rs b/src/models/graph/minimum_feedback_vertex_set.rs index 180e8b4df..fe6b277e4 100644 --- a/src/models/graph/minimum_feedback_vertex_set.rs +++ b/src/models/graph/minimum_feedback_vertex_set.rs @@ -5,8 +5,8 @@ use crate::registry::{FieldInfo, ProblemSchemaEntry, VariantDimension}; use crate::topology::DirectedGraph; -use crate::traits::{ObjectiveProblem, Problem}; -use crate::types::{ExtremumSense, Min, WeightElement}; +use crate::traits::Problem; +use crate::types::{Min, WeightElement}; use num_traits::Zero; use serde::{Deserialize, Serialize}; @@ -152,19 +152,8 @@ where } } -impl ObjectiveProblem for MinimumFeedbackVertexSet -where - W: WeightElement + crate::variant::VariantParam, -{ - type Objective = W::Sum; - - fn direction(&self) -> ExtremumSense { - ExtremumSense::Minimize - } -} - crate::declare_variants! { - default opt MinimumFeedbackVertexSet => "1.9977^num_vertices", + default MinimumFeedbackVertexSet => "1.9977^num_vertices", } #[cfg(feature = "example-db")] diff --git a/src/models/graph/minimum_multiway_cut.rs b/src/models/graph/minimum_multiway_cut.rs index 2b6d31071..010a27bb7 100644 --- a/src/models/graph/minimum_multiway_cut.rs +++ b/src/models/graph/minimum_multiway_cut.rs @@ -5,8 +5,8 @@ use crate::registry::{FieldInfo, ProblemSchemaEntry, VariantDimension}; use crate::topology::{Graph, SimpleGraph}; -use crate::traits::{ObjectiveProblem, Problem}; -use crate::types::{ExtremumSense, Min, WeightElement}; +use crate::traits::Problem; +use crate::types::{Min, WeightElement}; use num_traits::Zero; use serde::{Deserialize, Serialize}; use std::collections::VecDeque; @@ -187,20 +187,8 @@ where } } -impl ObjectiveProblem for MinimumMultiwayCut -where - G: Graph + crate::variant::VariantParam, - W: WeightElement + crate::variant::VariantParam, -{ - type Objective = W::Sum; - - fn direction(&self) -> ExtremumSense { - ExtremumSense::Minimize - } -} - crate::declare_variants! { - default opt MinimumMultiwayCut => "1.84^num_terminals * num_vertices^3", + default MinimumMultiwayCut => "1.84^num_terminals * num_vertices^3", } #[cfg(feature = "example-db")] diff --git a/src/models/graph/minimum_sum_multicenter.rs b/src/models/graph/minimum_sum_multicenter.rs index cb72b2370..e631f0d7c 100644 --- a/src/models/graph/minimum_sum_multicenter.rs +++ b/src/models/graph/minimum_sum_multicenter.rs @@ -5,8 +5,8 @@ use crate::registry::{FieldInfo, ProblemSchemaEntry, VariantDimension}; use crate::topology::{Graph, SimpleGraph}; -use crate::traits::{ObjectiveProblem, Problem}; -use crate::types::{ExtremumSense, Min, WeightElement}; +use crate::traits::Problem; +use crate::types::{Min, WeightElement}; use num_traits::Zero; use serde::{Deserialize, Serialize}; @@ -247,20 +247,8 @@ where } } -impl ObjectiveProblem for MinimumSumMulticenter -where - G: Graph + crate::variant::VariantParam, - W: WeightElement + crate::variant::VariantParam, -{ - type Objective = W::Sum; - - fn direction(&self) -> ExtremumSense { - ExtremumSense::Minimize - } -} - crate::declare_variants! { - default opt MinimumSumMulticenter => "2^num_vertices", + default MinimumSumMulticenter => "2^num_vertices", } #[cfg(feature = "example-db")] diff --git a/src/models/graph/minimum_vertex_cover.rs b/src/models/graph/minimum_vertex_cover.rs index 1bb96c02b..b8586b57a 100644 --- a/src/models/graph/minimum_vertex_cover.rs +++ b/src/models/graph/minimum_vertex_cover.rs @@ -5,8 +5,8 @@ use crate::registry::{FieldInfo, ProblemSchemaEntry, VariantDimension}; use crate::topology::{Graph, SimpleGraph}; -use crate::traits::{ObjectiveProblem, Problem}; -use crate::types::{ExtremumSense, Min, WeightElement}; +use crate::traits::Problem; +use crate::types::{Min, WeightElement}; use num_traits::Zero; use serde::{Deserialize, Serialize}; @@ -138,18 +138,6 @@ where } } -impl ObjectiveProblem for MinimumVertexCover -where - G: Graph + crate::variant::VariantParam, - W: WeightElement + crate::variant::VariantParam, -{ - type Objective = W::Sum; - - fn direction(&self) -> ExtremumSense { - ExtremumSense::Minimize - } -} - /// Check if a configuration forms a valid vertex cover. fn is_vertex_cover_config(graph: &G, config: &[usize]) -> bool { for (u, v) in graph.edges() { @@ -163,7 +151,7 @@ fn is_vertex_cover_config(graph: &G, config: &[usize]) -> bool { } crate::declare_variants! { - default opt MinimumVertexCover => "1.1996^num_vertices", + default MinimumVertexCover => "1.1996^num_vertices", } #[cfg(feature = "example-db")] diff --git a/src/models/graph/mixed_chinese_postman.rs b/src/models/graph/mixed_chinese_postman.rs index 8b2ea519e..00a3e0ea6 100644 --- a/src/models/graph/mixed_chinese_postman.rs +++ b/src/models/graph/mixed_chinese_postman.rs @@ -7,7 +7,7 @@ use crate::registry::{FieldInfo, ProblemSchemaEntry, VariantDimension}; use crate::topology::{DirectedGraph, MixedGraph}; -use crate::traits::{Problem, WitnessProblem}; +use crate::traits::Problem; use crate::types::{One, WeightElement}; use num_traits::Zero; use serde::{Deserialize, Serialize}; @@ -209,7 +209,7 @@ where { /// Check whether a configuration is satisfying. pub fn is_valid_solution(&self, config: &[usize]) -> bool { - self.evaluate(config) + self.evaluate(config).0 } } @@ -218,7 +218,7 @@ where W: WeightElement + crate::variant::VariantParam, { const NAME: &'static str = "MixedChinesePostman"; - type Value = bool; + type Value = crate::types::Or; fn variant() -> Vec<(&'static str, &'static str)> { crate::variant_params![W] @@ -228,42 +228,41 @@ where vec![2; self.graph.num_edges()] } - fn evaluate(&self, config: &[usize]) -> bool { - let Some(oriented_pairs) = self.oriented_arc_pairs(config) else { - return false; - }; - - // Connectivity uses the full available graph: original arcs plus both - // directions of every undirected edge. - if !DirectedGraph::new(self.graph.num_vertices(), self.available_arc_pairs()) - .is_strongly_connected() - { - return false; - } + fn evaluate(&self, config: &[usize]) -> crate::types::Or { + crate::types::Or({ + let Some(oriented_pairs) = self.oriented_arc_pairs(config) else { + return crate::types::Or(false); + }; + + // Connectivity uses the full available graph: original arcs plus both + // directions of every undirected edge. + if !DirectedGraph::new(self.graph.num_vertices(), self.available_arc_pairs()) + .is_strongly_connected() + { + return crate::types::Or(false); + } - // Shortest paths also use the full available graph so that balancing - // can route through undirected edges in either direction. - let distances = - all_pairs_shortest_paths(self.graph.num_vertices(), &self.weighted_available_arcs()); - // Degree imbalance is computed from the required arcs only (original - // arcs plus the chosen orientation of each undirected edge). - let balance = degree_imbalances(self.graph.num_vertices(), &oriented_pairs); - let Some(extra_cost) = minimum_balancing_cost(&balance, &distances) else { - return false; - }; - - self.base_cost() + extra_cost <= i64::from(self.bound) + // Shortest paths also use the full available graph so that balancing + // can route through undirected edges in either direction. + let distances = all_pairs_shortest_paths( + self.graph.num_vertices(), + &self.weighted_available_arcs(), + ); + // Degree imbalance is computed from the required arcs only (original + // arcs plus the chosen orientation of each undirected edge). + let balance = degree_imbalances(self.graph.num_vertices(), &oriented_pairs); + let Some(extra_cost) = minimum_balancing_cost(&balance, &distances) else { + return crate::types::Or(false); + }; + + self.base_cost() + extra_cost <= i64::from(self.bound) + }) } } -impl WitnessProblem for MixedChinesePostman where - W: WeightElement + crate::variant::VariantParam -{ -} - crate::declare_variants! { - default sat MixedChinesePostman => "2^num_edges * num_vertices^3", - sat MixedChinesePostman => "2^num_edges * num_vertices^3", + default MixedChinesePostman => "2^num_edges * num_vertices^3", + MixedChinesePostman => "2^num_edges * num_vertices^3", } #[cfg(feature = "example-db")] diff --git a/src/models/graph/multiple_choice_branching.rs b/src/models/graph/multiple_choice_branching.rs index 82c15b7ff..c0e90cdba 100644 --- a/src/models/graph/multiple_choice_branching.rs +++ b/src/models/graph/multiple_choice_branching.rs @@ -6,7 +6,7 @@ use crate::registry::{FieldInfo, ProblemSchemaEntry, VariantDimension}; use crate::topology::DirectedGraph; -use crate::traits::{Problem, WitnessProblem}; +use crate::traits::Problem; use crate::types::WeightElement; use num_traits::Zero; use serde::de::Error as _; @@ -176,7 +176,7 @@ where W: WeightElement + crate::variant::VariantParam, { const NAME: &'static str = "MultipleChoiceBranching"; - type Value = bool; + type Value = crate::types::Or; fn variant() -> Vec<(&'static str, &'static str)> { crate::variant_params![W] @@ -186,22 +186,19 @@ where vec![2; self.graph.num_arcs()] } - fn evaluate(&self, config: &[usize]) -> bool { - is_valid_multiple_choice_branching( - &self.graph, - &self.weights, - &self.partition, - &self.threshold, - config, - ) + fn evaluate(&self, config: &[usize]) -> crate::types::Or { + crate::types::Or({ + is_valid_multiple_choice_branching( + &self.graph, + &self.weights, + &self.partition, + &self.threshold, + config, + ) + }) } } -impl WitnessProblem for MultipleChoiceBranching where - W: WeightElement + crate::variant::VariantParam -{ -} - fn validate_partition(partition: &[Vec], num_arcs: usize) { if let Some(message) = partition_validation_error(partition, num_arcs) { panic!("{message}"); @@ -297,7 +294,7 @@ fn is_valid_multiple_choice_branching( } crate::declare_variants! { - default sat MultipleChoiceBranching => "2^num_arcs", + default MultipleChoiceBranching => "2^num_arcs", } #[cfg(feature = "example-db")] diff --git a/src/models/graph/multiple_copy_file_allocation.rs b/src/models/graph/multiple_copy_file_allocation.rs index 0f755a245..a2ef8cd79 100644 --- a/src/models/graph/multiple_copy_file_allocation.rs +++ b/src/models/graph/multiple_copy_file_allocation.rs @@ -5,7 +5,7 @@ use crate::registry::{FieldInfo, ProblemSchemaEntry, ProblemSizeFieldEntry}; use crate::topology::{Graph, SimpleGraph}; -use crate::traits::{Problem, WitnessProblem}; +use crate::traits::Problem; use serde::{Deserialize, Serialize}; use std::collections::VecDeque; @@ -179,7 +179,7 @@ impl MultipleCopyFileAllocation { impl Problem for MultipleCopyFileAllocation { const NAME: &'static str = "MultipleCopyFileAllocation"; - type Value = bool; + type Value = crate::types::Or; fn variant() -> Vec<(&'static str, &'static str)> { crate::variant_params![] @@ -189,13 +189,11 @@ impl Problem for MultipleCopyFileAllocation { vec![2; self.graph.num_vertices()] } - fn evaluate(&self, config: &[usize]) -> bool { - self.is_valid_solution(config) + fn evaluate(&self, config: &[usize]) -> crate::types::Or { + crate::types::Or(self.is_valid_solution(config)) } } -impl WitnessProblem for MultipleCopyFileAllocation {} - #[cfg(feature = "example-db")] pub(crate) fn canonical_model_example_specs() -> Vec { vec![crate::example_db::specs::ModelExampleSpec { @@ -212,7 +210,7 @@ pub(crate) fn canonical_model_example_specs() -> Vec "2^num_vertices", + default MultipleCopyFileAllocation => "2^num_vertices", } #[cfg(test)] diff --git a/src/models/graph/optimal_linear_arrangement.rs b/src/models/graph/optimal_linear_arrangement.rs index 451455e87..edf993b59 100644 --- a/src/models/graph/optimal_linear_arrangement.rs +++ b/src/models/graph/optimal_linear_arrangement.rs @@ -6,7 +6,7 @@ use crate::registry::{FieldInfo, ProblemSchemaEntry, VariantDimension}; use crate::topology::{Graph, SimpleGraph}; -use crate::traits::{Problem, WitnessProblem}; +use crate::traits::Problem; use serde::{Deserialize, Serialize}; inventory::submit! { @@ -145,7 +145,7 @@ where G: Graph + crate::variant::VariantParam, { const NAME: &'static str = "OptimalLinearArrangement"; - type Value = bool; + type Value = crate::types::Or; fn variant() -> Vec<(&'static str, &'static str)> { crate::variant_params![G] @@ -156,15 +156,13 @@ where vec![n; n] } - fn evaluate(&self, config: &[usize]) -> bool { - self.is_valid_solution(config) + fn evaluate(&self, config: &[usize]) -> crate::types::Or { + crate::types::Or(self.is_valid_solution(config)) } } -impl WitnessProblem for OptimalLinearArrangement {} - crate::declare_variants! { - default sat OptimalLinearArrangement => "2^num_vertices", + default OptimalLinearArrangement => "2^num_vertices", } #[cfg(feature = "example-db")] diff --git a/src/models/graph/partial_feedback_edge_set.rs b/src/models/graph/partial_feedback_edge_set.rs index 9e61941ef..5806cd534 100644 --- a/src/models/graph/partial_feedback_edge_set.rs +++ b/src/models/graph/partial_feedback_edge_set.rs @@ -5,7 +5,7 @@ use crate::registry::{FieldInfo, ProblemSchemaEntry, VariantDimension}; use crate::topology::{Graph, SimpleGraph}; -use crate::traits::{Problem, WitnessProblem}; +use crate::traits::Problem; use serde::{Deserialize, Serialize}; #[cfg(feature = "example-db")] use std::collections::BTreeSet; @@ -102,7 +102,7 @@ where G: Graph + crate::variant::VariantParam, { const NAME: &'static str = "PartialFeedbackEdgeSet"; - type Value = bool; + type Value = crate::types::Or; fn variant() -> Vec<(&'static str, &'static str)> { crate::variant_params![G] @@ -112,13 +112,11 @@ where vec![2; self.num_edges()] } - fn evaluate(&self, config: &[usize]) -> bool { - self.is_valid_solution(config) + fn evaluate(&self, config: &[usize]) -> crate::types::Or { + crate::types::Or(self.is_valid_solution(config)) } } -impl WitnessProblem for PartialFeedbackEdgeSet where G: Graph + crate::variant::VariantParam {} - fn has_cycle_with_length_at_most( graph: &G, kept_edges: &[bool], @@ -244,7 +242,7 @@ fn normalize_edge(u: usize, v: usize) -> (usize, usize) { } crate::declare_variants! { - default sat PartialFeedbackEdgeSet => "2^num_edges", + default PartialFeedbackEdgeSet => "2^num_edges", } #[cfg(test)] 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 f84e886f8..a825e3d39 100644 --- a/src/models/graph/partition_into_paths_of_length_2.rs +++ b/src/models/graph/partition_into_paths_of_length_2.rs @@ -8,7 +8,7 @@ use crate::registry::{FieldInfo, ProblemSchemaEntry, VariantDimension}; use crate::topology::{Graph, SimpleGraph}; -use crate::traits::{Problem, WitnessProblem}; +use crate::traits::Problem; use crate::variant::VariantParam; use serde::{Deserialize, Serialize}; @@ -148,7 +148,7 @@ where G: Graph + VariantParam, { const NAME: &'static str = "PartitionIntoPathsOfLength2"; - type Value = bool; + type Value = crate::types::Or; fn variant() -> Vec<(&'static str, &'static str)> { crate::variant_params![G] @@ -159,15 +159,13 @@ where vec![q; self.graph.num_vertices()] } - fn evaluate(&self, config: &[usize]) -> bool { - self.is_valid_partition(config) + fn evaluate(&self, config: &[usize]) -> crate::types::Or { + crate::types::Or(self.is_valid_partition(config)) } } -impl WitnessProblem for PartitionIntoPathsOfLength2 {} - crate::declare_variants! { - default sat PartitionIntoPathsOfLength2 => "3^num_vertices", + default PartitionIntoPathsOfLength2 => "3^num_vertices", } #[cfg(feature = "example-db")] diff --git a/src/models/graph/partition_into_triangles.rs b/src/models/graph/partition_into_triangles.rs index 8fc7e0694..b14d5efe5 100644 --- a/src/models/graph/partition_into_triangles.rs +++ b/src/models/graph/partition_into_triangles.rs @@ -5,7 +5,7 @@ use crate::registry::{FieldInfo, ProblemSchemaEntry, VariantDimension}; use crate::topology::{Graph, SimpleGraph}; -use crate::traits::{Problem, WitnessProblem}; +use crate::traits::Problem; use crate::variant::VariantParam; use serde::{Deserialize, Serialize}; @@ -91,7 +91,7 @@ where G: Graph + VariantParam, { const NAME: &'static str = "PartitionIntoTriangles"; - type Value = bool; + type Value = crate::types::Or; fn variant() -> Vec<(&'static str, &'static str)> { crate::variant_params![G] @@ -102,62 +102,62 @@ where vec![q; self.graph.num_vertices()] } - fn evaluate(&self, config: &[usize]) -> bool { - let n = self.graph.num_vertices(); - let q = n / 3; - - // Check config length - if config.len() != n { - return false; - } - - // Check all values are in range [0, q) - if config.iter().any(|&c| c >= q) { - return false; - } - - // Count vertices per group - let mut counts = vec![0usize; q]; - for &c in config { - counts[c] += 1; - } - - // Each group must have exactly 3 vertices - if counts.iter().any(|&c| c != 3) { - return false; - } - - // Build per-group vertex lists in a single pass over config. - let mut group_verts = vec![[0usize; 3]; q]; - let mut group_pos = vec![0usize; q]; - - for (v, &g) in config.iter().enumerate() { - let pos = group_pos[g]; - group_verts[g][pos] = v; - group_pos[g] = pos + 1; - } - - // Check each group forms a triangle - for verts in &group_verts { - if !self.graph.has_edge(verts[0], verts[1]) { - return false; + fn evaluate(&self, config: &[usize]) -> crate::types::Or { + crate::types::Or({ + let n = self.graph.num_vertices(); + let q = n / 3; + + // Check config length + if config.len() != n { + return crate::types::Or(false); + } + + // Check all values are in range [0, q) + if config.iter().any(|&c| c >= q) { + return crate::types::Or(false); } - if !self.graph.has_edge(verts[0], verts[2]) { - return false; + + // Count vertices per group + let mut counts = vec![0usize; q]; + for &c in config { + counts[c] += 1; } - if !self.graph.has_edge(verts[1], verts[2]) { - return false; + + // Each group must have exactly 3 vertices + if counts.iter().any(|&c| c != 3) { + return crate::types::Or(false); } - } - true + // Build per-group vertex lists in a single pass over config. + let mut group_verts = vec![[0usize; 3]; q]; + let mut group_pos = vec![0usize; q]; + + for (v, &g) in config.iter().enumerate() { + let pos = group_pos[g]; + group_verts[g][pos] = v; + group_pos[g] = pos + 1; + } + + // Check each group forms a triangle + for verts in &group_verts { + if !self.graph.has_edge(verts[0], verts[1]) { + return crate::types::Or(false); + } + if !self.graph.has_edge(verts[0], verts[2]) { + return crate::types::Or(false); + } + if !self.graph.has_edge(verts[1], verts[2]) { + return crate::types::Or(false); + } + } + + true + }) } } -impl WitnessProblem for PartitionIntoTriangles {} - crate::declare_variants! { - default sat PartitionIntoTriangles => "2^num_vertices", + default PartitionIntoTriangles => "2^num_vertices", } #[cfg(feature = "example-db")] diff --git a/src/models/graph/path_constrained_network_flow.rs b/src/models/graph/path_constrained_network_flow.rs index 087490ce8..373598e22 100644 --- a/src/models/graph/path_constrained_network_flow.rs +++ b/src/models/graph/path_constrained_network_flow.rs @@ -8,7 +8,7 @@ use crate::registry::{FieldInfo, ProblemSchemaEntry}; use crate::topology::DirectedGraph; -use crate::traits::{Problem, WitnessProblem}; +use crate::traits::Problem; use serde::{Deserialize, Serialize}; use std::collections::HashSet; @@ -216,7 +216,7 @@ impl PathConstrainedNetworkFlow { impl Problem for PathConstrainedNetworkFlow { const NAME: &'static str = "PathConstrainedNetworkFlow"; - type Value = bool; + type Value = crate::types::Or; fn dims(&self) -> Vec { self.paths @@ -225,8 +225,8 @@ impl Problem for PathConstrainedNetworkFlow { .collect() } - fn evaluate(&self, config: &[usize]) -> bool { - self.is_feasible(config) + fn evaluate(&self, config: &[usize]) -> crate::types::Or { + crate::types::Or(self.is_feasible(config)) } fn variant() -> Vec<(&'static str, &'static str)> { @@ -234,10 +234,8 @@ impl Problem for PathConstrainedNetworkFlow { } } -impl WitnessProblem for PathConstrainedNetworkFlow {} - crate::declare_variants! { - default sat PathConstrainedNetworkFlow => "(max_capacity + 1)^num_paths", + default PathConstrainedNetworkFlow => "(max_capacity + 1)^num_paths", } #[cfg(feature = "example-db")] diff --git a/src/models/graph/rooted_tree_arrangement.rs b/src/models/graph/rooted_tree_arrangement.rs index dbf86c687..6fc20b366 100644 --- a/src/models/graph/rooted_tree_arrangement.rs +++ b/src/models/graph/rooted_tree_arrangement.rs @@ -6,7 +6,7 @@ use crate::registry::{FieldInfo, ProblemSchemaEntry, VariantDimension}; use crate::topology::{Graph, SimpleGraph}; -use crate::traits::{Problem, WitnessProblem}; +use crate::traits::Problem; use crate::variant::VariantParam; use serde::{Deserialize, Serialize}; @@ -100,7 +100,7 @@ where G: Graph + VariantParam, { const NAME: &'static str = "RootedTreeArrangement"; - type Value = bool; + type Value = crate::types::Or; fn variant() -> Vec<(&'static str, &'static str)> { crate::variant_params![G] @@ -111,13 +111,11 @@ where vec![n; 2 * n] } - fn evaluate(&self, config: &[usize]) -> bool { - self.is_valid_solution(config) + fn evaluate(&self, config: &[usize]) -> crate::types::Or { + crate::types::Or(self.is_valid_solution(config)) } } -impl WitnessProblem for RootedTreeArrangement {} - fn analyze_parent_array(parent: &[usize]) -> Option { let n = parent.len(); if n == 0 { @@ -207,7 +205,7 @@ fn are_ancestor_comparable(parent: &[usize], u: usize, v: usize) -> bool { } crate::declare_variants! { - default sat RootedTreeArrangement => "2^num_vertices", + default RootedTreeArrangement => "2^num_vertices", } #[cfg(feature = "example-db")] diff --git a/src/models/graph/rural_postman.rs b/src/models/graph/rural_postman.rs index c3d256168..191c1a4d2 100644 --- a/src/models/graph/rural_postman.rs +++ b/src/models/graph/rural_postman.rs @@ -6,7 +6,7 @@ use crate::registry::{FieldInfo, ProblemSchemaEntry, VariantDimension}; use crate::topology::{Graph, SimpleGraph}; -use crate::traits::{Problem, WitnessProblem}; +use crate::traits::Problem; use crate::types::WeightElement; use num_traits::Zero; use serde::{Deserialize, Serialize}; @@ -252,7 +252,7 @@ where W: WeightElement + crate::variant::VariantParam, { const NAME: &'static str = "RuralPostman"; - type Value = bool; + type Value = crate::types::Or; fn variant() -> Vec<(&'static str, &'static str)> { crate::variant_params![G, W] @@ -262,20 +262,13 @@ where vec![3; self.graph.num_edges()] } - fn evaluate(&self, config: &[usize]) -> bool { - self.is_valid_solution(config) + fn evaluate(&self, config: &[usize]) -> crate::types::Or { + crate::types::Or(self.is_valid_solution(config)) } } -impl WitnessProblem for RuralPostman -where - G: Graph + crate::variant::VariantParam, - W: WeightElement + crate::variant::VariantParam, -{ -} - crate::declare_variants! { - default sat RuralPostman => "2^num_vertices * num_vertices^2", + default RuralPostman => "2^num_vertices * num_vertices^2", } #[cfg(feature = "example-db")] diff --git a/src/models/graph/shortest_weight_constrained_path.rs b/src/models/graph/shortest_weight_constrained_path.rs index 36d373bda..faba36a79 100644 --- a/src/models/graph/shortest_weight_constrained_path.rs +++ b/src/models/graph/shortest_weight_constrained_path.rs @@ -6,7 +6,7 @@ use crate::registry::{FieldInfo, ProblemSchemaEntry, VariantDimension}; use crate::topology::{Graph, SimpleGraph}; -use crate::traits::{Problem, WitnessProblem}; +use crate::traits::Problem; use crate::types::WeightElement; use num_traits::Zero; use serde::{Deserialize, Serialize}; @@ -308,7 +308,7 @@ where N: WeightElement + crate::variant::VariantParam, { const NAME: &'static str = "ShortestWeightConstrainedPath"; - type Value = bool; + type Value = crate::types::Or; fn variant() -> Vec<(&'static str, &'static str)> { crate::variant_params![G, N] @@ -318,18 +318,11 @@ where vec![2; self.graph.num_edges()] } - fn evaluate(&self, config: &[usize]) -> bool { - self.is_valid_solution(config) + fn evaluate(&self, config: &[usize]) -> crate::types::Or { + crate::types::Or(self.is_valid_solution(config)) } } -impl WitnessProblem for ShortestWeightConstrainedPath -where - G: Graph + crate::variant::VariantParam, - N: WeightElement + crate::variant::VariantParam, -{ -} - #[cfg(feature = "example-db")] pub(crate) fn canonical_model_example_specs() -> Vec { vec![crate::example_db::specs::ModelExampleSpec { @@ -361,7 +354,7 @@ pub(crate) fn canonical_model_example_specs() -> Vec => "2^num_edges", + default ShortestWeightConstrainedPath => "2^num_edges", } #[cfg(test)] diff --git a/src/models/graph/spin_glass.rs b/src/models/graph/spin_glass.rs index f16d4f158..0e86144a5 100644 --- a/src/models/graph/spin_glass.rs +++ b/src/models/graph/spin_glass.rs @@ -4,8 +4,8 @@ use crate::registry::{FieldInfo, ProblemSchemaEntry, VariantDimension}; use crate::topology::{Graph, SimpleGraph}; -use crate::traits::{ObjectiveProblem, Problem}; -use crate::types::{ExtremumSense, Min, WeightElement}; +use crate::traits::Problem; +use crate::types::{Min, WeightElement}; use serde::{Deserialize, Serialize}; inventory::submit! { @@ -236,29 +236,9 @@ where } } -impl ObjectiveProblem for SpinGlass -where - G: Graph + crate::variant::VariantParam, - W: WeightElement - + crate::variant::VariantParam - + PartialOrd - + num_traits::Num - + num_traits::Zero - + num_traits::Bounded - + std::ops::AddAssign - + std::ops::Mul - + From, -{ - type Objective = W::Sum; - - fn direction(&self) -> ExtremumSense { - ExtremumSense::Minimize - } -} - crate::declare_variants! { - default opt SpinGlass => "2^num_spins", - opt SpinGlass => "2^num_spins", + default SpinGlass => "2^num_spins", + SpinGlass => "2^num_spins", } #[cfg(feature = "example-db")] diff --git a/src/models/graph/steiner_tree.rs b/src/models/graph/steiner_tree.rs index 9b2ab90ef..b58f8c331 100644 --- a/src/models/graph/steiner_tree.rs +++ b/src/models/graph/steiner_tree.rs @@ -11,8 +11,8 @@ use serde::{Deserialize, Serialize}; use crate::{ registry::{FieldInfo, ProblemSchemaEntry, VariantDimension}, topology::{Graph, SimpleGraph}, - traits::{ObjectiveProblem, Problem}, - types::{ExtremumSense, Min, One, WeightElement}, + traits::Problem, + types::{Min, One, WeightElement}, }; inventory::submit! { @@ -247,21 +247,9 @@ where } } -impl ObjectiveProblem for SteinerTree -where - G: Graph + crate::variant::VariantParam, - W: WeightElement + crate::variant::VariantParam, -{ - type Objective = W::Sum; - - fn direction(&self) -> ExtremumSense { - ExtremumSense::Minimize - } -} - crate::declare_variants! { - default opt SteinerTree => "3^num_terminals * num_vertices + 2^num_terminals * num_vertices^2", - opt SteinerTree => "3^num_terminals * num_vertices + 2^num_terminals * num_vertices^2", + default SteinerTree => "3^num_terminals * num_vertices + 2^num_terminals * num_vertices^2", + SteinerTree => "3^num_terminals * num_vertices + 2^num_terminals * num_vertices^2", } #[cfg(feature = "example-db")] diff --git a/src/models/graph/steiner_tree_in_graphs.rs b/src/models/graph/steiner_tree_in_graphs.rs index 9807b9aed..9a191078c 100644 --- a/src/models/graph/steiner_tree_in_graphs.rs +++ b/src/models/graph/steiner_tree_in_graphs.rs @@ -5,8 +5,8 @@ use crate::registry::{FieldInfo, ProblemSchemaEntry, VariantDimension}; use crate::topology::{Graph, SimpleGraph}; -use crate::traits::{ObjectiveProblem, Problem}; -use crate::types::{ExtremumSense, Min, One, WeightElement}; +use crate::traits::Problem; +use crate::types::{Min, One, WeightElement}; use num_traits::Zero; use serde::{Deserialize, Serialize}; @@ -205,18 +205,6 @@ where } } -impl ObjectiveProblem for SteinerTreeInGraphs -where - G: Graph + crate::variant::VariantParam, - W: WeightElement + crate::variant::VariantParam, -{ - type Objective = W::Sum; - - fn direction(&self) -> ExtremumSense { - ExtremumSense::Minimize - } -} - /// Check if a selection of edges forms a valid Steiner tree (connected subgraph spanning all terminals). /// /// A valid Steiner tree requires: @@ -286,8 +274,8 @@ pub(crate) fn is_steiner_tree(graph: &G, terminals: &[usize], selected } crate::declare_variants! { - default opt SteinerTreeInGraphs => "2^num_terminals * num_vertices^3", - opt SteinerTreeInGraphs => "2^num_terminals * num_vertices^3", + default SteinerTreeInGraphs => "2^num_terminals * num_vertices^3", + SteinerTreeInGraphs => "2^num_terminals * num_vertices^3", } #[cfg(feature = "example-db")] diff --git a/src/models/graph/strong_connectivity_augmentation.rs b/src/models/graph/strong_connectivity_augmentation.rs index ee6048973..d22906400 100644 --- a/src/models/graph/strong_connectivity_augmentation.rs +++ b/src/models/graph/strong_connectivity_augmentation.rs @@ -5,7 +5,7 @@ use crate::registry::{FieldInfo, ProblemSchemaEntry, VariantDimension}; use crate::topology::DirectedGraph; -use crate::traits::{Problem, WitnessProblem}; +use crate::traits::Problem; use crate::types::WeightElement; use num_traits::Zero; use serde::{Deserialize, Deserializer, Serialize}; @@ -179,7 +179,7 @@ where W: WeightElement + crate::variant::VariantParam, { const NAME: &'static str = "StrongConnectivityAugmentation"; - type Value = bool; + type Value = crate::types::Or; fn variant() -> Vec<(&'static str, &'static str)> { crate::variant_params![W] @@ -189,18 +189,13 @@ where vec![2; self.candidate_arcs.len()] } - fn evaluate(&self, config: &[usize]) -> bool { - self.evaluate_config(config) + fn evaluate(&self, config: &[usize]) -> crate::types::Or { + crate::types::Or(self.evaluate_config(config)) } } -impl WitnessProblem for StrongConnectivityAugmentation where - W: WeightElement + crate::variant::VariantParam -{ -} - crate::declare_variants! { - default sat StrongConnectivityAugmentation => "2^num_potential_arcs", + default StrongConnectivityAugmentation => "2^num_potential_arcs", } #[derive(Deserialize)] diff --git a/src/models/graph/subgraph_isomorphism.rs b/src/models/graph/subgraph_isomorphism.rs index aeee3449d..ca7f7506b 100644 --- a/src/models/graph/subgraph_isomorphism.rs +++ b/src/models/graph/subgraph_isomorphism.rs @@ -7,7 +7,7 @@ use crate::registry::{FieldInfo, ProblemSchemaEntry}; use crate::topology::{Graph, SimpleGraph}; -use crate::traits::{Problem, WitnessProblem}; +use crate::traits::Problem; use serde::{Deserialize, Serialize}; inventory::submit! { @@ -114,13 +114,13 @@ impl SubgraphIsomorphism { /// Check if a configuration represents a valid subgraph isomorphism. pub fn is_valid_solution(&self, config: &[usize]) -> bool { - self.evaluate(config) + self.evaluate(config).0 } } impl Problem for SubgraphIsomorphism { const NAME: &'static str = "SubgraphIsomorphism"; - type Value = bool; + type Value = crate::types::Or; fn dims(&self) -> Vec { let n_host = self.host_graph.num_vertices(); @@ -134,42 +134,44 @@ impl Problem for SubgraphIsomorphism { } } - fn evaluate(&self, config: &[usize]) -> bool { - let n_pattern = self.pattern_graph.num_vertices(); - let n_host = self.host_graph.num_vertices(); + fn evaluate(&self, config: &[usize]) -> crate::types::Or { + crate::types::Or({ + let n_pattern = self.pattern_graph.num_vertices(); + let n_host = self.host_graph.num_vertices(); - // If the pattern has more vertices than the host, no injective mapping exists. - if n_pattern > n_host { - return false; - } + // If the pattern has more vertices than the host, no injective mapping exists. + if n_pattern > n_host { + return crate::types::Or(false); + } - // Config must have one entry per pattern vertex - if config.len() != n_pattern { - return false; - } + // Config must have one entry per pattern vertex + if config.len() != n_pattern { + return crate::types::Or(false); + } - // All values must be valid host vertex indices - if config.iter().any(|&v| v >= n_host) { - return false; - } + // All values must be valid host vertex indices + if config.iter().any(|&v| v >= n_host) { + return crate::types::Or(false); + } - // Check injectivity: all mapped host vertices must be distinct - for i in 0..n_pattern { - for j in (i + 1)..n_pattern { - if config[i] == config[j] { - return false; + // Check injectivity: all mapped host vertices must be distinct + for i in 0..n_pattern { + for j in (i + 1)..n_pattern { + if config[i] == config[j] { + return crate::types::Or(false); + } } } - } - // Check edge preservation: every pattern edge must map to a host edge - for (u, v) in self.pattern_graph.edges() { - if !self.host_graph.has_edge(config[u], config[v]) { - return false; + // Check edge preservation: every pattern edge must map to a host edge + for (u, v) in self.pattern_graph.edges() { + if !self.host_graph.has_edge(config[u], config[v]) { + return crate::types::Or(false); + } } - } - true + true + }) } fn variant() -> Vec<(&'static str, &'static str)> { @@ -177,10 +179,8 @@ impl Problem for SubgraphIsomorphism { } } -impl WitnessProblem for SubgraphIsomorphism {} - crate::declare_variants! { - default sat SubgraphIsomorphism => "num_host_vertices ^ num_pattern_vertices", + default SubgraphIsomorphism => "num_host_vertices ^ num_pattern_vertices", } #[cfg(feature = "example-db")] diff --git a/src/models/graph/traveling_salesman.rs b/src/models/graph/traveling_salesman.rs index 5d7daf019..017fabf7c 100644 --- a/src/models/graph/traveling_salesman.rs +++ b/src/models/graph/traveling_salesman.rs @@ -5,8 +5,8 @@ use crate::registry::{FieldInfo, ProblemSchemaEntry, VariantDimension}; use crate::topology::{Graph, SimpleGraph}; -use crate::traits::{ObjectiveProblem, Problem}; -use crate::types::{ExtremumSense, Min, WeightElement}; +use crate::traits::Problem; +use crate::types::{Min, WeightElement}; use num_traits::Zero; use serde::{Deserialize, Serialize}; @@ -176,18 +176,6 @@ where } } -impl ObjectiveProblem for TravelingSalesman -where - G: Graph + crate::variant::VariantParam, - W: WeightElement + crate::variant::VariantParam, -{ - type Objective = W::Sum; - - fn direction(&self) -> ExtremumSense { - ExtremumSense::Minimize - } -} - /// Check if a selection of edges forms a valid Hamiltonian cycle. /// /// # Panics @@ -272,7 +260,7 @@ pub(crate) fn canonical_model_example_specs() -> Vec => "2^num_vertices", + default TravelingSalesman => "2^num_vertices", } #[cfg(test)] diff --git a/src/models/graph/undirected_flow_lower_bounds.rs b/src/models/graph/undirected_flow_lower_bounds.rs index a20c11cdb..be86186dd 100644 --- a/src/models/graph/undirected_flow_lower_bounds.rs +++ b/src/models/graph/undirected_flow_lower_bounds.rs @@ -15,7 +15,7 @@ use crate::registry::{FieldInfo, ProblemSchemaEntry, ProblemSizeFieldEntry}; use crate::topology::{Graph, SimpleGraph}; -use crate::traits::{Problem, WitnessProblem}; +use crate::traits::Problem; use serde::{Deserialize, Serialize}; use std::collections::VecDeque; @@ -137,7 +137,7 @@ impl UndirectedFlowLowerBounds { } pub fn is_valid_solution(&self, config: &[usize]) -> bool { - self.evaluate(config) + self.evaluate(config).0 } fn total_capacity(&self) -> Option { @@ -216,7 +216,7 @@ impl UndirectedFlowLowerBounds { impl Problem for UndirectedFlowLowerBounds { const NAME: &'static str = "UndirectedFlowLowerBounds"; - type Value = bool; + type Value = crate::types::Or; fn variant() -> Vec<(&'static str, &'static str)> { crate::variant_params![] @@ -226,15 +226,13 @@ impl Problem for UndirectedFlowLowerBounds { vec![2; self.num_edges()] } - fn evaluate(&self, config: &[usize]) -> bool { - self.has_feasible_orientation(config) + fn evaluate(&self, config: &[usize]) -> crate::types::Or { + crate::types::Or(self.has_feasible_orientation(config)) } } -impl WitnessProblem for UndirectedFlowLowerBounds {} - crate::declare_variants! { - default sat UndirectedFlowLowerBounds => "2^num_edges", + default UndirectedFlowLowerBounds => "2^num_edges", } #[cfg(feature = "example-db")] diff --git a/src/models/graph/undirected_two_commodity_integral_flow.rs b/src/models/graph/undirected_two_commodity_integral_flow.rs index 004b239ed..15319893b 100644 --- a/src/models/graph/undirected_two_commodity_integral_flow.rs +++ b/src/models/graph/undirected_two_commodity_integral_flow.rs @@ -5,7 +5,7 @@ use crate::registry::{FieldInfo, ProblemSchemaEntry, ProblemSizeFieldEntry}; use crate::topology::{Graph, SimpleGraph}; -use crate::traits::{Problem, WitnessProblem}; +use crate::traits::Problem; use serde::{Deserialize, Serialize}; inventory::submit! { @@ -150,7 +150,7 @@ impl UndirectedTwoCommodityIntegralFlow { } pub fn is_valid_solution(&self, config: &[usize]) -> bool { - self.evaluate(config) + self.evaluate(config).0 } fn config_len(&self) -> usize { @@ -218,7 +218,7 @@ impl UndirectedTwoCommodityIntegralFlow { impl Problem for UndirectedTwoCommodityIntegralFlow { const NAME: &'static str = "UndirectedTwoCommodityIntegralFlow"; - type Value = bool; + type Value = crate::types::Or; fn variant() -> Vec<(&'static str, &'static str)> { crate::variant_params![] @@ -234,66 +234,66 @@ impl Problem for UndirectedTwoCommodityIntegralFlow { .collect() } - fn evaluate(&self, config: &[usize]) -> bool { - if config.len() != self.config_len() { - return false; - } - - for (edge_index, &capacity) in self.capacities.iter().enumerate() { - let Some(flows) = self.edge_flows(config, edge_index) else { - return false; - }; - - if flows - .iter() - .any(|&value| u64::try_from(value).map_or(true, |value| value > capacity)) - { - return false; - } - - if flows[0] > 0 && flows[1] > 0 { - return false; - } - if flows[2] > 0 && flows[3] > 0 { - return false; + fn evaluate(&self, config: &[usize]) -> crate::types::Or { + crate::types::Or({ + if config.len() != self.config_len() { + return crate::types::Or(false); } - let commodity_1 = u64::try_from(std::cmp::max(flows[0], flows[1])) - .expect("flow values already validated against u64 capacities"); - let commodity_2 = u64::try_from(std::cmp::max(flows[2], flows[3])) - .expect("flow values already validated against u64 capacities"); - let Some(shared) = commodity_1.checked_add(commodity_2) else { - return false; - }; - if shared > capacity { - return false; + for (edge_index, &capacity) in self.capacities.iter().enumerate() { + let Some(flows) = self.edge_flows(config, edge_index) else { + return crate::types::Or(false); + }; + + if flows + .iter() + .any(|&value| u64::try_from(value).map_or(true, |value| value > capacity)) + { + return crate::types::Or(false); + } + + if flows[0] > 0 && flows[1] > 0 { + return crate::types::Or(false); + } + if flows[2] > 0 && flows[3] > 0 { + return crate::types::Or(false); + } + + let commodity_1 = u64::try_from(std::cmp::max(flows[0], flows[1])) + .expect("flow values already validated against u64 capacities"); + let commodity_2 = u64::try_from(std::cmp::max(flows[2], flows[3])) + .expect("flow values already validated against u64 capacities"); + let Some(shared) = commodity_1.checked_add(commodity_2) else { + return crate::types::Or(false); + }; + if shared > capacity { + return crate::types::Or(false); + } } - } - for vertex in 0..self.num_vertices() { - if self.is_terminal(vertex) { - continue; - } + for vertex in 0..self.num_vertices() { + if self.is_terminal(vertex) { + continue; + } - if self.commodity_balance(config, 1, vertex) != Some(0) - || self.commodity_balance(config, 2, vertex) != Some(0) - { - return false; + if self.commodity_balance(config, 1, vertex) != Some(0) + || self.commodity_balance(config, 2, vertex) != Some(0) + { + return crate::types::Or(false); + } } - } - self.net_flow_into_sink(config, 1) - .is_some_and(|flow| flow >= self.requirement_1) - && self - .net_flow_into_sink(config, 2) - .is_some_and(|flow| flow >= self.requirement_2) + self.net_flow_into_sink(config, 1) + .is_some_and(|flow| flow >= self.requirement_1) + && self + .net_flow_into_sink(config, 2) + .is_some_and(|flow| flow >= self.requirement_2) + }) } } -impl WitnessProblem for UndirectedTwoCommodityIntegralFlow {} - crate::declare_variants! { - default sat UndirectedTwoCommodityIntegralFlow => "5^num_edges", + default UndirectedTwoCommodityIntegralFlow => "5^num_edges", } #[cfg(feature = "example-db")] diff --git a/src/models/misc/additional_key.rs b/src/models/misc/additional_key.rs index 8f79257f9..6073fc46c 100644 --- a/src/models/misc/additional_key.rs +++ b/src/models/misc/additional_key.rs @@ -8,7 +8,7 @@ //! The problem is NP-complete (Garey & Johnson, SR7). use crate::registry::{FieldInfo, ProblemSchemaEntry}; -use crate::traits::{Problem, WitnessProblem}; +use crate::traits::Problem; use serde::{Deserialize, Serialize}; inventory::submit! { @@ -192,7 +192,7 @@ impl AdditionalKey { impl Problem for AdditionalKey { const NAME: &'static str = "AdditionalKey"; - type Value = bool; + type Value = crate::types::Or; fn variant() -> Vec<(&'static str, &'static str)> { crate::variant_params![] @@ -202,62 +202,62 @@ impl Problem for AdditionalKey { vec![2; self.relation_attrs.len()] } - fn evaluate(&self, config: &[usize]) -> bool { - // Check config length - if config.len() != self.relation_attrs.len() { - return false; - } - // Check all values are 0 or 1 - if config.iter().any(|&v| v >= 2) { - return false; - } + fn evaluate(&self, config: &[usize]) -> crate::types::Or { + crate::types::Or({ + // Check config length + if config.len() != self.relation_attrs.len() { + return crate::types::Or(false); + } + // Check all values are 0 or 1 + if config.iter().any(|&v| v >= 2) { + return crate::types::Or(false); + } - // Build selected attribute set - let selected: Vec = config - .iter() - .enumerate() - .filter(|(_, &v)| v == 1) - .map(|(i, _)| self.relation_attrs[i]) - .collect(); + // Build selected attribute set + let selected: Vec = config + .iter() + .enumerate() + .filter(|(_, &v)| v == 1) + .map(|(i, _)| self.relation_attrs[i]) + .collect(); - // Empty selection is not a key - if selected.is_empty() { - return false; - } + // Empty selection is not a key + if selected.is_empty() { + return crate::types::Or(false); + } - // Compute closure of selected attributes - let mut attr_set = vec![false; self.num_attributes]; - for &a in &selected { - attr_set[a] = true; - } - let closure = self.compute_closure(&attr_set); + // Compute closure of selected attributes + let mut attr_set = vec![false; self.num_attributes]; + for &a in &selected { + attr_set[a] = true; + } + let closure = self.compute_closure(&attr_set); - // Check closure covers all relation_attrs - if !self.relation_attrs.iter().all(|&a| closure[a]) { - return false; - } + // Check closure covers all relation_attrs + if !self.relation_attrs.iter().all(|&a| closure[a]) { + return crate::types::Or(false); + } - // Check minimality: removing any single selected attribute should break coverage - for &a in &selected { - let mut reduced = attr_set.clone(); - reduced[a] = false; - let reduced_closure = self.compute_closure(&reduced); - if self.relation_attrs.iter().all(|&ra| reduced_closure[ra]) { - return false; // Not minimal + // Check minimality: removing any single selected attribute should break coverage + for &a in &selected { + let mut reduced = attr_set.clone(); + reduced[a] = false; + let reduced_closure = self.compute_closure(&reduced); + if self.relation_attrs.iter().all(|&ra| reduced_closure[ra]) { + return crate::types::Or(false); // Not minimal + } } - } - // Build sorted selected vec and check it's not in known_keys - let mut sorted_selected = selected; - sorted_selected.sort_unstable(); - !self.known_keys.contains(&sorted_selected) + // Build sorted selected vec and check it's not in known_keys + let mut sorted_selected = selected; + sorted_selected.sort_unstable(); + !self.known_keys.contains(&sorted_selected) + }) } } -impl WitnessProblem for AdditionalKey {} - crate::declare_variants! { - default sat AdditionalKey => "2^num_relation_attrs * num_dependencies * num_attributes", + default AdditionalKey => "2^num_relation_attrs * num_dependencies * num_attributes", } #[cfg(feature = "example-db")] diff --git a/src/models/misc/bin_packing.rs b/src/models/misc/bin_packing.rs index cc5f93b4e..a778c2395 100644 --- a/src/models/misc/bin_packing.rs +++ b/src/models/misc/bin_packing.rs @@ -4,8 +4,8 @@ //! that minimizes the number of bins used while respecting capacity constraints. use crate::registry::{FieldInfo, ProblemSchemaEntry, VariantDimension}; -use crate::traits::{ObjectiveProblem, Problem}; -use crate::types::{ExtremumSense, Min, WeightElement}; +use crate::traits::Problem; +use crate::types::{Min, WeightElement}; use serde::{Deserialize, Serialize}; inventory::submit! { @@ -107,18 +107,6 @@ where } } -impl ObjectiveProblem for BinPacking -where - W: WeightElement + crate::variant::VariantParam, - W::Sum: PartialOrd, -{ - type Objective = i32; - - fn direction(&self) -> ExtremumSense { - ExtremumSense::Minimize - } -} - /// Check if a configuration is a valid bin packing (all bins within capacity). fn is_valid_packing(sizes: &[W], capacity: &W, config: &[usize]) -> bool where @@ -154,8 +142,8 @@ fn count_bins(config: &[usize]) -> usize { } crate::declare_variants! { - default opt BinPacking => "2^num_items", - opt BinPacking => "2^num_items", + default BinPacking => "2^num_items", + BinPacking => "2^num_items", } #[cfg(feature = "example-db")] diff --git a/src/models/misc/boyce_codd_normal_form_violation.rs b/src/models/misc/boyce_codd_normal_form_violation.rs index 1e6594abd..3e465c5bf 100644 --- a/src/models/misc/boyce_codd_normal_form_violation.rs +++ b/src/models/misc/boyce_codd_normal_form_violation.rs @@ -6,7 +6,7 @@ //! some but not all attributes of `A' \ X` — i.e., a witness to a BCNF violation. use crate::registry::{FieldInfo, ProblemSchemaEntry}; -use crate::traits::{Problem, WitnessProblem}; +use crate::traits::Problem; use serde::{Deserialize, Serialize}; use std::collections::HashSet; @@ -176,36 +176,38 @@ impl BoyceCoddNormalFormViolation { impl Problem for BoyceCoddNormalFormViolation { const NAME: &'static str = "BoyceCoddNormalFormViolation"; - type Value = bool; + type Value = crate::types::Or; fn dims(&self) -> Vec { vec![2; self.target_subset.len()] } - fn evaluate(&self, config: &[usize]) -> bool { - if config.len() != self.target_subset.len() || config.iter().any(|&v| v > 1) { - return false; - } - let x: HashSet = config - .iter() - .enumerate() - .filter(|(_, &v)| v == 1) - .map(|(i, _)| self.target_subset[i]) - .collect(); - let closure = Self::compute_closure(&x, &self.functional_deps); - // Check: ∃ y, z ∈ A' \ X s.t. y ∈ closure ∧ z ∉ closure - let mut has_in_closure = false; - let mut has_not_in_closure = false; - for &a in &self.target_subset { - if !x.contains(&a) { - if closure.contains(&a) { - has_in_closure = true; - } else { - has_not_in_closure = true; + fn evaluate(&self, config: &[usize]) -> crate::types::Or { + crate::types::Or({ + if config.len() != self.target_subset.len() || config.iter().any(|&v| v > 1) { + return crate::types::Or(false); + } + let x: HashSet = config + .iter() + .enumerate() + .filter(|(_, &v)| v == 1) + .map(|(i, _)| self.target_subset[i]) + .collect(); + let closure = Self::compute_closure(&x, &self.functional_deps); + // Check: ∃ y, z ∈ A' \ X s.t. y ∈ closure ∧ z ∉ closure + let mut has_in_closure = false; + let mut has_not_in_closure = false; + for &a in &self.target_subset { + if !x.contains(&a) { + if closure.contains(&a) { + has_in_closure = true; + } else { + has_not_in_closure = true; + } } } - } - has_in_closure && has_not_in_closure + has_in_closure && has_not_in_closure + }) } fn variant() -> Vec<(&'static str, &'static str)> { @@ -213,10 +215,8 @@ impl Problem for BoyceCoddNormalFormViolation { } } -impl WitnessProblem for BoyceCoddNormalFormViolation {} - crate::declare_variants! { - default sat BoyceCoddNormalFormViolation => "2^num_target_attributes * num_target_attributes^2 * num_functional_deps", + default BoyceCoddNormalFormViolation => "2^num_target_attributes * num_target_attributes^2 * num_functional_deps", } #[cfg(feature = "example-db")] diff --git a/src/models/misc/capacity_assignment.rs b/src/models/misc/capacity_assignment.rs index bbeb2ee5d..a648f4b92 100644 --- a/src/models/misc/capacity_assignment.rs +++ b/src/models/misc/capacity_assignment.rs @@ -5,7 +5,7 @@ //! their respective budgets. use crate::registry::{FieldInfo, ProblemSchemaEntry}; -use crate::traits::{Problem, WitnessProblem}; +use crate::traits::Problem; use serde::{Deserialize, Serialize}; inventory::submit! { @@ -155,17 +155,19 @@ impl CapacityAssignment { impl Problem for CapacityAssignment { const NAME: &'static str = "CapacityAssignment"; - type Value = bool; + type Value = crate::types::Or; fn dims(&self) -> Vec { vec![self.num_capacities(); self.num_links()] } - fn evaluate(&self, config: &[usize]) -> bool { - let Some((total_cost, total_delay)) = self.total_cost_and_delay(config) else { - return false; - }; - total_cost <= self.cost_budget as u128 && total_delay <= self.delay_budget as u128 + fn evaluate(&self, config: &[usize]) -> crate::types::Or { + crate::types::Or({ + let Some((total_cost, total_delay)) = self.total_cost_and_delay(config) else { + return crate::types::Or(false); + }; + total_cost <= self.cost_budget as u128 && total_delay <= self.delay_budget as u128 + }) } fn variant() -> Vec<(&'static str, &'static str)> { @@ -173,10 +175,8 @@ impl Problem for CapacityAssignment { } } -impl WitnessProblem for CapacityAssignment {} - crate::declare_variants! { - default sat CapacityAssignment => "num_capacities ^ num_links", + default CapacityAssignment => "num_capacities ^ num_links", } #[cfg(feature = "example-db")] diff --git a/src/models/misc/conjunctive_boolean_query.rs b/src/models/misc/conjunctive_boolean_query.rs index 059f89c5a..e9e4b8737 100644 --- a/src/models/misc/conjunctive_boolean_query.rs +++ b/src/models/misc/conjunctive_boolean_query.rs @@ -11,7 +11,7 @@ //! variables such that every conjunct's resolved tuple belongs to its relation. use crate::registry::{FieldInfo, ProblemSchemaEntry}; -use crate::traits::{Problem, WitnessProblem}; +use crate::traits::Problem; use serde::{Deserialize, Serialize}; inventory::submit! { @@ -191,7 +191,7 @@ impl ConjunctiveBooleanQuery { impl Problem for ConjunctiveBooleanQuery { const NAME: &'static str = "ConjunctiveBooleanQuery"; - type Value = bool; + type Value = crate::types::Or; fn variant() -> Vec<(&'static str, &'static str)> { crate::variant_params![] @@ -201,30 +201,30 @@ impl Problem for ConjunctiveBooleanQuery { vec![self.domain_size; self.num_variables] } - fn evaluate(&self, config: &[usize]) -> bool { - if config.len() != self.num_variables { - return false; - } - if config.iter().any(|&v| v >= self.domain_size) { - return false; - } - self.conjuncts.iter().all(|(rel_idx, args)| { - let tuple: Vec = args - .iter() - .map(|arg| match arg { - QueryArg::Variable(i) => config[*i], - QueryArg::Constant(c) => *c, - }) - .collect(); - self.relations[*rel_idx].tuples.contains(&tuple) + fn evaluate(&self, config: &[usize]) -> crate::types::Or { + crate::types::Or({ + if config.len() != self.num_variables { + return crate::types::Or(false); + } + if config.iter().any(|&v| v >= self.domain_size) { + return crate::types::Or(false); + } + self.conjuncts.iter().all(|(rel_idx, args)| { + let tuple: Vec = args + .iter() + .map(|arg| match arg { + QueryArg::Variable(i) => config[*i], + QueryArg::Constant(c) => *c, + }) + .collect(); + self.relations[*rel_idx].tuples.contains(&tuple) + }) }) } } -impl WitnessProblem for ConjunctiveBooleanQuery {} - crate::declare_variants! { - default sat ConjunctiveBooleanQuery => "domain_size ^ num_variables", + default ConjunctiveBooleanQuery => "domain_size ^ num_variables", } #[cfg(feature = "example-db")] diff --git a/src/models/misc/conjunctive_query_foldability.rs b/src/models/misc/conjunctive_query_foldability.rs index 604312da5..cd3963c50 100644 --- a/src/models/misc/conjunctive_query_foldability.rs +++ b/src/models/misc/conjunctive_query_foldability.rs @@ -5,7 +5,7 @@ //! that transforms Q1 into Q2. NP-complete (Chandra & Merlin, 1977). use crate::registry::{FieldInfo, ProblemSchemaEntry}; -use crate::traits::{Problem, WitnessProblem}; +use crate::traits::Problem; use serde::{Deserialize, Serialize}; use std::collections::HashSet; @@ -262,7 +262,7 @@ impl ConjunctiveQueryFoldability { impl Problem for ConjunctiveQueryFoldability { const NAME: &'static str = "ConjunctiveQueryFoldability"; - type Value = bool; + type Value = crate::types::Or; fn variant() -> Vec<(&'static str, &'static str)> { crate::variant_params![] @@ -284,39 +284,40 @@ impl Problem for ConjunctiveQueryFoldability { /// /// Returns `true` iff applying the substitution encoded by `config` to every /// atom of Q1 produces exactly the set of atoms in Q2. - fn evaluate(&self, config: &[usize]) -> bool { - if config.len() != self.num_undistinguished { - return false; - } - let range = self.domain_size + self.num_distinguished + self.num_undistinguished; - if config.iter().any(|&v| v >= range) { - return false; - } + fn evaluate(&self, config: &[usize]) -> crate::types::Or { + crate::types::Or({ + if config.len() != self.num_undistinguished { + return crate::types::Or(false); + } + let range = self.domain_size + self.num_distinguished + self.num_undistinguished; + if config.iter().any(|&v| v >= range) { + return crate::types::Or(false); + } - // Apply σ to every atom of Q1. - let substituted: HashSet<(usize, Vec)> = self - .query1_conjuncts - .iter() - .map(|(rel_idx, args)| { - let new_args = args - .iter() - .map(|term| self.apply_substitution(term, config)) - .collect(); - (*rel_idx, new_args) - }) - .collect(); + // Apply σ to every atom of Q1. + let substituted: HashSet<(usize, Vec)> = self + .query1_conjuncts + .iter() + .map(|(rel_idx, args)| { + let new_args = args + .iter() + .map(|term| self.apply_substitution(term, config)) + .collect(); + (*rel_idx, new_args) + }) + .collect(); - // Collect Q2 as a set. - let q2_set: HashSet<(usize, Vec)> = self.query2_conjuncts.iter().cloned().collect(); + // Collect Q2 as a set. + let q2_set: HashSet<(usize, Vec)> = + self.query2_conjuncts.iter().cloned().collect(); - substituted == q2_set + substituted == q2_set + }) } } -impl WitnessProblem for ConjunctiveQueryFoldability {} - crate::declare_variants! { - default sat ConjunctiveQueryFoldability => "(num_distinguished + num_undistinguished + domain_size)^num_undistinguished * num_conjuncts_q1", + default ConjunctiveQueryFoldability => "(num_distinguished + num_undistinguished + domain_size)^num_undistinguished * num_conjuncts_q1", } #[cfg(feature = "example-db")] diff --git a/src/models/misc/consistency_of_database_frequency_tables.rs b/src/models/misc/consistency_of_database_frequency_tables.rs index dc160aea3..04ade7a9e 100644 --- a/src/models/misc/consistency_of_database_frequency_tables.rs +++ b/src/models/misc/consistency_of_database_frequency_tables.rs @@ -7,7 +7,7 @@ //! frequency table and every known value. use crate::registry::{FieldInfo, ProblemSchemaEntry}; -use crate::traits::{Problem, WitnessProblem}; +use crate::traits::Problem; use serde::{Deserialize, Serialize}; use std::collections::BTreeSet; @@ -278,7 +278,7 @@ impl ConsistencyOfDatabaseFrequencyTables { impl Problem for ConsistencyOfDatabaseFrequencyTables { const NAME: &'static str = "ConsistencyOfDatabaseFrequencyTables"; - type Value = bool; + type Value = crate::types::Or; fn variant() -> Vec<(&'static str, &'static str)> { crate::variant_params![] @@ -292,51 +292,51 @@ impl Problem for ConsistencyOfDatabaseFrequencyTables { dims } - fn evaluate(&self, config: &[usize]) -> bool { - if config.len() != self.num_assignment_variables() { - return false; - } + fn evaluate(&self, config: &[usize]) -> crate::types::Or { + crate::types::Or({ + if config.len() != self.num_assignment_variables() { + return crate::types::Or(false); + } - for object in 0..self.num_objects { - for (attribute, &domain_size) in self.attribute_domains.iter().enumerate() { - if config[self.config_index(object, attribute)] >= domain_size { - return false; + for object in 0..self.num_objects { + for (attribute, &domain_size) in self.attribute_domains.iter().enumerate() { + if config[self.config_index(object, attribute)] >= domain_size { + return crate::types::Or(false); + } } } - } - for known_value in &self.known_values { - if config[self.config_index(known_value.object(), known_value.attribute())] - != known_value.value() - { - return false; + for known_value in &self.known_values { + if config[self.config_index(known_value.object(), known_value.attribute())] + != known_value.value() + { + return crate::types::Or(false); + } } - } - for table in &self.frequency_tables { - let rows = self.attribute_domains[table.attribute_a()]; - let cols = self.attribute_domains[table.attribute_b()]; - let mut observed = vec![vec![0usize; cols]; rows]; + for table in &self.frequency_tables { + let rows = self.attribute_domains[table.attribute_a()]; + let cols = self.attribute_domains[table.attribute_b()]; + let mut observed = vec![vec![0usize; cols]; rows]; - for object in 0..self.num_objects { - let value_a = config[self.config_index(object, table.attribute_a())]; - let value_b = config[self.config_index(object, table.attribute_b())]; - observed[value_a][value_b] += 1; - } + for object in 0..self.num_objects { + let value_a = config[self.config_index(object, table.attribute_a())]; + let value_b = config[self.config_index(object, table.attribute_b())]; + observed[value_a][value_b] += 1; + } - if observed != table.counts { - return false; + if observed != table.counts { + return crate::types::Or(false); + } } - } - true + true + }) } } -impl WitnessProblem for ConsistencyOfDatabaseFrequencyTables {} - crate::declare_variants! { - default sat ConsistencyOfDatabaseFrequencyTables => "domain_size_product^num_objects", + default ConsistencyOfDatabaseFrequencyTables => "domain_size_product^num_objects", } #[cfg(feature = "example-db")] diff --git a/src/models/misc/ensemble_computation.rs b/src/models/misc/ensemble_computation.rs index cdf57eb80..0de0d77f0 100644 --- a/src/models/misc/ensemble_computation.rs +++ b/src/models/misc/ensemble_computation.rs @@ -1,7 +1,7 @@ //! Ensemble Computation problem implementation. use crate::registry::{FieldInfo, ProblemSchemaEntry}; -use crate::traits::{Problem, WitnessProblem}; +use crate::traits::Problem; use serde::{Deserialize, Serialize}; inventory::submit! { @@ -146,47 +146,49 @@ impl EnsembleComputation { impl Problem for EnsembleComputation { const NAME: &'static str = "EnsembleComputation"; - type Value = bool; + type Value = crate::types::Or; fn dims(&self) -> Vec { vec![self.universe_size + self.budget; 2 * self.budget] } - fn evaluate(&self, config: &[usize]) -> bool { - if config.len() != 2 * self.budget { - return false; - } - - let Some(required_subsets) = self.required_subsets() else { - return false; - }; - if required_subsets.is_empty() { - return true; - } - - let mut computed = Vec::with_capacity(self.budget); - for step in 0..self.budget { - let left_operand = config[2 * step]; - let right_operand = config[2 * step + 1]; + fn evaluate(&self, config: &[usize]) -> crate::types::Or { + crate::types::Or({ + if config.len() != 2 * self.budget { + return crate::types::Or(false); + } - let Some(left) = self.decode_operand(left_operand, &computed) else { - return false; + let Some(required_subsets) = self.required_subsets() else { + return crate::types::Or(false); }; - let Some(right) = self.decode_operand(right_operand, &computed) else { - return false; - }; - - if !Self::are_disjoint(&left, &right) { - return false; + if required_subsets.is_empty() { + return crate::types::Or(true); } - computed.push(Self::union_disjoint(&left, &right)); - if Self::all_required_subsets_present(&required_subsets, &computed) { - return true; + let mut computed = Vec::with_capacity(self.budget); + for step in 0..self.budget { + let left_operand = config[2 * step]; + let right_operand = config[2 * step + 1]; + + let Some(left) = self.decode_operand(left_operand, &computed) else { + return crate::types::Or(false); + }; + let Some(right) = self.decode_operand(right_operand, &computed) else { + return crate::types::Or(false); + }; + + if !Self::are_disjoint(&left, &right) { + return crate::types::Or(false); + } + + computed.push(Self::union_disjoint(&left, &right)); + if Self::all_required_subsets_present(&required_subsets, &computed) { + return crate::types::Or(true); + } } - } - false + false + }) } fn variant() -> Vec<(&'static str, &'static str)> { @@ -194,10 +196,8 @@ impl Problem for EnsembleComputation { } } -impl WitnessProblem for EnsembleComputation {} - crate::declare_variants! { - default sat EnsembleComputation => "(universe_size + budget)^(2 * budget)", + default EnsembleComputation => "(universe_size + budget)^(2 * budget)", } #[derive(Debug, Clone, Deserialize)] diff --git a/src/models/misc/expected_retrieval_cost.rs b/src/models/misc/expected_retrieval_cost.rs index 17b35dabb..38725c85b 100644 --- a/src/models/misc/expected_retrieval_cost.rs +++ b/src/models/misc/expected_retrieval_cost.rs @@ -5,7 +5,7 @@ //! prescribed bound. use crate::registry::{FieldInfo, ProblemSchemaEntry, ProblemSizeFieldEntry}; -use crate::traits::{Problem, WitnessProblem}; +use crate::traits::Problem; use serde::{Deserialize, Serialize}; const FLOAT_TOLERANCE: f64 = 1e-9; @@ -126,7 +126,7 @@ impl ExpectedRetrievalCost { impl Problem for ExpectedRetrievalCost { const NAME: &'static str = "ExpectedRetrievalCost"; - type Value = bool; + type Value = crate::types::Or; fn variant() -> Vec<(&'static str, &'static str)> { crate::variant_params![] @@ -136,13 +136,11 @@ impl Problem for ExpectedRetrievalCost { vec![self.num_sectors; self.num_records()] } - fn evaluate(&self, config: &[usize]) -> bool { - self.is_valid_solution(config) + fn evaluate(&self, config: &[usize]) -> crate::types::Or { + crate::types::Or(self.is_valid_solution(config)) } } -impl WitnessProblem for ExpectedRetrievalCost {} - fn latency_distance(num_sectors: usize, source: usize, target: usize) -> usize { if source < target { target - source - 1 @@ -152,7 +150,7 @@ fn latency_distance(num_sectors: usize, source: usize, target: usize) -> usize { } crate::declare_variants! { - default sat ExpectedRetrievalCost => "num_sectors ^ num_records", + default ExpectedRetrievalCost => "num_sectors ^ num_records", } #[cfg(feature = "example-db")] diff --git a/src/models/misc/factoring.rs b/src/models/misc/factoring.rs index fef48b531..9b72b2754 100644 --- a/src/models/misc/factoring.rs +++ b/src/models/misc/factoring.rs @@ -4,8 +4,8 @@ //! Given a number N, find two factors (a, b) such that a * b = N. use crate::registry::{FieldInfo, ProblemSchemaEntry}; -use crate::traits::{ObjectiveProblem, Problem}; -use crate::types::{ExtremumSense, Min}; +use crate::traits::Problem; +use crate::types::Min; use serde::{Deserialize, Serialize}; inventory::submit! { @@ -157,16 +157,8 @@ impl Problem for Factoring { } } -impl ObjectiveProblem for Factoring { - type Objective = i32; - - fn direction(&self) -> ExtremumSense { - ExtremumSense::Minimize - } -} - crate::declare_variants! { - default opt Factoring => "exp((m + n)^(1/3) * log(m + n)^(2/3))", + default Factoring => "exp((m + n)^(1/3) * log(m + n)^(2/3))", } #[cfg(feature = "example-db")] diff --git a/src/models/misc/flow_shop_scheduling.rs b/src/models/misc/flow_shop_scheduling.rs index 4300ed6fb..02f55fb87 100644 --- a/src/models/misc/flow_shop_scheduling.rs +++ b/src/models/misc/flow_shop_scheduling.rs @@ -5,7 +5,7 @@ //! be completed by a global deadline D. use crate::registry::{FieldInfo, ProblemSchemaEntry}; -use crate::traits::{Problem, WitnessProblem}; +use crate::traits::Problem; use serde::{Deserialize, Serialize}; inventory::submit! { @@ -160,7 +160,7 @@ impl FlowShopScheduling { impl Problem for FlowShopScheduling { const NAME: &'static str = "FlowShopScheduling"; - type Value = bool; + type Value = crate::types::Or; fn variant() -> Vec<(&'static str, &'static str)> { crate::variant_params![] @@ -171,32 +171,32 @@ impl Problem for FlowShopScheduling { (0..n).rev().map(|i| i + 1).collect() } - fn evaluate(&self, config: &[usize]) -> bool { - let n = self.num_jobs(); - if config.len() != n { - return false; - } + fn evaluate(&self, config: &[usize]) -> crate::types::Or { + crate::types::Or({ + let n = self.num_jobs(); + if config.len() != n { + return crate::types::Or(false); + } - // Decode Lehmer code into a permutation. - // config[i] must be < n - i (the domain size for position i). - let mut available: Vec = (0..n).collect(); - let mut job_order = Vec::with_capacity(n); - for &c in config.iter() { - if c >= available.len() { - return false; + // Decode Lehmer code into a permutation. + // config[i] must be < n - i (the domain size for position i). + let mut available: Vec = (0..n).collect(); + let mut job_order = Vec::with_capacity(n); + for &c in config.iter() { + if c >= available.len() { + return crate::types::Or(false); + } + job_order.push(available.remove(c)); } - job_order.push(available.remove(c)); - } - let makespan = self.compute_makespan(&job_order); - makespan <= self.deadline + let makespan = self.compute_makespan(&job_order); + makespan <= self.deadline + }) } } -impl WitnessProblem for FlowShopScheduling {} - crate::declare_variants! { - default sat FlowShopScheduling => "factorial(num_jobs)", + default FlowShopScheduling => "factorial(num_jobs)", } #[cfg(feature = "example-db")] diff --git a/src/models/misc/grouping_by_swapping.rs b/src/models/misc/grouping_by_swapping.rs index 9fd595db1..ff6cc0d36 100644 --- a/src/models/misc/grouping_by_swapping.rs +++ b/src/models/misc/grouping_by_swapping.rs @@ -5,7 +5,7 @@ //! symbol appears in a single contiguous block. use crate::registry::{FieldInfo, ProblemSchemaEntry}; -use crate::traits::{Problem, WitnessProblem}; +use crate::traits::Problem; use serde::{Deserialize, Serialize}; inventory::submit! { @@ -141,15 +141,17 @@ impl GroupingBySwapping { impl Problem for GroupingBySwapping { const NAME: &'static str = "GroupingBySwapping"; - type Value = bool; + type Value = crate::types::Or; fn dims(&self) -> Vec { vec![self.string_len(); self.budget] } - fn evaluate(&self, config: &[usize]) -> bool { - self.apply_swap_program(config) - .is_some_and(|candidate| self.is_grouped(&candidate)) + fn evaluate(&self, config: &[usize]) -> crate::types::Or { + crate::types::Or({ + self.apply_swap_program(config) + .is_some_and(|candidate| self.is_grouped(&candidate)) + }) } fn variant() -> Vec<(&'static str, &'static str)> { @@ -157,10 +159,8 @@ impl Problem for GroupingBySwapping { } } -impl WitnessProblem for GroupingBySwapping {} - crate::declare_variants! { - default sat GroupingBySwapping => "string_len ^ budget", + default GroupingBySwapping => "string_len ^ budget", } #[cfg(feature = "example-db")] diff --git a/src/models/misc/knapsack.rs b/src/models/misc/knapsack.rs index e6c1a7374..2c282fc8c 100644 --- a/src/models/misc/knapsack.rs +++ b/src/models/misc/knapsack.rs @@ -4,8 +4,8 @@ //! total value while respecting a weight capacity constraint. use crate::registry::{FieldInfo, ProblemSchemaEntry}; -use crate::traits::{ObjectiveProblem, Problem}; -use crate::types::{ExtremumSense, Max}; +use crate::traits::Problem; +use crate::types::Max; use serde::{Deserialize, Serialize}; inventory::submit! { @@ -155,16 +155,8 @@ impl Problem for Knapsack { } } -impl ObjectiveProblem for Knapsack { - type Objective = i64; - - fn direction(&self) -> ExtremumSense { - ExtremumSense::Maximize - } -} - crate::declare_variants! { - default opt Knapsack => "2^(num_items / 2)", + default Knapsack => "2^(num_items / 2)", } mod nonnegative_i64 { diff --git a/src/models/misc/longest_common_subsequence.rs b/src/models/misc/longest_common_subsequence.rs index ac6396916..b6672f8fb 100644 --- a/src/models/misc/longest_common_subsequence.rs +++ b/src/models/misc/longest_common_subsequence.rs @@ -6,7 +6,7 @@ //! "length at least `K`" decision formulation. use crate::registry::{FieldInfo, ProblemSchemaEntry}; -use crate::traits::{Problem, WitnessProblem}; +use crate::traits::Problem; use serde::{Deserialize, Serialize}; inventory::submit! { @@ -134,7 +134,7 @@ fn is_subsequence(candidate: &[usize], target: &[usize]) -> bool { impl Problem for LongestCommonSubsequence { const NAME: &'static str = "LongestCommonSubsequence"; - type Value = bool; + type Value = crate::types::Or; fn variant() -> Vec<(&'static str, &'static str)> { crate::variant_params![] @@ -144,21 +144,21 @@ impl Problem for LongestCommonSubsequence { vec![self.alphabet_size; self.bound] } - fn evaluate(&self, config: &[usize]) -> bool { - if config.len() != self.bound { - return false; - } - if config.iter().any(|&symbol| symbol >= self.alphabet_size) { - return false; - } - self.strings.iter().all(|s| is_subsequence(config, s)) + fn evaluate(&self, config: &[usize]) -> crate::types::Or { + crate::types::Or({ + if config.len() != self.bound { + return crate::types::Or(false); + } + if config.iter().any(|&symbol| symbol >= self.alphabet_size) { + return crate::types::Or(false); + } + self.strings.iter().all(|s| is_subsequence(config, s)) + }) } } -impl WitnessProblem for LongestCommonSubsequence {} - crate::declare_variants! { - default sat LongestCommonSubsequence => "alphabet_size ^ bound", + default LongestCommonSubsequence => "alphabet_size ^ bound", } #[cfg(feature = "example-db")] diff --git a/src/models/misc/minimum_tardiness_sequencing.rs b/src/models/misc/minimum_tardiness_sequencing.rs index 0f7a734f2..17a7fe3d9 100644 --- a/src/models/misc/minimum_tardiness_sequencing.rs +++ b/src/models/misc/minimum_tardiness_sequencing.rs @@ -6,8 +6,8 @@ //! Corresponds to scheduling notation `1|prec, pj=1|sum Uj`. use crate::registry::{FieldInfo, ProblemSchemaEntry}; -use crate::traits::{ObjectiveProblem, Problem}; -use crate::types::{ExtremumSense, Min}; +use crate::traits::Problem; +use crate::types::Min; use serde::{Deserialize, Serialize}; inventory::submit! { @@ -178,16 +178,8 @@ impl Problem for MinimumTardinessSequencing { } } -impl ObjectiveProblem for MinimumTardinessSequencing { - type Objective = usize; - - fn direction(&self) -> ExtremumSense { - ExtremumSense::Minimize - } -} - crate::declare_variants! { - default opt MinimumTardinessSequencing => "2^num_tasks", + default MinimumTardinessSequencing => "2^num_tasks", } #[cfg(feature = "example-db")] diff --git a/src/models/misc/multiprocessor_scheduling.rs b/src/models/misc/multiprocessor_scheduling.rs index 26aa7bd08..4339a99ce 100644 --- a/src/models/misc/multiprocessor_scheduling.rs +++ b/src/models/misc/multiprocessor_scheduling.rs @@ -5,7 +5,7 @@ //! total load exceeds a given deadline. use crate::registry::{FieldInfo, ProblemSchemaEntry}; -use crate::traits::{Problem, WitnessProblem}; +use crate::traits::Problem; use serde::{Deserialize, Serialize}; inventory::submit! { @@ -105,7 +105,7 @@ impl MultiprocessorScheduling { impl Problem for MultiprocessorScheduling { const NAME: &'static str = "MultiprocessorScheduling"; - type Value = bool; + type Value = crate::types::Or; fn variant() -> Vec<(&'static str, &'static str)> { crate::variant_params![] @@ -115,26 +115,26 @@ impl Problem for MultiprocessorScheduling { vec![self.num_processors; self.num_tasks()] } - fn evaluate(&self, config: &[usize]) -> bool { - if config.len() != self.num_tasks() { - return false; - } - let m = self.num_processors; - if config.iter().any(|&p| p >= m) { - return false; - } - let mut loads = vec![0u64; m]; - for (i, &processor) in config.iter().enumerate() { - loads[processor] += self.lengths[i]; - } - loads.iter().all(|&load| load <= self.deadline) + fn evaluate(&self, config: &[usize]) -> crate::types::Or { + crate::types::Or({ + if config.len() != self.num_tasks() { + return crate::types::Or(false); + } + let m = self.num_processors; + if config.iter().any(|&p| p >= m) { + return crate::types::Or(false); + } + let mut loads = vec![0u64; m]; + for (i, &processor) in config.iter().enumerate() { + loads[processor] += self.lengths[i]; + } + loads.iter().all(|&load| load <= self.deadline) + }) } } -impl WitnessProblem for MultiprocessorScheduling {} - crate::declare_variants! { - default sat MultiprocessorScheduling => "2^num_tasks", + default MultiprocessorScheduling => "2^num_tasks", } #[cfg(feature = "example-db")] diff --git a/src/models/misc/paintshop.rs b/src/models/misc/paintshop.rs index e576e4de3..ade889b1a 100644 --- a/src/models/misc/paintshop.rs +++ b/src/models/misc/paintshop.rs @@ -6,8 +6,8 @@ //! The goal is to minimize color switches between adjacent positions. use crate::registry::{FieldInfo, ProblemSchemaEntry}; -use crate::traits::{ObjectiveProblem, Problem}; -use crate::types::{ExtremumSense, Min}; +use crate::traits::Problem; +use crate::types::Min; use serde::{Deserialize, Serialize}; use std::collections::{HashMap, HashSet}; @@ -184,16 +184,8 @@ impl Problem for PaintShop { } } -impl ObjectiveProblem for PaintShop { - type Objective = i32; - - fn direction(&self) -> ExtremumSense { - ExtremumSense::Minimize - } -} - crate::declare_variants! { - default opt PaintShop => "2^num_cars", + default PaintShop => "2^num_cars", } #[cfg(feature = "example-db")] diff --git a/src/models/misc/partially_ordered_knapsack.rs b/src/models/misc/partially_ordered_knapsack.rs index 1ce3c9f33..405dab450 100644 --- a/src/models/misc/partially_ordered_knapsack.rs +++ b/src/models/misc/partially_ordered_knapsack.rs @@ -5,8 +5,8 @@ //! NP-complete in the strong sense (Garey & Johnson, A6 MP12). use crate::registry::{FieldInfo, ProblemSchemaEntry}; -use crate::traits::{ObjectiveProblem, Problem}; -use crate::types::{ExtremumSense, Max}; +use crate::traits::Problem; +use crate::types::Max; use serde::{Deserialize, Serialize}; inventory::submit! { @@ -265,16 +265,8 @@ impl Problem for PartiallyOrderedKnapsack { } } -impl ObjectiveProblem for PartiallyOrderedKnapsack { - type Objective = i64; - - fn direction(&self) -> ExtremumSense { - ExtremumSense::Maximize - } -} - crate::declare_variants! { - default opt PartiallyOrderedKnapsack => "2^num_items", + default PartiallyOrderedKnapsack => "2^num_items", } #[cfg(feature = "example-db")] diff --git a/src/models/misc/partition.rs b/src/models/misc/partition.rs index f9ba59b0a..1f42dddb0 100644 --- a/src/models/misc/partition.rs +++ b/src/models/misc/partition.rs @@ -5,7 +5,7 @@ //! NP-complete problems (1972), Garey & Johnson SP12. use crate::registry::{FieldInfo, ProblemSchemaEntry}; -use crate::traits::{Problem, WitnessProblem}; +use crate::traits::Problem; use serde::{Deserialize, Serialize}; inventory::submit! { @@ -82,7 +82,7 @@ impl Partition { impl Problem for Partition { const NAME: &'static str = "Partition"; - type Value = bool; + type Value = crate::types::Or; fn variant() -> Vec<(&'static str, &'static str)> { crate::variant_params![] @@ -92,27 +92,27 @@ impl Problem for Partition { vec![2; self.num_elements()] } - fn evaluate(&self, config: &[usize]) -> bool { - if config.len() != self.num_elements() { - return false; - } - if config.iter().any(|&v| v >= 2) { - return false; - } - let selected_sum: u64 = config - .iter() - .enumerate() - .filter(|(_, &x)| x == 1) - .map(|(i, _)| self.sizes[i]) - .sum(); - selected_sum * 2 == self.total_sum() + fn evaluate(&self, config: &[usize]) -> crate::types::Or { + crate::types::Or({ + if config.len() != self.num_elements() { + return crate::types::Or(false); + } + if config.iter().any(|&v| v >= 2) { + return crate::types::Or(false); + } + let selected_sum: u64 = config + .iter() + .enumerate() + .filter(|(_, &x)| x == 1) + .map(|(i, _)| self.sizes[i]) + .sum(); + selected_sum * 2 == self.total_sum() + }) } } -impl WitnessProblem for Partition {} - crate::declare_variants! { - default sat Partition => "2^(num_elements / 2)", + default Partition => "2^(num_elements / 2)", } #[cfg(feature = "example-db")] diff --git a/src/models/misc/precedence_constrained_scheduling.rs b/src/models/misc/precedence_constrained_scheduling.rs index a8db36703..e5c887e07 100644 --- a/src/models/misc/precedence_constrained_scheduling.rs +++ b/src/models/misc/precedence_constrained_scheduling.rs @@ -5,7 +5,7 @@ //! respecting precedences. NP-complete via reduction from 3SAT (Ullman, 1975). use crate::registry::{FieldInfo, ProblemSchemaEntry}; -use crate::traits::{Problem, WitnessProblem}; +use crate::traits::Problem; use serde::{Deserialize, Serialize}; inventory::submit! { @@ -118,7 +118,7 @@ impl PrecedenceConstrainedScheduling { impl Problem for PrecedenceConstrainedScheduling { const NAME: &'static str = "PrecedenceConstrainedScheduling"; - type Value = bool; + type Value = crate::types::Or; fn variant() -> Vec<(&'static str, &'static str)> { crate::variant_params![] @@ -128,36 +128,36 @@ impl Problem for PrecedenceConstrainedScheduling { vec![self.deadline; self.num_tasks] } - fn evaluate(&self, config: &[usize]) -> bool { - if config.len() != self.num_tasks { - return false; - } - // Check all values are valid time slots - if config.iter().any(|&v| v >= self.deadline) { - return false; - } - // Check processor capacity: at most num_processors tasks per time slot - let mut slot_count = vec![0usize; self.deadline]; - for &slot in config { - slot_count[slot] += 1; - if slot_count[slot] > self.num_processors { - return false; + fn evaluate(&self, config: &[usize]) -> crate::types::Or { + crate::types::Or({ + if config.len() != self.num_tasks { + return 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 { - return false; + // Check all values are valid time slots + if config.iter().any(|&v| v >= self.deadline) { + return crate::types::Or(false); } - } - true + // Check processor capacity: at most num_processors tasks per time slot + let mut slot_count = vec![0usize; self.deadline]; + for &slot in config { + slot_count[slot] += 1; + if slot_count[slot] > self.num_processors { + return 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 { + return crate::types::Or(false); + } + } + true + }) } } -impl WitnessProblem for PrecedenceConstrainedScheduling {} - crate::declare_variants! { - default sat PrecedenceConstrainedScheduling => "2^num_tasks", + default PrecedenceConstrainedScheduling => "2^num_tasks", } #[cfg(feature = "example-db")] diff --git a/src/models/misc/rectilinear_picture_compression.rs b/src/models/misc/rectilinear_picture_compression.rs index 6d202a93a..13243e40d 100644 --- a/src/models/misc/rectilinear_picture_compression.rs +++ b/src/models/misc/rectilinear_picture_compression.rs @@ -7,7 +7,7 @@ //! and every covered entry must be 1. use crate::registry::{FieldInfo, ProblemSchemaEntry}; -use crate::traits::{Problem, WitnessProblem}; +use crate::traits::Problem; use serde::de::Deserializer; use serde::{Deserialize, Serialize}; @@ -234,7 +234,7 @@ impl RectilinearPictureCompression { impl Problem for RectilinearPictureCompression { const NAME: &'static str = "RectilinearPictureCompression"; - type Value = bool; + type Value = crate::types::Or; fn variant() -> Vec<(&'static str, &'static str)> { crate::variant_params![] @@ -244,52 +244,52 @@ impl Problem for RectilinearPictureCompression { vec![2; self.maximal_rects.len()] } - fn evaluate(&self, config: &[usize]) -> bool { - let rects = &self.maximal_rects; - if config.len() != rects.len() { - return false; - } - if config.iter().any(|&v| v >= 2) { - return false; - } + fn evaluate(&self, config: &[usize]) -> crate::types::Or { + crate::types::Or({ + let rects = &self.maximal_rects; + if config.len() != rects.len() { + return crate::types::Or(false); + } + if config.iter().any(|&v| v >= 2) { + return crate::types::Or(false); + } - // Count selected rectangles. - let selected_count: usize = config.iter().sum(); - if (selected_count as i64) > self.bound { - return false; - } + // Count selected rectangles. + let selected_count: usize = config.iter().sum(); + if (selected_count as i64) > self.bound { + return crate::types::Or(false); + } - // Check that all 1-entries are covered. - let m = self.num_rows(); - let n = self.num_cols(); - let mut covered = vec![vec![false; n]; m]; - for (i, &x) in config.iter().enumerate() { - if x == 1 { - let (r1, c1, r2, c2) = rects[i]; - for row in &mut covered[r1..=r2] { - for cell in &mut row[c1..=c2] { - *cell = true; + // Check that all 1-entries are covered. + let m = self.num_rows(); + let n = self.num_cols(); + let mut covered = vec![vec![false; n]; m]; + for (i, &x) in config.iter().enumerate() { + if x == 1 { + let (r1, c1, r2, c2) = rects[i]; + for row in &mut covered[r1..=r2] { + for cell in &mut row[c1..=c2] { + *cell = true; + } } } } - } - for (row_m, row_c) in self.matrix.iter().zip(covered.iter()) { - for (&entry, &cov) in row_m.iter().zip(row_c.iter()) { - if entry && !cov { - return false; + for (row_m, row_c) in self.matrix.iter().zip(covered.iter()) { + for (&entry, &cov) in row_m.iter().zip(row_c.iter()) { + if entry && !cov { + return crate::types::Or(false); + } } } - } - true + true + }) } } -impl WitnessProblem for RectilinearPictureCompression {} - crate::declare_variants! { - default sat RectilinearPictureCompression => "2^(num_rows * num_cols)", + default RectilinearPictureCompression => "2^(num_rows * num_cols)", } #[cfg(feature = "example-db")] diff --git a/src/models/misc/resource_constrained_scheduling.rs b/src/models/misc/resource_constrained_scheduling.rs index 1c8608cac..4a714371f 100644 --- a/src/models/misc/resource_constrained_scheduling.rs +++ b/src/models/misc/resource_constrained_scheduling.rs @@ -5,7 +5,7 @@ //! processor capacity limit and resource usage constraints per time slot. use crate::registry::{FieldInfo, ProblemSchemaEntry}; -use crate::traits::{Problem, WitnessProblem}; +use crate::traits::Problem; use serde::{Deserialize, Serialize}; inventory::submit! { @@ -133,7 +133,7 @@ impl ResourceConstrainedScheduling { impl Problem for ResourceConstrainedScheduling { const NAME: &'static str = "ResourceConstrainedScheduling"; - type Value = bool; + type Value = crate::types::Or; fn variant() -> Vec<(&'static str, &'static str)> { crate::variant_params![] @@ -143,61 +143,61 @@ impl Problem for ResourceConstrainedScheduling { vec![self.deadline as usize; self.num_tasks()] } - fn evaluate(&self, config: &[usize]) -> bool { - let n = self.num_tasks(); - let d = self.deadline as usize; - let r = self.num_resources(); + fn evaluate(&self, config: &[usize]) -> crate::types::Or { + crate::types::Or({ + let n = self.num_tasks(); + let d = self.deadline as usize; + let r = self.num_resources(); - // Check config length - if config.len() != n { - return false; - } + // Check config length + if config.len() != n { + return crate::types::Or(false); + } - // Check all time slots are in range - if config.iter().any(|&slot| slot >= d) { - return false; - } + // Check all time slots are in range + if config.iter().any(|&slot| slot >= d) { + return crate::types::Or(false); + } - // Check processor capacity and resource constraints at each time slot - for u in 0..d { - // Collect tasks scheduled at time slot u - let mut task_count = 0usize; - let mut resource_usage = vec![0u64; r]; - - for (t, &slot) in config.iter().enumerate() { - if slot == u { - task_count += 1; - // Accumulate resource usage - for (usage, &req) in resource_usage - .iter_mut() - .zip(self.resource_requirements[t].iter()) - { - *usage = usage.saturating_add(req); + // Check processor capacity and resource constraints at each time slot + for u in 0..d { + // Collect tasks scheduled at time slot u + let mut task_count = 0usize; + let mut resource_usage = vec![0u64; r]; + + for (t, &slot) in config.iter().enumerate() { + if slot == u { + task_count += 1; + // Accumulate resource usage + for (usage, &req) in resource_usage + .iter_mut() + .zip(self.resource_requirements[t].iter()) + { + *usage = usage.saturating_add(req); + } } } - } - // Check processor capacity - if task_count > self.num_processors { - return false; - } + // Check processor capacity + if task_count > self.num_processors { + return crate::types::Or(false); + } - // Check resource bounds - for (usage, bound) in resource_usage.iter().zip(self.resource_bounds.iter()) { - if usage > bound { - return false; + // Check resource bounds + for (usage, bound) in resource_usage.iter().zip(self.resource_bounds.iter()) { + if usage > bound { + return crate::types::Or(false); + } } } - } - true + true + }) } } -impl WitnessProblem for ResourceConstrainedScheduling {} - crate::declare_variants! { - default sat ResourceConstrainedScheduling => "deadline ^ num_tasks", + default ResourceConstrainedScheduling => "deadline ^ num_tasks", } #[cfg(feature = "example-db")] diff --git a/src/models/misc/scheduling_with_individual_deadlines.rs b/src/models/misc/scheduling_with_individual_deadlines.rs index 2f39e48e1..391ca772b 100644 --- a/src/models/misc/scheduling_with_individual_deadlines.rs +++ b/src/models/misc/scheduling_with_individual_deadlines.rs @@ -5,7 +5,7 @@ //! every task finishes by its own deadline. use crate::registry::{FieldInfo, ProblemSchemaEntry}; -use crate::traits::{Problem, WitnessProblem}; +use crate::traits::Problem; use serde::{Deserialize, Serialize}; use std::collections::BTreeMap; @@ -102,7 +102,7 @@ impl SchedulingWithIndividualDeadlines { impl Problem for SchedulingWithIndividualDeadlines { const NAME: &'static str = "SchedulingWithIndividualDeadlines"; - type Value = bool; + type Value = crate::types::Or; fn variant() -> Vec<(&'static str, &'static str)> { crate::variant_params![] @@ -112,40 +112,40 @@ impl Problem for SchedulingWithIndividualDeadlines { self.deadlines.clone() } - fn evaluate(&self, config: &[usize]) -> bool { - if config.len() != self.num_tasks { - return false; - } + fn evaluate(&self, config: &[usize]) -> crate::types::Or { + crate::types::Or({ + if config.len() != self.num_tasks { + return crate::types::Or(false); + } - for (&start, &deadline) in config.iter().zip(&self.deadlines) { - if start >= deadline { - return false; + for (&start, &deadline) in config.iter().zip(&self.deadlines) { + if start >= deadline { + return crate::types::Or(false); + } } - } - for &(pred, succ) in &self.precedences { - if config[pred] + 1 > config[succ] { - return false; + for &(pred, succ) in &self.precedences { + if config[pred] + 1 > config[succ] { + return crate::types::Or(false); + } } - } - let mut slot_loads = BTreeMap::new(); - for &start in config { - let load = slot_loads.entry(start).or_insert(0usize); - *load += 1; - if *load > self.num_processors { - return false; + let mut slot_loads = BTreeMap::new(); + for &start in config { + let load = slot_loads.entry(start).or_insert(0usize); + *load += 1; + if *load > self.num_processors { + return crate::types::Or(false); + } } - } - true + true + }) } } -impl WitnessProblem for SchedulingWithIndividualDeadlines {} - crate::declare_variants! { - default sat SchedulingWithIndividualDeadlines => "max_deadline^num_tasks", + default SchedulingWithIndividualDeadlines => "max_deadline^num_tasks", } #[cfg(feature = "example-db")] 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 0fd0294e0..56a8e5136 100644 --- a/src/models/misc/sequencing_to_minimize_maximum_cumulative_cost.rs +++ b/src/models/misc/sequencing_to_minimize_maximum_cumulative_cost.rs @@ -5,7 +5,7 @@ //! cost never exceeds a given bound. use crate::registry::{FieldInfo, ProblemSchemaEntry}; -use crate::traits::{Problem, WitnessProblem}; +use crate::traits::Problem; use serde::de::Error as _; use serde::{Deserialize, Serialize}; @@ -153,7 +153,7 @@ fn precedence_validation_error(precedences: &[(usize, usize)], num_tasks: usize) impl Problem for SequencingToMinimizeMaximumCumulativeCost { const NAME: &'static str = "SequencingToMinimizeMaximumCumulativeCost"; - type Value = bool; + type Value = crate::types::Or; fn variant() -> Vec<(&'static str, &'static str)> { crate::variant_params![] @@ -164,36 +164,36 @@ impl Problem for SequencingToMinimizeMaximumCumulativeCost { (0..n).rev().map(|i| i + 1).collect() } - fn evaluate(&self, config: &[usize]) -> bool { - let Some(schedule) = self.decode_schedule(config) else { - return false; - }; + fn evaluate(&self, config: &[usize]) -> crate::types::Or { + crate::types::Or({ + let Some(schedule) = self.decode_schedule(config) else { + return crate::types::Or(false); + }; - let mut positions = vec![0usize; self.num_tasks()]; - for (position, &task) in schedule.iter().enumerate() { - positions[task] = position; - } - for &(pred, succ) in &self.precedences { - if positions[pred] >= positions[succ] { - return false; + let mut positions = vec![0usize; self.num_tasks()]; + for (position, &task) in schedule.iter().enumerate() { + positions[task] = position; + } + for &(pred, succ) in &self.precedences { + if positions[pred] >= positions[succ] { + return crate::types::Or(false); + } } - } - let mut cumulative = 0i64; - for &task in &schedule { - cumulative += self.costs[task]; - if cumulative > self.bound { - return false; + let mut cumulative = 0i64; + for &task in &schedule { + cumulative += self.costs[task]; + if cumulative > self.bound { + return crate::types::Or(false); + } } - } - true + true + }) } } -impl WitnessProblem for SequencingToMinimizeMaximumCumulativeCost {} - crate::declare_variants! { - default sat SequencingToMinimizeMaximumCumulativeCost => "factorial(num_tasks)", + default SequencingToMinimizeMaximumCumulativeCost => "factorial(num_tasks)", } #[cfg(feature = "example-db")] 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 12c76c514..76cd0962f 100644 --- a/src/models/misc/sequencing_to_minimize_weighted_completion_time.rs +++ b/src/models/misc/sequencing_to_minimize_weighted_completion_time.rs @@ -6,8 +6,8 @@ //! weighted completion time. use crate::registry::{FieldInfo, ProblemSchemaEntry}; -use crate::traits::{ObjectiveProblem, Problem}; -use crate::types::{ExtremumSense, Min}; +use crate::traits::Problem; +use crate::types::Min; use serde::{Deserialize, Serialize}; inventory::submit! { @@ -226,16 +226,8 @@ impl Problem for SequencingToMinimizeWeightedCompletionTime { } } -impl ObjectiveProblem for SequencingToMinimizeWeightedCompletionTime { - type Objective = u64; - - fn direction(&self) -> ExtremumSense { - ExtremumSense::Minimize - } -} - crate::declare_variants! { - default opt SequencingToMinimizeWeightedCompletionTime => "factorial(num_tasks)", + default SequencingToMinimizeWeightedCompletionTime => "factorial(num_tasks)", } #[cfg(feature = "example-db")] diff --git a/src/models/misc/sequencing_to_minimize_weighted_tardiness.rs b/src/models/misc/sequencing_to_minimize_weighted_tardiness.rs index 83d6e9e62..8d1daa326 100644 --- a/src/models/misc/sequencing_to_minimize_weighted_tardiness.rs +++ b/src/models/misc/sequencing_to_minimize_weighted_tardiness.rs @@ -6,7 +6,7 @@ //! Corresponds to scheduling notation `1 || sum w_j T_j`. use crate::registry::{FieldInfo, ProblemSchemaEntry}; -use crate::traits::{Problem, WitnessProblem}; +use crate::traits::Problem; use serde::{Deserialize, Serialize}; inventory::submit! { @@ -153,7 +153,7 @@ impl SequencingToMinimizeWeightedTardiness { impl Problem for SequencingToMinimizeWeightedTardiness { const NAME: &'static str = "SequencingToMinimizeWeightedTardiness"; - type Value = bool; + type Value = crate::types::Or; fn variant() -> Vec<(&'static str, &'static str)> { crate::variant_params![] @@ -164,16 +164,16 @@ impl Problem for SequencingToMinimizeWeightedTardiness { (0..n).rev().map(|i| i + 1).collect() } - fn evaluate(&self, config: &[usize]) -> bool { - self.total_weighted_tardiness(config) - .is_some_and(|total| total <= self.bound) + fn evaluate(&self, config: &[usize]) -> crate::types::Or { + crate::types::Or({ + self.total_weighted_tardiness(config) + .is_some_and(|total| total <= self.bound) + }) } } -impl WitnessProblem for SequencingToMinimizeWeightedTardiness {} - crate::declare_variants! { - default sat SequencingToMinimizeWeightedTardiness => "factorial(num_tasks)", + default SequencingToMinimizeWeightedTardiness => "factorial(num_tasks)", } #[cfg(feature = "example-db")] 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 27d243291..f81bce5fe 100644 --- a/src/models/misc/sequencing_with_release_times_and_deadlines.rs +++ b/src/models/misc/sequencing_with_release_times_and_deadlines.rs @@ -6,7 +6,7 @@ //! Strongly NP-complete (Garey & Johnson, A5 SS1). use crate::registry::{FieldInfo, ProblemSchemaEntry}; -use crate::traits::{Problem, WitnessProblem}; +use crate::traits::Problem; use serde::{Deserialize, Serialize}; inventory::submit! { @@ -106,7 +106,7 @@ impl SequencingWithReleaseTimesAndDeadlines { impl Problem for SequencingWithReleaseTimesAndDeadlines { const NAME: &'static str = "SequencingWithReleaseTimesAndDeadlines"; - type Value = bool; + type Value = crate::types::Or; fn variant() -> Vec<(&'static str, &'static str)> { crate::variant_params![] @@ -117,41 +117,41 @@ impl Problem for SequencingWithReleaseTimesAndDeadlines { (0..n).rev().map(|i| i + 1).collect() } - fn evaluate(&self, config: &[usize]) -> bool { - let n = self.num_tasks(); - if config.len() != n { - return false; - } + fn evaluate(&self, config: &[usize]) -> crate::types::Or { + crate::types::Or({ + let n = self.num_tasks(); + if config.len() != n { + return crate::types::Or(false); + } - // Decode Lehmer code into a permutation of task indices. - let mut available: Vec = (0..n).collect(); - let mut schedule = Vec::with_capacity(n); - for &c in config.iter() { - if c >= available.len() { - return false; + // Decode Lehmer code into a permutation of task indices. + let mut available: Vec = (0..n).collect(); + let mut schedule = Vec::with_capacity(n); + for &c in config.iter() { + if c >= available.len() { + return crate::types::Or(false); + } + schedule.push(available.remove(c)); } - schedule.push(available.remove(c)); - } - // Schedule tasks left-to-right: each task starts at max(release_time, current_time). - let mut current_time: u64 = 0; - for &task in &schedule { - let start = current_time.max(self.release_times[task]); - let finish = start + self.lengths[task]; - if finish > self.deadlines[task] { - return false; + // Schedule tasks left-to-right: each task starts at max(release_time, current_time). + let mut current_time: u64 = 0; + for &task in &schedule { + let start = current_time.max(self.release_times[task]); + let finish = start + self.lengths[task]; + if finish > self.deadlines[task] { + return crate::types::Or(false); + } + current_time = finish; } - current_time = finish; - } - true + true + }) } } -impl WitnessProblem for SequencingWithReleaseTimesAndDeadlines {} - crate::declare_variants! { - default sat SequencingWithReleaseTimesAndDeadlines => "2^num_tasks * num_tasks", + default SequencingWithReleaseTimesAndDeadlines => "2^num_tasks * num_tasks", } #[cfg(feature = "example-db")] diff --git a/src/models/misc/sequencing_within_intervals.rs b/src/models/misc/sequencing_within_intervals.rs index 6306b910a..cc391444a 100644 --- a/src/models/misc/sequencing_within_intervals.rs +++ b/src/models/misc/sequencing_within_intervals.rs @@ -5,7 +5,7 @@ //! task runs entirely within its allowed time window. use crate::registry::{FieldInfo, ProblemSchemaEntry}; -use crate::traits::{Problem, WitnessProblem}; +use crate::traits::Problem; use serde::{Deserialize, Serialize}; inventory::submit! { @@ -122,7 +122,7 @@ impl SequencingWithinIntervals { impl Problem for SequencingWithinIntervals { const NAME: &'static str = "SequencingWithinIntervals"; - type Value = bool; + type Value = crate::types::Or; fn variant() -> Vec<(&'static str, &'static str)> { crate::variant_params![] @@ -134,45 +134,46 @@ impl Problem for SequencingWithinIntervals { .collect() } - fn evaluate(&self, config: &[usize]) -> bool { - let n = self.num_tasks(); - if config.len() != n { - return false; - } + fn evaluate(&self, config: &[usize]) -> crate::types::Or { + crate::types::Or({ + let n = self.num_tasks(); + if config.len() != n { + return crate::types::Or(false); + } - // Check each variable is within range and compute start times - let mut starts = Vec::with_capacity(n); - for (i, &c) in config.iter().enumerate() { - let dim = (self.deadlines[i] - self.release_times[i] - self.lengths[i] + 1) as usize; - if c >= dim { - return false; + // Check each variable is within range and compute start times + let mut starts = Vec::with_capacity(n); + for (i, &c) in config.iter().enumerate() { + let dim = + (self.deadlines[i] - self.release_times[i] - self.lengths[i] + 1) as usize; + if c >= dim { + return crate::types::Or(false); + } + // start = r[i] + c, and c < dim = d[i] - r[i] - l[i] + 1, + // so start + l[i] <= d[i] is guaranteed by construction. + let start = self.release_times[i] + c as u64; + starts.push(start); } - // start = r[i] + c, and c < dim = d[i] - r[i] - l[i] + 1, - // so start + l[i] <= d[i] is guaranteed by construction. - let start = self.release_times[i] + c as u64; - starts.push(start); - } - // Check no two tasks overlap - for i in 0..n { - for j in (i + 1)..n { - let end_i = starts[i] + self.lengths[i]; - let end_j = starts[j] + self.lengths[j]; - // Tasks overlap if neither finishes before the other starts - if !(end_i <= starts[j] || end_j <= starts[i]) { - return false; + // Check no two tasks overlap + for i in 0..n { + for j in (i + 1)..n { + let end_i = starts[i] + self.lengths[i]; + let end_j = starts[j] + self.lengths[j]; + // Tasks overlap if neither finishes before the other starts + if !(end_i <= starts[j] || end_j <= starts[i]) { + return crate::types::Or(false); + } } } - } - true + true + }) } } -impl WitnessProblem for SequencingWithinIntervals {} - crate::declare_variants! { - default sat SequencingWithinIntervals => "2^num_tasks", + default SequencingWithinIntervals => "2^num_tasks", } #[cfg(feature = "example-db")] diff --git a/src/models/misc/shortest_common_supersequence.rs b/src/models/misc/shortest_common_supersequence.rs index bb8337849..759b9efbc 100644 --- a/src/models/misc/shortest_common_supersequence.rs +++ b/src/models/misc/shortest_common_supersequence.rs @@ -11,7 +11,7 @@ //! to the standard `|w| ≤ B` formulation. This problem is NP-hard (Maier, 1978). use crate::registry::{FieldInfo, ProblemSchemaEntry}; -use crate::traits::{Problem, WitnessProblem}; +use crate::traits::Problem; use serde::{Deserialize, Serialize}; inventory::submit! { @@ -125,7 +125,7 @@ fn is_subsequence(needle: &[usize], haystack: &[usize]) -> bool { impl Problem for ShortestCommonSupersequence { const NAME: &'static str = "ShortestCommonSupersequence"; - type Value = bool; + type Value = crate::types::Or; fn variant() -> Vec<(&'static str, &'static str)> { crate::variant_params![] @@ -135,21 +135,21 @@ impl Problem for ShortestCommonSupersequence { vec![self.alphabet_size; self.bound] } - fn evaluate(&self, config: &[usize]) -> bool { - if config.len() != self.bound { - return false; - } - if config.iter().any(|&v| v >= self.alphabet_size) { - return false; - } - self.strings.iter().all(|s| is_subsequence(s, config)) + fn evaluate(&self, config: &[usize]) -> crate::types::Or { + crate::types::Or({ + if config.len() != self.bound { + return crate::types::Or(false); + } + if config.iter().any(|&v| v >= self.alphabet_size) { + return crate::types::Or(false); + } + self.strings.iter().all(|s| is_subsequence(s, config)) + }) } } -impl WitnessProblem for ShortestCommonSupersequence {} - crate::declare_variants! { - default sat ShortestCommonSupersequence => "alphabet_size ^ bound", + default ShortestCommonSupersequence => "alphabet_size ^ bound", } #[cfg(feature = "example-db")] diff --git a/src/models/misc/stacker_crane.rs b/src/models/misc/stacker_crane.rs index 974067dae..40f8b27e4 100644 --- a/src/models/misc/stacker_crane.rs +++ b/src/models/misc/stacker_crane.rs @@ -5,7 +5,7 @@ //! traverses every required arc in some order and stays within the bound. use crate::registry::{FieldInfo, ProblemSchemaEntry}; -use crate::traits::{Problem, WitnessProblem}; +use crate::traits::Problem; use serde::{Deserialize, Serialize}; use std::cmp::Reverse; use std::collections::BinaryHeap; @@ -259,7 +259,7 @@ impl StackerCrane { impl Problem for StackerCrane { const NAME: &'static str = "StackerCrane"; - type Value = bool; + type Value = crate::types::Or; fn variant() -> Vec<(&'static str, &'static str)> { crate::variant_params![] @@ -269,15 +269,15 @@ impl Problem for StackerCrane { vec![self.num_arcs(); self.num_arcs()] } - fn evaluate(&self, config: &[usize]) -> bool { - matches!(self.closed_walk_length(config), Some(total) if total <= self.bound) + fn evaluate(&self, config: &[usize]) -> crate::types::Or { + crate::types::Or({ + matches!(self.closed_walk_length(config), Some(total) if total <= self.bound) + }) } } -impl WitnessProblem for StackerCrane {} - crate::declare_variants! { - default sat StackerCrane => "num_vertices^2 * 2^num_arcs", + default StackerCrane => "num_vertices^2 * 2^num_arcs", } #[derive(Debug, Clone, Deserialize)] diff --git a/src/models/misc/staff_scheduling.rs b/src/models/misc/staff_scheduling.rs index 1a19e4523..7db6f75be 100644 --- a/src/models/misc/staff_scheduling.rs +++ b/src/models/misc/staff_scheduling.rs @@ -5,7 +5,7 @@ //! all requirements are met without exceeding the budget. use crate::registry::{FieldInfo, ProblemSchemaEntry}; -use crate::traits::{Problem, WitnessProblem}; +use crate::traits::Problem; use serde::{Deserialize, Serialize}; inventory::submit! { @@ -150,19 +150,21 @@ impl StaffScheduling { impl Problem for StaffScheduling { const NAME: &'static str = "StaffScheduling"; - type Value = bool; + type Value = crate::types::Or; fn dims(&self) -> Vec { vec![self.worker_limit() + 1; self.num_schedules()] } - fn evaluate(&self, config: &[usize]) -> bool { - if config.len() != self.num_schedules() { - return false; - } - self.worker_counts_valid(config) - && self.within_budget(config) - && self.meets_requirements(config) + fn evaluate(&self, config: &[usize]) -> crate::types::Or { + crate::types::Or({ + if config.len() != self.num_schedules() { + return crate::types::Or(false); + } + self.worker_counts_valid(config) + && self.within_budget(config) + && self.meets_requirements(config) + }) } fn variant() -> Vec<(&'static str, &'static str)> { @@ -170,10 +172,8 @@ impl Problem for StaffScheduling { } } -impl WitnessProblem for StaffScheduling {} - crate::declare_variants! { - default sat StaffScheduling => "(num_workers + 1)^num_schedules", + default StaffScheduling => "(num_workers + 1)^num_schedules", } #[cfg(feature = "example-db")] diff --git a/src/models/misc/string_to_string_correction.rs b/src/models/misc/string_to_string_correction.rs index 03e7fce94..30f02a3d0 100644 --- a/src/models/misc/string_to_string_correction.rs +++ b/src/models/misc/string_to_string_correction.rs @@ -15,7 +15,7 @@ //! This problem is NP-complete (Wagner, 1975). use crate::registry::{FieldInfo, ProblemSchemaEntry}; -use crate::traits::{Problem, WitnessProblem}; +use crate::traits::Problem; use serde::{Deserialize, Serialize}; inventory::submit! { @@ -139,7 +139,7 @@ impl StringToStringCorrection { impl Problem for StringToStringCorrection { const NAME: &'static str = "StringToStringCorrection"; - type Value = bool; + type Value = crate::types::Or; fn variant() -> Vec<(&'static str, &'static str)> { crate::variant_params![] @@ -149,49 +149,49 @@ impl Problem for StringToStringCorrection { vec![2 * self.source.len() + 1; self.bound] } - fn evaluate(&self, config: &[usize]) -> bool { - if config.len() != self.bound { - return false; - } - if self.target.len() > self.source.len() - || self.target.len() < self.source.len().saturating_sub(self.bound) - { - return false; - } - let n = self.source.len(); - let domain = 2 * n + 1; - if config.iter().any(|&v| v >= domain) { - return false; - } - let noop = 2 * n; - let mut working = self.source.clone(); - for &op in config { - if op == noop { - // no-op - continue; + fn evaluate(&self, config: &[usize]) -> crate::types::Or { + crate::types::Or({ + if config.len() != self.bound { + return crate::types::Or(false); + } + if self.target.len() > self.source.len() + || self.target.len() < self.source.len().saturating_sub(self.bound) + { + return crate::types::Or(false); } - let current_len = working.len(); - if op < current_len { - // delete at index op - working.remove(op); - } else { - let swap_pos = op - current_len; - if swap_pos + 1 < current_len { - working.swap(swap_pos, swap_pos + 1); + let n = self.source.len(); + let domain = 2 * n + 1; + if config.iter().any(|&v| v >= domain) { + return crate::types::Or(false); + } + let noop = 2 * n; + let mut working = self.source.clone(); + for &op in config { + if op == noop { + // no-op + continue; + } + let current_len = working.len(); + if op < current_len { + // delete at index op + working.remove(op); } else { - // invalid operation for current string state - return false; + let swap_pos = op - current_len; + if swap_pos + 1 < current_len { + working.swap(swap_pos, swap_pos + 1); + } else { + // invalid operation for current string state + return crate::types::Or(false); + } } } - } - working == self.target + working == self.target + }) } } -impl WitnessProblem for StringToStringCorrection {} - crate::declare_variants! { - default sat StringToStringCorrection => "(2 * source_length + 1) ^ bound", + default StringToStringCorrection => "(2 * source_length + 1) ^ bound", } #[cfg(feature = "example-db")] diff --git a/src/models/misc/subset_sum.rs b/src/models/misc/subset_sum.rs index 03fe0f457..873233e99 100644 --- a/src/models/misc/subset_sum.rs +++ b/src/models/misc/subset_sum.rs @@ -8,7 +8,7 @@ //! reductions can construct large instances without fixed-width overflow. use crate::registry::{FieldInfo, ProblemSchemaEntry}; -use crate::traits::{Problem, WitnessProblem}; +use crate::traits::Problem; use num_bigint::{BigUint, ToBigUint}; use num_traits::Zero; use serde::{Deserialize, Serialize}; @@ -108,7 +108,7 @@ impl SubsetSum { impl Problem for SubsetSum { const NAME: &'static str = "SubsetSum"; - type Value = bool; + type Value = crate::types::Or; fn variant() -> Vec<(&'static str, &'static str)> { crate::variant_params![] @@ -118,27 +118,27 @@ impl Problem for SubsetSum { vec![2; self.num_elements()] } - fn evaluate(&self, config: &[usize]) -> bool { - if config.len() != self.num_elements() { - return false; - } - if config.iter().any(|&v| v >= 2) { - return false; - } - let mut total = BigUint::zero(); - for (i, &x) in config.iter().enumerate() { - if x == 1 { - total += &self.sizes[i]; + fn evaluate(&self, config: &[usize]) -> crate::types::Or { + crate::types::Or({ + if config.len() != self.num_elements() { + return crate::types::Or(false); } - } - total == self.target + if config.iter().any(|&v| v >= 2) { + return crate::types::Or(false); + } + let mut total = BigUint::zero(); + for (i, &x) in config.iter().enumerate() { + if x == 1 { + total += &self.sizes[i]; + } + } + total == self.target + }) } } -impl WitnessProblem for SubsetSum {} - crate::declare_variants! { - default sat SubsetSum => "2^(num_elements / 2)", + default SubsetSum => "2^(num_elements / 2)", } mod decimal_biguint { diff --git a/src/models/misc/sum_of_squares_partition.rs b/src/models/misc/sum_of_squares_partition.rs index 31a7000b0..c3768290f 100644 --- a/src/models/misc/sum_of_squares_partition.rs +++ b/src/models/misc/sum_of_squares_partition.rs @@ -6,7 +6,7 @@ //! NP-complete in the strong sense (Garey & Johnson, SP19). use crate::registry::{FieldInfo, ProblemSchemaEntry}; -use crate::traits::{Problem, WitnessProblem}; +use crate::traits::Problem; use serde::de::Error; use serde::{Deserialize, Deserializer, Serialize}; @@ -164,7 +164,7 @@ impl<'de> Deserialize<'de> for SumOfSquaresPartition { impl Problem for SumOfSquaresPartition { const NAME: &'static str = "SumOfSquaresPartition"; - type Value = bool; + type Value = crate::types::Or; fn variant() -> Vec<(&'static str, &'static str)> { crate::variant_params![] @@ -174,18 +174,18 @@ impl Problem for SumOfSquaresPartition { vec![self.num_groups; self.sizes.len()] } - fn evaluate(&self, config: &[usize]) -> bool { - match self.sum_of_squares(config) { - Some(sos) => sos <= self.bound, - None => false, - } + fn evaluate(&self, config: &[usize]) -> crate::types::Or { + crate::types::Or({ + match self.sum_of_squares(config) { + Some(sos) => sos <= self.bound, + None => false, + } + }) } } -impl WitnessProblem for SumOfSquaresPartition {} - crate::declare_variants! { - default sat SumOfSquaresPartition => "num_groups^num_elements", + default SumOfSquaresPartition => "num_groups^num_elements", } #[cfg(feature = "example-db")] diff --git a/src/models/misc/timetable_design.rs b/src/models/misc/timetable_design.rs index c0776172f..ba290ce40 100644 --- a/src/models/misc/timetable_design.rs +++ b/src/models/misc/timetable_design.rs @@ -5,7 +5,7 @@ //! requirements. use crate::registry::{FieldInfo, ProblemSchemaEntry}; -use crate::traits::{Problem, WitnessProblem}; +use crate::traits::Problem; use serde::{Deserialize, Serialize}; inventory::submit! { @@ -303,47 +303,51 @@ impl TimetableDesign { impl Problem for TimetableDesign { const NAME: &'static str = "TimetableDesign"; - type Value = bool; + type Value = crate::types::Or; fn dims(&self) -> Vec { vec![2; self.config_len()] } - fn evaluate(&self, config: &[usize]) -> bool { - if config.len() != self.config_len() { - return false; - } - if config.iter().any(|&value| value > 1) { - return false; - } - - let mut craftsman_busy = vec![vec![false; self.num_periods]; self.num_craftsmen]; - let mut task_busy = vec![vec![false; self.num_periods]; self.num_tasks]; - let mut pair_counts = vec![vec![0u64; self.num_tasks]; self.num_craftsmen]; - - for craftsman in 0..self.num_craftsmen { - for task in 0..self.num_tasks { - for period in 0..self.num_periods { - if config[self.index(craftsman, task, period)] == 0 { - continue; - } - - if !self.craftsman_avail[craftsman][period] || !self.task_avail[task][period] { - return false; - } + fn evaluate(&self, config: &[usize]) -> crate::types::Or { + crate::types::Or({ + if config.len() != self.config_len() { + return crate::types::Or(false); + } + if config.iter().any(|&value| value > 1) { + return crate::types::Or(false); + } - if craftsman_busy[craftsman][period] || task_busy[task][period] { - return false; + let mut craftsman_busy = vec![vec![false; self.num_periods]; self.num_craftsmen]; + let mut task_busy = vec![vec![false; self.num_periods]; self.num_tasks]; + let mut pair_counts = vec![vec![0u64; self.num_tasks]; self.num_craftsmen]; + + for craftsman in 0..self.num_craftsmen { + for task in 0..self.num_tasks { + for period in 0..self.num_periods { + if config[self.index(craftsman, task, period)] == 0 { + continue; + } + + if !self.craftsman_avail[craftsman][period] + || !self.task_avail[task][period] + { + return crate::types::Or(false); + } + + if craftsman_busy[craftsman][period] || task_busy[task][period] { + return crate::types::Or(false); + } + + craftsman_busy[craftsman][period] = true; + task_busy[task][period] = true; + pair_counts[craftsman][task] += 1; } - - craftsman_busy[craftsman][period] = true; - task_busy[task][period] = true; - pair_counts[craftsman][task] += 1; } } - } - pair_counts == self.requirements + pair_counts == self.requirements + }) } fn variant() -> Vec<(&'static str, &'static str)> { @@ -351,10 +355,8 @@ impl Problem for TimetableDesign { } } -impl WitnessProblem for TimetableDesign {} - crate::declare_variants! { - default sat TimetableDesign => "2^(num_craftsmen * num_tasks * num_periods)", + default TimetableDesign => "2^(num_craftsmen * num_tasks * num_periods)", } #[cfg(any(test, feature = "example-db"))] diff --git a/src/models/set/comparative_containment.rs b/src/models/set/comparative_containment.rs index e04cb3a7b..4076925da 100644 --- a/src/models/set/comparative_containment.rs +++ b/src/models/set/comparative_containment.rs @@ -5,7 +5,7 @@ //! in the first family is at least its containment weight in the second. use crate::registry::{FieldInfo, ProblemSchemaEntry, VariantDimension}; -use crate::traits::{Problem, WitnessProblem}; +use crate::traits::Problem; use crate::types::{One, WeightElement}; use num_traits::Zero; use serde::{Deserialize, Serialize}; @@ -177,14 +177,14 @@ where W: WeightElement + crate::variant::VariantParam, { const NAME: &'static str = "ComparativeContainment"; - type Value = bool; + type Value = crate::types::Or; fn dims(&self) -> Vec { vec![2; self.universe_size] } - fn evaluate(&self, config: &[usize]) -> bool { - self.is_valid_solution(config) + fn evaluate(&self, config: &[usize]) -> crate::types::Or { + crate::types::Or(self.is_valid_solution(config)) } fn variant() -> Vec<(&'static str, &'static str)> { @@ -192,15 +192,10 @@ where } } -impl WitnessProblem for ComparativeContainment where - W: WeightElement + crate::variant::VariantParam -{ -} - crate::declare_variants! { - sat ComparativeContainment => "2^universe_size", - default sat ComparativeContainment => "2^universe_size", - sat ComparativeContainment => "2^universe_size", + ComparativeContainment => "2^universe_size", + default ComparativeContainment => "2^universe_size", + ComparativeContainment => "2^universe_size", } fn validate_set_family(label: &str, universe_size: usize, sets: &[Vec]) { diff --git a/src/models/set/consecutive_sets.rs b/src/models/set/consecutive_sets.rs index d95796d08..1e50f29dc 100644 --- a/src/models/set/consecutive_sets.rs +++ b/src/models/set/consecutive_sets.rs @@ -6,7 +6,7 @@ //! contiguous block in some order) within the string. use crate::registry::{FieldInfo, ProblemSchemaEntry}; -use crate::traits::{Problem, WitnessProblem}; +use crate::traits::Problem; use serde::{Deserialize, Serialize}; use std::collections::HashSet; @@ -135,83 +135,85 @@ impl ConsecutiveSets { impl Problem for ConsecutiveSets { const NAME: &'static str = "ConsecutiveSets"; - type Value = bool; + type Value = crate::types::Or; fn dims(&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 evaluate(&self, config: &[usize]) -> bool { - // 1. Validate config - if config.len() != self.bound_k || config.iter().any(|&v| v > self.alphabet_size) { - return false; - } + fn evaluate(&self, config: &[usize]) -> crate::types::Or { + crate::types::Or({ + // 1. Validate config + if config.len() != self.bound_k || config.iter().any(|&v| v > self.alphabet_size) { + return crate::types::Or(false); + } - // 2. Build string: find the actual string length (strip trailing "unused") - let unused = self.alphabet_size; - let str_len = config - .iter() - .rposition(|&v| v != unused) - .map_or(0, |p| p + 1); - - // 3. Check no internal "unused" symbols - let w = &config[..str_len]; - if w.contains(&unused) { - return false; - } + // 2. Build string: find the actual string length (strip trailing "unused") + let unused = self.alphabet_size; + let str_len = config + .iter() + .rposition(|&v| v != unused) + .map_or(0, |p| p + 1); + + // 3. Check no internal "unused" symbols + let w = &config[..str_len]; + if w.contains(&unused) { + return crate::types::Or(false); + } - let mut subset_membership = vec![0usize; self.alphabet_size]; - let mut seen_in_window = vec![0usize; self.alphabet_size]; - let mut subset_stamp = 1usize; - let mut window_stamp = 1usize; + let mut subset_membership = vec![0usize; self.alphabet_size]; + let mut seen_in_window = vec![0usize; self.alphabet_size]; + let mut subset_stamp = 1usize; + let mut window_stamp = 1usize; - // 4. Check each subset has a consecutive block - for subset in &self.subsets { - let subset_len = subset.len(); - if subset_len == 0 { - continue; // empty subset trivially satisfied - } - if subset_len > str_len { - return false; // can't fit - } + // 4. Check each subset has a consecutive block + for subset in &self.subsets { + let subset_len = subset.len(); + if subset_len == 0 { + continue; // empty subset trivially satisfied + } + if subset_len > str_len { + return crate::types::Or(false); // can't fit + } - for &elem in subset { - subset_membership[elem] = subset_stamp; - } + for &elem in subset { + subset_membership[elem] = subset_stamp; + } - let mut found = false; - for start in 0..=(str_len - subset_len) { - let window = &w[start..start + subset_len]; - let current_window_stamp = window_stamp; - window_stamp += 1; - - // Because subsets are validated to contain unique elements, - // a window matches iff every symbol belongs to the subset and - // appears at most once. - if window.iter().all(|&elem| { - let is_member = subset_membership[elem] == subset_stamp; - let is_new = seen_in_window[elem] != current_window_stamp; - if is_member && is_new { - seen_in_window[elem] = current_window_stamp; - true - } else { - false + let mut found = false; + for start in 0..=(str_len - subset_len) { + let window = &w[start..start + subset_len]; + let current_window_stamp = window_stamp; + window_stamp += 1; + + // Because subsets are validated to contain unique elements, + // a window matches iff every symbol belongs to the subset and + // appears at most once. + if window.iter().all(|&elem| { + let is_member = subset_membership[elem] == subset_stamp; + let is_new = seen_in_window[elem] != current_window_stamp; + if is_member && is_new { + seen_in_window[elem] = current_window_stamp; + true + } else { + false + } + }) { + // subset is already sorted + found = true; + break; } - }) { - // subset is already sorted - found = true; - break; } - } - if !found { - return false; - } + if !found { + return crate::types::Or(false); + } - subset_stamp += 1; - } + subset_stamp += 1; + } - true + true + }) } fn variant() -> Vec<(&'static str, &'static str)> { @@ -219,10 +221,8 @@ impl Problem for ConsecutiveSets { } } -impl WitnessProblem for ConsecutiveSets {} - crate::declare_variants! { - default sat ConsecutiveSets => "alphabet_size^bound_k * num_subsets", + default ConsecutiveSets => "alphabet_size^bound_k * num_subsets", } #[cfg(feature = "example-db")] diff --git a/src/models/set/exact_cover_by_3_sets.rs b/src/models/set/exact_cover_by_3_sets.rs index 109644482..62fa875ce 100644 --- a/src/models/set/exact_cover_by_3_sets.rs +++ b/src/models/set/exact_cover_by_3_sets.rs @@ -5,7 +5,7 @@ //! q disjoint triples covering every element exactly once. use crate::registry::{FieldInfo, ProblemSchemaEntry}; -use crate::traits::{Problem, WitnessProblem}; +use crate::traits::Problem; use serde::{Deserialize, Serialize}; use std::collections::HashSet; @@ -124,7 +124,7 @@ impl ExactCoverBy3Sets { /// A valid exact cover selects exactly q = universe_size/3 subsets /// that are pairwise disjoint and whose union equals the universe. pub fn is_valid_solution(&self, config: &[usize]) -> bool { - self.evaluate(config) + self.evaluate(config).0 } /// Get the elements covered by the selected subsets. @@ -143,42 +143,44 @@ impl ExactCoverBy3Sets { impl Problem for ExactCoverBy3Sets { const NAME: &'static str = "ExactCoverBy3Sets"; - type Value = bool; + type Value = crate::types::Or; fn dims(&self) -> Vec { vec![2; self.subsets.len()] } - fn evaluate(&self, config: &[usize]) -> bool { - if config.len() != self.subsets.len() || config.iter().any(|&value| value > 1) { - return false; - } + fn evaluate(&self, config: &[usize]) -> crate::types::Or { + crate::types::Or({ + if config.len() != self.subsets.len() || config.iter().any(|&value| value > 1) { + return crate::types::Or(false); + } - let q = self.universe_size / 3; + let q = self.universe_size / 3; - // Count selected subsets - let selected_count: usize = config.iter().filter(|&&v| v == 1).sum(); - if selected_count != q { - return false; - } + // Count selected subsets + let selected_count: usize = config.iter().filter(|&&v| v == 1).sum(); + if selected_count != q { + return crate::types::Or(false); + } - // Check that selected subsets are pairwise disjoint and cover everything - let mut covered = HashSet::with_capacity(self.universe_size); - for (i, &selected) in config.iter().enumerate() { - if selected == 1 { - if let Some(subset) = self.subsets.get(i) { - for &elem in subset { - if !covered.insert(elem) { - // Element already covered -- not disjoint - return false; + // Check that selected subsets are pairwise disjoint and cover everything + let mut covered = HashSet::with_capacity(self.universe_size); + for (i, &selected) in config.iter().enumerate() { + if selected == 1 { + if let Some(subset) = self.subsets.get(i) { + for &elem in subset { + if !covered.insert(elem) { + // Element already covered -- not disjoint + return crate::types::Or(false); + } } } } } - } - // Check all elements are covered - covered.len() == self.universe_size + // Check all elements are covered + covered.len() == self.universe_size + }) } fn variant() -> Vec<(&'static str, &'static str)> { @@ -186,10 +188,8 @@ impl Problem for ExactCoverBy3Sets { } } -impl WitnessProblem for ExactCoverBy3Sets {} - crate::declare_variants! { - default sat ExactCoverBy3Sets => "2^universe_size", + default ExactCoverBy3Sets => "2^universe_size", } #[cfg(feature = "example-db")] diff --git a/src/models/set/maximum_set_packing.rs b/src/models/set/maximum_set_packing.rs index ee68ebd19..fb199d5a2 100644 --- a/src/models/set/maximum_set_packing.rs +++ b/src/models/set/maximum_set_packing.rs @@ -4,8 +4,8 @@ //! pairwise disjoint sets. use crate::registry::{FieldInfo, ProblemSchemaEntry, VariantDimension}; -use crate::traits::{ObjectiveProblem, Problem}; -use crate::types::{ExtremumSense, Max, One, WeightElement}; +use crate::traits::Problem; +use crate::types::{Max, One, WeightElement}; use num_traits::Zero; use serde::{Deserialize, Serialize}; use std::collections::HashSet; @@ -165,21 +165,10 @@ where } } -impl ObjectiveProblem for MaximumSetPacking -where - W: WeightElement + crate::variant::VariantParam, -{ - type Objective = W::Sum; - - fn direction(&self) -> ExtremumSense { - ExtremumSense::Maximize - } -} - crate::declare_variants! { - default opt MaximumSetPacking => "2^num_sets", - opt MaximumSetPacking => "2^num_sets", - opt MaximumSetPacking => "2^num_sets", + default MaximumSetPacking => "2^num_sets", + MaximumSetPacking => "2^num_sets", + MaximumSetPacking => "2^num_sets", } /// Check if a selection forms a valid set packing (pairwise disjoint). diff --git a/src/models/set/minimum_cardinality_key.rs b/src/models/set/minimum_cardinality_key.rs index 2a5b890cf..43ced5f41 100644 --- a/src/models/set/minimum_cardinality_key.rs +++ b/src/models/set/minimum_cardinality_key.rs @@ -4,7 +4,7 @@ //! determine whether there exists a candidate key of cardinality at most M. use crate::registry::{FieldInfo, ProblemSchemaEntry}; -use crate::traits::{Problem, WitnessProblem}; +use crate::traits::Problem; use serde::{Deserialize, Serialize}; inventory::submit! { @@ -148,25 +148,27 @@ impl MinimumCardinalityKey { impl Problem for MinimumCardinalityKey { const NAME: &'static str = "MinimumCardinalityKey"; - type Value = bool; + type Value = crate::types::Or; fn dims(&self) -> Vec { vec![2; self.num_attributes] } - fn evaluate(&self, config: &[usize]) -> bool { - if config.len() != self.num_attributes || config.iter().any(|&v| v > 1) { - return false; - } + fn evaluate(&self, config: &[usize]) -> crate::types::Or { + crate::types::Or({ + if config.len() != self.num_attributes || config.iter().any(|&v| v > 1) { + return crate::types::Or(false); + } - let selected: Vec = config.iter().map(|&v| v == 1).collect(); - let count = selected.iter().filter(|&&v| v).count(); + let selected: Vec = config.iter().map(|&v| v == 1).collect(); + let count = selected.iter().filter(|&&v| v).count(); - if (count as i64) > self.bound { - return false; - } + if (count as i64) > self.bound { + return crate::types::Or(false); + } - self.is_minimal_key(&selected) + self.is_minimal_key(&selected) + }) } fn variant() -> Vec<(&'static str, &'static str)> { @@ -174,10 +176,8 @@ impl Problem for MinimumCardinalityKey { } } -impl WitnessProblem for MinimumCardinalityKey {} - crate::declare_variants! { - default sat MinimumCardinalityKey => "2^num_attributes", + default MinimumCardinalityKey => "2^num_attributes", } #[cfg(feature = "example-db")] diff --git a/src/models/set/minimum_hitting_set.rs b/src/models/set/minimum_hitting_set.rs index 8d986319a..e7b0d47d2 100644 --- a/src/models/set/minimum_hitting_set.rs +++ b/src/models/set/minimum_hitting_set.rs @@ -4,8 +4,8 @@ //! elements that intersects every set in a collection. use crate::registry::{FieldInfo, ProblemSchemaEntry, ProblemSizeFieldEntry}; -use crate::traits::{ObjectiveProblem, Problem}; -use crate::types::{ExtremumSense, Min}; +use crate::traits::Problem; +use crate::types::Min; use serde::{Deserialize, Serialize}; inventory::submit! { @@ -143,16 +143,8 @@ impl Problem for MinimumHittingSet { } } -impl ObjectiveProblem for MinimumHittingSet { - type Objective = usize; - - fn direction(&self) -> ExtremumSense { - ExtremumSense::Minimize - } -} - crate::declare_variants! { - default opt MinimumHittingSet => "2^universe_size", + default MinimumHittingSet => "2^universe_size", } #[cfg(feature = "example-db")] diff --git a/src/models/set/minimum_set_covering.rs b/src/models/set/minimum_set_covering.rs index c6f30c395..4387375a5 100644 --- a/src/models/set/minimum_set_covering.rs +++ b/src/models/set/minimum_set_covering.rs @@ -4,8 +4,8 @@ //! that covers all elements in the universe. use crate::registry::{FieldInfo, ProblemSchemaEntry, VariantDimension}; -use crate::traits::{ObjectiveProblem, Problem}; -use crate::types::{ExtremumSense, Min, WeightElement}; +use crate::traits::Problem; +use crate::types::{Min, WeightElement}; use num_traits::Zero; use serde::{Deserialize, Serialize}; use std::collections::HashSet; @@ -170,19 +170,8 @@ where } } -impl ObjectiveProblem for MinimumSetCovering -where - W: WeightElement + crate::variant::VariantParam, -{ - type Objective = W::Sum; - - fn direction(&self) -> ExtremumSense { - ExtremumSense::Minimize - } -} - crate::declare_variants! { - default opt MinimumSetCovering => "2^num_sets", + default MinimumSetCovering => "2^num_sets", } /// Check if a selection of sets forms a valid set cover. diff --git a/src/models/set/prime_attribute_name.rs b/src/models/set/prime_attribute_name.rs index b40050a68..96a9741e1 100644 --- a/src/models/set/prime_attribute_name.rs +++ b/src/models/set/prime_attribute_name.rs @@ -4,7 +4,7 @@ //! and a query attribute x, determine if x belongs to any candidate key of . use crate::registry::{FieldInfo, ProblemSchemaEntry}; -use crate::traits::{Problem, WitnessProblem}; +use crate::traits::Problem; use serde::{Deserialize, Serialize}; inventory::submit! { @@ -155,46 +155,48 @@ impl PrimeAttributeName { impl Problem for PrimeAttributeName { const NAME: &'static str = "PrimeAttributeName"; - type Value = bool; + type Value = crate::types::Or; fn dims(&self) -> Vec { vec![2; self.num_attributes] } - fn evaluate(&self, config: &[usize]) -> bool { - // Check config length and binary values - if config.len() != self.num_attributes || config.iter().any(|&v| v > 1) { - return false; - } + fn evaluate(&self, config: &[usize]) -> crate::types::Or { + crate::types::Or({ + // Check config length and binary values + if config.len() != self.num_attributes || config.iter().any(|&v| v > 1) { + return crate::types::Or(false); + } - // K = {i : config[i] = 1} - let k: Vec = config.iter().map(|&v| v == 1).collect(); + // K = {i : config[i] = 1} + let k: Vec = config.iter().map(|&v| v == 1).collect(); - // query_attribute must be in K - if !k[self.query_attribute] { - return false; - } + // query_attribute must be in K + if !k[self.query_attribute] { + return crate::types::Or(false); + } - // Compute closure(K) -- must equal all attributes (K is a superkey) - let closure = self.compute_closure(&k); - if closure.iter().any(|&v| !v) { - return false; - } + // Compute closure(K) -- must equal all attributes (K is a superkey) + let closure = self.compute_closure(&k); + if closure.iter().any(|&v| !v) { + return crate::types::Or(false); + } - // Check minimality: removing any attribute from K must break the superkey property - for i in 0..self.num_attributes { - if k[i] { - let mut reduced = k.clone(); - reduced[i] = false; - let reduced_closure = self.compute_closure(&reduced); - if reduced_closure.iter().all(|&v| v) { - // K \ {i} is still a superkey, so K is not minimal - return false; + // Check minimality: removing any attribute from K must break the superkey property + for i in 0..self.num_attributes { + if k[i] { + let mut reduced = k.clone(); + reduced[i] = false; + let reduced_closure = self.compute_closure(&reduced); + if reduced_closure.iter().all(|&v| v) { + // K \ {i} is still a superkey, so K is not minimal + return crate::types::Or(false); + } } } - } - true + true + }) } fn variant() -> Vec<(&'static str, &'static str)> { @@ -202,10 +204,8 @@ impl Problem for PrimeAttributeName { } } -impl WitnessProblem for PrimeAttributeName {} - crate::declare_variants! { - default sat PrimeAttributeName => "2^num_attributes * num_dependencies * num_attributes", + default PrimeAttributeName => "2^num_attributes * num_dependencies * num_attributes", } #[cfg(feature = "example-db")] diff --git a/src/models/set/rooted_tree_storage_assignment.rs b/src/models/set/rooted_tree_storage_assignment.rs index c13b5ba98..b4138f5af 100644 --- a/src/models/set/rooted_tree_storage_assignment.rs +++ b/src/models/set/rooted_tree_storage_assignment.rs @@ -1,7 +1,7 @@ //! Rooted Tree Storage Assignment problem implementation. use crate::registry::{FieldInfo, ProblemSchemaEntry}; -use crate::traits::{Problem, WitnessProblem}; +use crate::traits::Problem; use serde::{Deserialize, Serialize}; use std::collections::HashSet; @@ -174,39 +174,41 @@ impl RootedTreeStorageAssignment { impl Problem for RootedTreeStorageAssignment { const NAME: &'static str = "RootedTreeStorageAssignment"; - type Value = bool; + type Value = crate::types::Or; fn dims(&self) -> Vec { vec![self.universe_size; self.universe_size] } - fn evaluate(&self, config: &[usize]) -> bool { - if config.len() != self.universe_size { - return false; - } - if config.iter().any(|&parent| parent >= self.universe_size) { - return false; - } - if self.universe_size == 0 { - return self.subsets.is_empty(); - } - - let Some(depth) = Self::analyze_tree(config) else { - return false; - }; + fn evaluate(&self, config: &[usize]) -> crate::types::Or { + crate::types::Or({ + if config.len() != self.universe_size { + return crate::types::Or(false); + } + if config.iter().any(|&parent| parent >= self.universe_size) { + return crate::types::Or(false); + } + if self.universe_size == 0 { + return crate::types::Or(self.subsets.is_empty()); + } - let mut total_cost = 0usize; - for subset in &self.subsets { - let Some(cost) = self.subset_extension_cost(subset, config, &depth) else { - return false; + let Some(depth) = Self::analyze_tree(config) else { + return crate::types::Or(false); }; - total_cost += cost; - if total_cost > self.bound { - return false; + + let mut total_cost = 0usize; + for subset in &self.subsets { + let Some(cost) = self.subset_extension_cost(subset, config, &depth) else { + return crate::types::Or(false); + }; + total_cost += cost; + if total_cost > self.bound { + return crate::types::Or(false); + } } - } - true + true + }) } fn variant() -> Vec<(&'static str, &'static str)> { @@ -214,10 +216,8 @@ impl Problem for RootedTreeStorageAssignment { } } -impl WitnessProblem for RootedTreeStorageAssignment {} - crate::declare_variants! { - default sat RootedTreeStorageAssignment => "universe_size^universe_size", + default RootedTreeStorageAssignment => "universe_size^universe_size", } #[cfg(feature = "example-db")] diff --git a/src/models/set/set_basis.rs b/src/models/set/set_basis.rs index 76f9e2457..e1b9f92bf 100644 --- a/src/models/set/set_basis.rs +++ b/src/models/set/set_basis.rs @@ -5,7 +5,7 @@ //! can be reconstructed as a union of some subcollection of the basis. use crate::registry::{FieldInfo, ProblemSchemaEntry}; -use crate::traits::{Problem, WitnessProblem}; +use crate::traits::Problem; use serde::{Deserialize, Serialize}; inventory::submit! { @@ -96,7 +96,7 @@ impl SetBasis { /// Check whether the configuration is a satisfying Set Basis solution. pub fn is_valid_solution(&self, config: &[usize]) -> bool { - self.evaluate(config) + self.evaluate(config).0 } fn decode_basis(&self, config: &[usize]) -> Option>> { @@ -147,20 +147,22 @@ impl SetBasis { impl Problem for SetBasis { const NAME: &'static str = "SetBasis"; - type Value = bool; + type Value = crate::types::Or; fn dims(&self) -> Vec { vec![2; self.k * self.universe_size] } - fn evaluate(&self, config: &[usize]) -> bool { - let Some(basis) = self.decode_basis(config) else { - return false; - }; + fn evaluate(&self, config: &[usize]) -> crate::types::Or { + crate::types::Or({ + let Some(basis) = self.decode_basis(config) else { + return crate::types::Or(false); + }; - self.collection - .iter() - .all(|target| Self::can_represent_target(&basis, target, self.universe_size)) + self.collection + .iter() + .all(|target| Self::can_represent_target(&basis, target, self.universe_size)) + }) } fn variant() -> Vec<(&'static str, &'static str)> { @@ -168,10 +170,8 @@ impl Problem for SetBasis { } } -impl WitnessProblem for SetBasis {} - crate::declare_variants! { - default sat SetBasis => "2^(basis_size * universe_size)", + default SetBasis => "2^(basis_size * universe_size)", } #[cfg(feature = "example-db")] diff --git a/src/models/set/two_dimensional_consecutive_sets.rs b/src/models/set/two_dimensional_consecutive_sets.rs index 19cacf0f2..de247c110 100644 --- a/src/models/set/two_dimensional_consecutive_sets.rs +++ b/src/models/set/two_dimensional_consecutive_sets.rs @@ -6,7 +6,7 @@ //! are spread across consecutive groups. use crate::registry::{FieldInfo, ProblemSchemaEntry}; -use crate::traits::{Problem, WitnessProblem}; +use crate::traits::Problem; use serde::de::Error as _; use serde::{Deserialize, Serialize}; use std::collections::HashSet; @@ -145,55 +145,57 @@ impl TwoDimensionalConsecutiveSets { impl Problem for TwoDimensionalConsecutiveSets { const NAME: &'static str = "TwoDimensionalConsecutiveSets"; - type Value = bool; + type Value = crate::types::Or; fn dims(&self) -> Vec { vec![self.alphabet_size; self.alphabet_size] } - fn evaluate(&self, config: &[usize]) -> bool { - if config.len() != self.alphabet_size { - return false; - } - if config.iter().any(|&v| v >= self.alphabet_size) { - return false; - } - - // Empty labels do not create gaps in the partition order, so compress used labels first. - let mut used = vec![false; self.alphabet_size]; - for &group in config { - used[group] = true; - } - let mut dense_labels = vec![0; self.alphabet_size]; - let mut next_label = 0; - for (label, is_used) in used.into_iter().enumerate() { - if is_used { - dense_labels[label] = next_label; - next_label += 1; + fn evaluate(&self, config: &[usize]) -> crate::types::Or { + crate::types::Or({ + if config.len() != self.alphabet_size { + return crate::types::Or(false); } - } - - for subset in &self.subsets { - if subset.is_empty() { - continue; + if config.iter().any(|&v| v >= self.alphabet_size) { + return crate::types::Or(false); } - let groups: Vec = subset.iter().map(|&s| dense_labels[config[s]]).collect(); - // Intersection constraint: all group indices must be distinct - let unique: HashSet = groups.iter().copied().collect(); - if unique.len() != subset.len() { - return false; + // Empty labels do not create gaps in the partition order, so compress used labels first. + let mut used = vec![false; self.alphabet_size]; + for &group in config { + used[group] = true; + } + let mut dense_labels = vec![0; self.alphabet_size]; + let mut next_label = 0; + for (label, is_used) in used.into_iter().enumerate() { + if is_used { + dense_labels[label] = next_label; + next_label += 1; + } } - // Consecutiveness: group indices must form a contiguous range - let min_g = *unique.iter().min().unwrap(); - let max_g = *unique.iter().max().unwrap(); - if max_g - min_g + 1 != subset.len() { - return false; + for subset in &self.subsets { + if subset.is_empty() { + continue; + } + let groups: Vec = subset.iter().map(|&s| dense_labels[config[s]]).collect(); + + // Intersection constraint: all group indices must be distinct + let unique: HashSet = groups.iter().copied().collect(); + if unique.len() != subset.len() { + return crate::types::Or(false); + } + + // Consecutiveness: group indices must form a contiguous range + let min_g = *unique.iter().min().unwrap(); + let max_g = *unique.iter().max().unwrap(); + if max_g - min_g + 1 != subset.len() { + return crate::types::Or(false); + } } - } - true + true + }) } fn variant() -> Vec<(&'static str, &'static str)> { @@ -201,10 +203,8 @@ impl Problem for TwoDimensionalConsecutiveSets { } } -impl WitnessProblem for TwoDimensionalConsecutiveSets {} - crate::declare_variants! { - default sat TwoDimensionalConsecutiveSets => "alphabet_size^alphabet_size", + default TwoDimensionalConsecutiveSets => "alphabet_size^alphabet_size", } #[cfg(feature = "example-db")] diff --git a/src/rules/test_helpers.rs b/src/rules/test_helpers.rs index f9aa3e10d..6053ad187 100644 --- a/src/rules/test_helpers.rs +++ b/src/rules/test_helpers.rs @@ -1,6 +1,6 @@ use crate::rules::{ReductionChain, ReductionResult}; use crate::solvers::BruteForce; -use crate::traits::{ObjectiveProblem, Problem, WitnessProblem}; +use crate::traits::Problem; use crate::types::Aggregate; use std::collections::HashSet; @@ -11,7 +11,7 @@ fn verify_optimization_round_trip( target_solution_kind: &str, context: &str, ) where - Source: ObjectiveProblem + 'static, + Source: Problem + 'static, ::Value: Aggregate + std::fmt::Debug + PartialEq, Extract: Fn(&[usize]) -> Vec, { @@ -62,7 +62,7 @@ fn verify_satisfaction_round_trip( target_solution_kind: &str, context: &str, ) where - Source: WitnessProblem + 'static, + Source: Problem + 'static, ::Value: Aggregate + std::fmt::Debug, Extract: Fn(&[usize]) -> Vec, { @@ -95,8 +95,8 @@ pub(crate) fn assert_optimization_round_trip_from_optimization_target( context: &str, ) where R: ReductionResult, - R::Source: ObjectiveProblem + 'static, - R::Target: ObjectiveProblem + 'static, + R::Source: Problem + 'static, + R::Target: Problem + 'static, ::Value: Aggregate + std::fmt::Debug + PartialEq, ::Value: Aggregate, { @@ -116,8 +116,8 @@ pub(crate) fn assert_optimization_round_trip_from_satisfaction_target( context: &str, ) where R: ReductionResult, - R::Source: ObjectiveProblem + 'static, - R::Target: WitnessProblem + 'static, + R::Source: Problem + 'static, + R::Target: Problem + 'static, ::Value: Aggregate + std::fmt::Debug + PartialEq, ::Value: Aggregate, { @@ -136,8 +136,8 @@ pub(crate) fn assert_optimization_round_trip_chain( chain: &ReductionChain, context: &str, ) where - Source: ObjectiveProblem + 'static, - Target: ObjectiveProblem + 'static, + Source: Problem + 'static, + Target: Problem + 'static, ::Value: Aggregate + std::fmt::Debug + PartialEq, ::Value: Aggregate, { @@ -157,8 +157,8 @@ pub(crate) fn assert_satisfaction_round_trip_from_optimization_target( context: &str, ) where R: ReductionResult, - R::Source: WitnessProblem + 'static, - R::Target: ObjectiveProblem + 'static, + R::Source: Problem + 'static, + R::Target: Problem + 'static, ::Value: Aggregate + std::fmt::Debug, ::Value: Aggregate, { @@ -178,8 +178,8 @@ pub(crate) fn assert_satisfaction_round_trip_from_satisfaction_target( context: &str, ) where R: ReductionResult, - R::Source: WitnessProblem + 'static, - R::Target: WitnessProblem + 'static, + R::Source: Problem + 'static, + R::Target: Problem + 'static, ::Value: Aggregate + std::fmt::Debug, ::Value: Aggregate, { @@ -195,7 +195,7 @@ pub(crate) fn assert_satisfaction_round_trip_from_satisfaction_target( pub(crate) fn solve_optimization_problem

(problem: &P) -> Option> where - P: ObjectiveProblem + 'static, + P: Problem + 'static, P::Value: Aggregate, { BruteForce::new().find_witness(problem) @@ -203,7 +203,7 @@ where pub(crate) fn solve_satisfaction_problem

(problem: &P) -> Option> where - P: WitnessProblem + 'static, + P: Problem + 'static, P::Value: Aggregate, { BruteForce::new().find_witness(problem) @@ -218,14 +218,14 @@ mod tests { assert_satisfaction_round_trip_from_satisfaction_target, }; use crate::rules::ReductionResult; - use crate::traits::{ObjectiveProblem, Problem, WitnessProblem}; - use crate::types::{ExtremumSense, Max}; + use crate::traits::Problem; + use crate::types::{Max, Or}; #[derive(Clone)] - struct ToyObjectiveProblem; + struct ToyExtremumProblem; - impl Problem for ToyObjectiveProblem { - const NAME: &'static str = "ToyObjectiveProblem"; + impl Problem for ToyExtremumProblem { + const NAME: &'static str = "ToyExtremumProblem"; type Value = Max; fn dims(&self) -> Vec { @@ -244,27 +244,19 @@ mod tests { } } - impl ObjectiveProblem for ToyObjectiveProblem { - type Objective = i32; - - fn direction(&self) -> ExtremumSense { - ExtremumSense::Maximize - } - } - #[derive(Clone)] - struct ToyWitnessProblem; + struct ToyOrProblem; - impl Problem for ToyWitnessProblem { - const NAME: &'static str = "ToyWitnessProblem"; - type Value = bool; + impl Problem for ToyOrProblem { + const NAME: &'static str = "ToyOrProblem"; + type Value = Or; fn dims(&self) -> Vec { vec![2, 2] } fn evaluate(&self, config: &[usize]) -> Self::Value { - matches!(config, [1, 0] | [0, 1]) + Or(matches!(config, [1, 0] | [0, 1])) } fn variant() -> Vec<(&'static str, &'static str)> { @@ -272,15 +264,13 @@ mod tests { } } - impl WitnessProblem for ToyWitnessProblem {} - struct OptToOptReduction { - target: ToyObjectiveProblem, + target: ToyExtremumProblem, } impl ReductionResult for OptToOptReduction { - type Source = ToyObjectiveProblem; - type Target = ToyObjectiveProblem; + type Source = ToyExtremumProblem; + type Target = ToyExtremumProblem; fn target_problem(&self) -> &Self::Target { &self.target @@ -292,12 +282,12 @@ mod tests { } struct OptToSatReduction { - target: ToyWitnessProblem, + target: ToyOrProblem, } impl ReductionResult for OptToSatReduction { - type Source = ToyObjectiveProblem; - type Target = ToyWitnessProblem; + type Source = ToyExtremumProblem; + type Target = ToyOrProblem; fn target_problem(&self) -> &Self::Target { &self.target @@ -309,12 +299,12 @@ mod tests { } struct SatToOptReduction { - target: ToyObjectiveProblem, + target: ToyExtremumProblem, } impl ReductionResult for SatToOptReduction { - type Source = ToyWitnessProblem; - type Target = ToyObjectiveProblem; + type Source = ToyOrProblem; + type Target = ToyExtremumProblem; fn target_problem(&self) -> &Self::Target { &self.target @@ -326,12 +316,12 @@ mod tests { } struct SatToSatReduction { - target: ToyWitnessProblem, + target: ToyOrProblem, } impl ReductionResult for SatToSatReduction { - type Source = ToyWitnessProblem; - type Target = ToyWitnessProblem; + type Source = ToyOrProblem; + type Target = ToyOrProblem; fn target_problem(&self) -> &Self::Target { &self.target @@ -344,41 +334,41 @@ mod tests { #[test] fn test_optimization_round_trip_wrappers_accept_identity_reductions() { - let source = ToyObjectiveProblem; + let source = ToyExtremumProblem; assert_optimization_round_trip_from_optimization_target( &source, &OptToOptReduction { - target: ToyObjectiveProblem, + target: ToyExtremumProblem, }, - "opt->opt", + "extremum->extremum", ); assert_optimization_round_trip_from_satisfaction_target( &source, &OptToSatReduction { - target: ToyWitnessProblem, + target: ToyOrProblem, }, - "opt->sat", + "extremum->witness", ); } #[test] fn test_satisfaction_round_trip_wrappers_accept_identity_reductions() { - let source = ToyWitnessProblem; + let source = ToyOrProblem; assert_satisfaction_round_trip_from_optimization_target( &source, &SatToOptReduction { - target: ToyObjectiveProblem, + target: ToyExtremumProblem, }, - "sat->opt", + "witness->extremum", ); assert_satisfaction_round_trip_from_satisfaction_target( &source, &SatToSatReduction { - target: ToyWitnessProblem, + target: ToyOrProblem, }, - "sat->sat", + "witness->witness", ); } } diff --git a/src/solvers/brute_force.rs b/src/solvers/brute_force.rs index 73b33e742..caf8ca817 100644 --- a/src/solvers/brute_force.rs +++ b/src/solvers/brute_force.rs @@ -2,7 +2,7 @@ use crate::config::DimsIterator; use crate::solvers::Solver; -use crate::traits::{ObjectiveProblem, Problem, WitnessProblem}; +use crate::traits::Problem; use crate::types::Aggregate; /// A brute force solver that enumerates all possible configurations. @@ -19,42 +19,6 @@ impl BruteForce { Self } - /// Temporary compatibility helper for optimization problems. - pub fn find_all_best

(&self, problem: &P) -> Vec> - where - P: ObjectiveProblem, - P::Value: Aggregate, - { - self.find_all_witnesses(problem) - } - - /// Temporary compatibility helper for optimization problems. - pub fn find_best

(&self, problem: &P) -> Option> - where - P: ObjectiveProblem, - P::Value: Aggregate, - { - self.find_witness(problem) - } - - /// Temporary compatibility helper for satisfaction problems. - pub fn find_all_satisfying

(&self, problem: &P) -> Vec> - where - P: WitnessProblem, - P::Value: Aggregate, - { - self.find_all_witnesses(problem) - } - - /// Temporary compatibility helper for satisfaction problems. - pub fn find_satisfying

(&self, problem: &P) -> Option> - where - P: WitnessProblem, - P::Value: Aggregate, - { - self.find_witness(problem) - } - /// Find one witness configuration when the aggregate value admits them. pub fn find_witness

(&self, problem: &P) -> Option> where @@ -117,22 +81,6 @@ impl Solver for BruteForce { .map(|config| problem.evaluate(&config)) .fold(P::Value::identity(), P::Value::combine) } - - fn find_best

(&self, problem: &P) -> Option> - where - P: ObjectiveProblem, - P::Value: Aggregate, - { - BruteForce::find_witness(self, problem) - } - - fn find_satisfying

(&self, problem: &P) -> Option> - where - P: WitnessProblem, - P::Value: Aggregate, - { - BruteForce::find_witness(self, problem) - } } #[cfg(test)] diff --git a/src/solvers/mod.rs b/src/solvers/mod.rs index 326872aad..074841c31 100644 --- a/src/solvers/mod.rs +++ b/src/solvers/mod.rs @@ -10,7 +10,7 @@ pub use brute_force::BruteForce; #[cfg(feature = "ilp-solver")] pub use ilp::ILPSolver; -use crate::traits::{ObjectiveProblem, Problem, WitnessProblem}; +use crate::traits::Problem; /// Trait for problem solvers. pub trait Solver { @@ -19,16 +19,4 @@ pub trait Solver { where P: Problem, P::Value: crate::types::Aggregate; - - /// Temporary compatibility helper for optimization problems. - fn find_best

(&self, problem: &P) -> Option> - where - P: ObjectiveProblem, - P::Value: crate::types::Aggregate; - - /// Temporary compatibility helper for satisfaction problems. - fn find_satisfying

(&self, problem: &P) -> Option> - where - P: WitnessProblem, - P::Value: crate::types::Aggregate; } diff --git a/src/traits.rs b/src/traits.rs index 57bf1c5fe..5792e2407 100644 --- a/src/traits.rs +++ b/src/traits.rs @@ -33,17 +33,6 @@ pub trait Problem: Clone { } } -/// Temporary compatibility trait for optimization problems during the aggregate migration. -pub trait ObjectiveProblem: Problem { - /// The inner objective value type (e.g., `i32`, `f64`). - type Objective: PartialOrd + Clone; - /// Whether to maximize or minimize the metric. - fn direction(&self) -> crate::types::ExtremumSense; -} - -/// Temporary compatibility trait for satisfaction problems during the aggregate migration. -pub trait WitnessProblem: Problem {} - /// Marker trait for explicitly declared problem variants. /// /// Implemented automatically by [`declare_variants!`] for each concrete type. diff --git a/src/types.rs b/src/types.rs index c5e249c64..b9236aec7 100644 --- a/src/types.rs +++ b/src/types.rs @@ -324,6 +324,16 @@ impl fmt::Display for Sum { #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)] pub struct Or(pub bool); +impl Or { + pub fn is_valid(&self) -> bool { + self.0 + } + + pub fn unwrap(self) -> bool { + self.0 + } +} + impl Aggregate for Or { fn identity() -> Self { Or(false) @@ -342,27 +352,29 @@ impl Aggregate for Or { } } -impl Aggregate for bool { - fn identity() -> Self { - false +impl fmt::Display for Or { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "Or({})", self.0) } +} - fn combine(self, other: Self) -> Self { - self || other - } +impl std::ops::Not for Or { + type Output = bool; - fn supports_witnesses() -> bool { - true + fn not(self) -> Self::Output { + !self.0 } +} - fn contributes_to_witnesses(config_value: &Self, total: &Self) -> bool { - *config_value && *total +impl PartialEq for Or { + fn eq(&self, other: &bool) -> bool { + self.0 == *other } } -impl fmt::Display for Or { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "Or({})", self.0) +impl PartialEq for bool { + fn eq(&self, other: &Or) -> bool { + *self == other.0 } } diff --git a/src/unit_tests/graph_models.rs b/src/unit_tests/graph_models.rs index 07b9a266f..95979843f 100644 --- a/src/unit_tests/graph_models.rs +++ b/src/unit_tests/graph_models.rs @@ -9,8 +9,8 @@ use crate::models::graph::minimum_vertex_cover::is_vertex_cover; use crate::models::graph::{KColoring, MaximumIndependentSet, MinimumVertexCover}; use crate::prelude::*; use crate::topology::{Graph, SimpleGraph}; -use crate::traits::{ObjectiveProblem, Problem}; -use crate::types::{ExtremumSense, Max, Min}; +use crate::traits::Problem; +use crate::types::{Max, Min}; use crate::variant::{K1, K2, K3, K4}; // ============================================================================= @@ -174,8 +174,7 @@ mod maximum_independent_set { #[test] fn test_direction() { - let problem = MaximumIndependentSet::new(SimpleGraph::new(3, vec![(0, 1)]), vec![1i32; 3]); - assert_eq!(problem.direction(), ExtremumSense::Maximize); + let _problem = MaximumIndependentSet::new(SimpleGraph::new(3, vec![(0, 1)]), vec![1i32; 3]); } #[test] @@ -335,8 +334,7 @@ mod minimum_vertex_cover { #[test] fn test_direction() { - let problem = MinimumVertexCover::new(SimpleGraph::new(3, vec![(0, 1)]), vec![1i32; 3]); - assert_eq!(problem.direction(), ExtremumSense::Minimize); + let _problem = MinimumVertexCover::new(SimpleGraph::new(3, vec![(0, 1)]), vec![1i32; 3]); } #[test] diff --git a/src/unit_tests/models/algebraic/bmf.rs b/src/unit_tests/models/algebraic/bmf.rs index 8b7dc13d0..edcd60a43 100644 --- a/src/unit_tests/models/algebraic/bmf.rs +++ b/src/unit_tests/models/algebraic/bmf.rs @@ -1,7 +1,7 @@ use super::*; use crate::solvers::BruteForce; -use crate::traits::{ObjectiveProblem, Problem}; -use crate::types::{ExtremumSense, Min}; +use crate::traits::Problem; +use crate::types::Min; include!("../../jl_helpers.rs"); @@ -147,13 +147,6 @@ fn test_matrix_hamming_distance_function() { assert_eq!(matrix_hamming_distance(&a, &c), 0); } -#[test] -fn test_direction() { - let matrix = vec![vec![true]]; - let problem = BMF::new(matrix, 1); - assert_eq!(problem.direction(), ExtremumSense::Minimize); -} - #[test] fn test_empty_matrix() { let matrix: Vec> = vec![]; @@ -173,8 +166,7 @@ fn test_is_exact() { #[test] fn test_bmf_problem() { - use crate::traits::{ObjectiveProblem, Problem}; - use crate::types::ExtremumSense; + use crate::traits::Problem; // 2x2 identity matrix with rank 2 let matrix = vec![vec![true, false], vec![false, true]]; @@ -197,7 +189,6 @@ fn test_bmf_problem() { ); // ExtremumSense is minimize - assert_eq!(problem.direction(), ExtremumSense::Minimize); // Test with 1x1 matrix let matrix = vec![vec![true]]; diff --git a/src/unit_tests/models/algebraic/closest_vector_problem.rs b/src/unit_tests/models/algebraic/closest_vector_problem.rs index 5025ec06d..ff5e41dc4 100644 --- a/src/unit_tests/models/algebraic/closest_vector_problem.rs +++ b/src/unit_tests/models/algebraic/closest_vector_problem.rs @@ -1,7 +1,7 @@ use super::*; use crate::solvers::BruteForce; -use crate::traits::{ObjectiveProblem, Problem}; -use crate::types::{ExtremumSense, Min}; +use crate::traits::Problem; +use crate::types::Min; #[test] fn test_cvp_creation() { @@ -38,15 +38,6 @@ fn test_cvp_evaluate() { assert_eq!(result, Min(Some(1.0))); } -#[test] -fn test_cvp_direction() { - let basis = vec![vec![1, 0], vec![0, 1]]; - let target = vec![0.5, 0.5]; - let bounds = vec![VarBounds::bounded(0, 2), VarBounds::bounded(0, 2)]; - let cvp = ClosestVectorProblem::new(basis, target, bounds); - assert_eq!(cvp.direction(), ExtremumSense::Minimize); -} - #[test] fn test_cvp_dims() { let basis = vec![vec![1, 0], vec![0, 1]]; 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 0f70c1038..16fc52b93 100644 --- a/src/unit_tests/models/algebraic/consecutive_ones_matrix_augmentation.rs +++ b/src/unit_tests/models/algebraic/consecutive_ones_matrix_augmentation.rs @@ -51,7 +51,7 @@ fn test_consecutive_ones_matrix_augmentation_no_instance() { let problem = ConsecutiveOnesMatrixAugmentation::new(issue_no_matrix(), 0); let solver = BruteForce::new(); - assert!(solver.find_satisfying(&problem).is_none()); + assert!(solver.find_witness(&problem).is_none()); } #[test] diff --git a/src/unit_tests/models/algebraic/ilp.rs b/src/unit_tests/models/algebraic/ilp.rs index 2c6efec52..d993f0716 100644 --- a/src/unit_tests/models/algebraic/ilp.rs +++ b/src/unit_tests/models/algebraic/ilp.rs @@ -1,7 +1,7 @@ use super::*; use crate::solvers::BruteForce; -use crate::traits::{ObjectiveProblem, Problem}; -use crate::types::{Extremum, ExtremumSense}; +use crate::traits::Problem; +use crate::types::Extremum; // ============================================================ // Comparison tests @@ -97,17 +97,6 @@ fn test_linear_constraint_out_of_bounds() { // ObjectiveSense tests // ============================================================ -#[test] -fn test_objective_sense_direction_conversions() { - // Test that ObjectiveSense and ExtremumSense can be converted - let max_sense = ObjectiveSense::Maximize; - let min_sense = ObjectiveSense::Minimize; - - // ExtremumSense values match ObjectiveSense semantics - assert_eq!(max_sense, ObjectiveSense::Maximize); - assert_eq!(min_sense, ObjectiveSense::Minimize); -} - // ============================================================ // ILP tests // ============================================================ @@ -189,15 +178,6 @@ fn test_ilp_num_variables() { assert_eq!(ilp.num_variables(), 5); } -#[test] -fn test_ilp_direction() { - let max_ilp = ILP::::new(2, vec![], vec![], ObjectiveSense::Maximize); - let min_ilp = ILP::::new(2, vec![], vec![], ObjectiveSense::Minimize); - - assert_eq!(max_ilp.direction(), ExtremumSense::Maximize); - assert_eq!(min_ilp.direction(), ExtremumSense::Minimize); -} - #[test] fn test_ilp_evaluate_valid() { // Maximize x0 + 2*x1 subject to x0 + x1 <= 1 @@ -401,8 +381,6 @@ fn test_ilp_problem() { ); // [1, 1] -> infeasible assert_eq!(Problem::evaluate(&ilp, &[1, 1]), Extremum::maximize(None)); - - assert_eq!(ilp.direction(), ExtremumSense::Maximize); } #[test] @@ -422,7 +400,6 @@ fn test_ilp_problem_minimize() { Problem::evaluate(&ilp, &[1, 1]), Extremum::minimize(Some(2.0)) ); - assert_eq!(ilp.direction(), ExtremumSense::Minimize); } #[test] diff --git a/src/unit_tests/models/algebraic/quadratic_assignment.rs b/src/unit_tests/models/algebraic/quadratic_assignment.rs index 58c7bd596..283c49e63 100644 --- a/src/unit_tests/models/algebraic/quadratic_assignment.rs +++ b/src/unit_tests/models/algebraic/quadratic_assignment.rs @@ -1,7 +1,7 @@ use super::*; use crate::solvers::BruteForce; -use crate::traits::{ObjectiveProblem, Problem}; -use crate::types::{ExtremumSense, Min}; +use crate::traits::Problem; +use crate::types::Min; /// Create a 4x4 test instance matching issue #300's example. /// @@ -80,12 +80,6 @@ fn test_quadratic_assignment_evaluate_invalid() { assert_eq!(Problem::evaluate(&qap, &[0, 1, 2, 3, 0]), Min(None)); } -#[test] -fn test_quadratic_assignment_direction() { - let qap = make_test_instance(); - assert_eq!(qap.direction(), ExtremumSense::Minimize); -} - #[test] fn test_quadratic_assignment_serialization() { let qap = make_test_instance(); diff --git a/src/unit_tests/models/algebraic/qubo.rs b/src/unit_tests/models/algebraic/qubo.rs index c73febbf8..83ebae164 100644 --- a/src/unit_tests/models/algebraic/qubo.rs +++ b/src/unit_tests/models/algebraic/qubo.rs @@ -1,7 +1,7 @@ use super::*; use crate::solvers::BruteForce; -use crate::traits::{ObjectiveProblem, Problem}; -use crate::types::{ExtremumSense, Min}; +use crate::traits::Problem; +use crate::types::Min; include!("../../jl_helpers.rs"); #[test] @@ -21,12 +21,6 @@ fn test_qubo_new() { assert_eq!(problem.get(0, 1), Some(&3.0)); } -#[test] -fn test_direction() { - let problem = QUBO::::from_matrix(vec![vec![1.0]]); - assert_eq!(problem.direction(), ExtremumSense::Minimize); -} - #[test] fn test_num_variables() { let problem = QUBO::::from_matrix(vec![vec![0.0; 5]; 5]); diff --git a/src/unit_tests/models/graph/biclique_cover.rs b/src/unit_tests/models/graph/biclique_cover.rs index a088a0bb8..b4560be6e 100644 --- a/src/unit_tests/models/graph/biclique_cover.rs +++ b/src/unit_tests/models/graph/biclique_cover.rs @@ -1,8 +1,8 @@ use super::*; use crate::solvers::BruteForce; use crate::topology::BipartiteGraph; -use crate::traits::{ObjectiveProblem, Problem}; -use crate::types::{ExtremumSense, Min}; +use crate::traits::Problem; +use crate::types::Min; include!("../../jl_helpers.rs"); @@ -135,13 +135,6 @@ fn test_is_biclique_cover_function() { assert!(!is_biclique_cover(&edges, &left, &right)); } -#[test] -fn test_direction() { - let graph = BipartiteGraph::new(1, 1, vec![(0, 0)]); - let problem = BicliqueCover::new(graph, 1); - assert_eq!(problem.direction(), ExtremumSense::Minimize); -} - #[test] fn test_empty_edges() { let graph = BipartiteGraph::new(2, 2, vec![]); @@ -152,8 +145,7 @@ fn test_empty_edges() { #[test] fn test_biclique_problem() { - use crate::traits::{ObjectiveProblem, Problem}; - use crate::types::ExtremumSense; + use crate::traits::Problem; // Single edge (0,0) in local coords with k=1, 2 left + 2 right vertices let graph = BipartiteGraph::new(2, 2, vec![(0, 0)]); @@ -176,7 +168,6 @@ fn test_biclique_problem() { assert_eq!(problem.evaluate(&[0, 0, 0, 0]), Min(None)); // ExtremumSense is minimize - assert_eq!(problem.direction(), ExtremumSense::Minimize); // Test with no edges: any config is valid let empty_graph = BipartiteGraph::new(2, 2, vec![]); diff --git a/src/unit_tests/models/graph/bottleneck_traveling_salesman.rs b/src/unit_tests/models/graph/bottleneck_traveling_salesman.rs index d12116566..e8f72c0df 100644 --- a/src/unit_tests/models/graph/bottleneck_traveling_salesman.rs +++ b/src/unit_tests/models/graph/bottleneck_traveling_salesman.rs @@ -1,8 +1,8 @@ use super::*; use crate::solvers::BruteForce; use crate::topology::SimpleGraph; -use crate::traits::{ObjectiveProblem, Problem}; -use crate::types::{ExtremumSense, Min}; +use crate::traits::Problem; +use crate::types::Min; fn k5_btsp() -> BottleneckTravelingSalesman { BottleneckTravelingSalesman::new( @@ -82,16 +82,6 @@ fn test_bottleneck_traveling_salesman_evaluate_disconnected_subtour_invalid() { assert_eq!(problem.evaluate(&disconnected_subtour), Min(None)); } -#[test] -fn test_bottleneck_traveling_salesman_direction() { - let problem = BottleneckTravelingSalesman::new( - SimpleGraph::new(3, vec![(0, 1), (1, 2), (0, 2)]), - vec![7, 4, 6], - ); - - assert_eq!(problem.direction(), ExtremumSense::Minimize); -} - #[test] fn test_bottleneck_traveling_salesman_bruteforce_unique_optimum() { let problem = k5_btsp(); diff --git a/src/unit_tests/models/graph/graph_partitioning.rs b/src/unit_tests/models/graph/graph_partitioning.rs index d2faca863..fe844ed47 100644 --- a/src/unit_tests/models/graph/graph_partitioning.rs +++ b/src/unit_tests/models/graph/graph_partitioning.rs @@ -1,8 +1,7 @@ use super::*; use crate::solvers::BruteForce; use crate::topology::SimpleGraph; -use crate::traits::{ObjectiveProblem, Problem}; -use crate::types::ExtremumSense; +use crate::traits::Problem; /// Issue example: 6 vertices, edges forming two triangles connected by 3 edges. /// Optimal partition A={0,1,2}, B={3,4,5}, cut=3. @@ -39,12 +38,6 @@ fn test_graphpartitioning_basic() { assert_eq!(result, Min(Some(3))); } -#[test] -fn test_graphpartitioning_direction() { - let problem = issue_example(); - assert_eq!(problem.direction(), ExtremumSense::Minimize); -} - #[test] fn test_graphpartitioning_serialization() { let problem = issue_example(); 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 ae200f3eb..2ce6a5e7d 100644 --- a/src/unit_tests/models/graph/integral_flow_homologous_arcs.rs +++ b/src/unit_tests/models/graph/integral_flow_homologous_arcs.rs @@ -140,5 +140,7 @@ fn test_integral_flow_homologous_arcs_paper_example() { let solutions = solver.find_all_witnesses(&problem); assert!(!solutions.is_empty()); - assert!(solutions.iter().all(|solution| problem.evaluate(solution))); + assert!(solutions + .iter() + .all(|solution| problem.evaluate(solution).0)); } diff --git a/src/unit_tests/models/graph/kcoloring.rs b/src/unit_tests/models/graph/kcoloring.rs index f7747820f..2185b0337 100644 --- a/src/unit_tests/models/graph/kcoloring.rs +++ b/src/unit_tests/models/graph/kcoloring.rs @@ -163,7 +163,7 @@ fn test_jl_parity_evaluation() { let problem = KColoring::::new(SimpleGraph::new(nv, edges)); for eval in instance["evaluations"].as_array().unwrap() { let config = jl_parse_config(&eval["config"]); - let result: bool = problem.evaluate(&config); + let result = problem.evaluate(&config).0; let jl_size = eval["size"].as_i64().unwrap() as usize; assert_eq!( result, 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 23481792b..1c3464260 100644 --- a/src/unit_tests/models/graph/kth_best_spanning_tree.rs +++ b/src/unit_tests/models/graph/kth_best_spanning_tree.rs @@ -93,7 +93,7 @@ fn test_kthbestspanningtree_solver_exhaustive() { // Exactly 2 spanning trees have weight ≤ 4, so exactly 2! = 2 satisfying configs. let all = solver.find_all_witnesses(&problem); assert_eq!(all.len(), 2); - assert!(all.iter().all(|config| problem.evaluate(config))); + assert!(all.iter().all(|config| problem.evaluate(config).0)); } #[test] @@ -112,7 +112,7 @@ fn test_kthbestspanningtree_small_exhaustive_search() { let all = solver.find_all_witnesses(&problem); assert_eq!(all.len(), 6); - assert!(all.iter().all(|config| problem.evaluate(config))); + assert!(all.iter().all(|config| problem.evaluate(config).0)); } #[test] 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 a3e93c9da..a83afd1bf 100644 --- a/src/unit_tests/models/graph/length_bounded_disjoint_paths.rs +++ b/src/unit_tests/models/graph/length_bounded_disjoint_paths.rs @@ -189,5 +189,5 @@ fn test_length_bounded_disjoint_paths_paper_example() { assert_eq!(satisfying.len(), 6); assert!(satisfying .iter() - .all(|candidate| problem.evaluate(candidate))); + .all(|candidate| problem.evaluate(candidate).0)); } diff --git a/src/unit_tests/models/graph/longest_path.rs b/src/unit_tests/models/graph/longest_path.rs index a8b086f9d..9b61616fe 100644 --- a/src/unit_tests/models/graph/longest_path.rs +++ b/src/unit_tests/models/graph/longest_path.rs @@ -1,8 +1,8 @@ use super::*; use crate::solvers::BruteForce; use crate::topology::SimpleGraph; -use crate::traits::{ObjectiveProblem, Problem}; -use crate::types::{ExtremumSense, Max, One}; +use crate::traits::Problem; +use crate::types::{Max, One}; fn issue_problem() -> LongestPath { LongestPath::new( @@ -48,7 +48,6 @@ fn test_longest_path_creation() { assert_eq!(problem.dims(), vec![2; 10]); assert_eq!(problem.edge_lengths(), &[3, 2, 4, 1, 5, 2, 3, 2, 4, 1]); assert!(problem.is_weighted()); - assert_eq!(problem.direction(), ExtremumSense::Maximize); problem.set_lengths(vec![1; 10]); assert_eq!(problem.edge_lengths(), &[1; 10]); diff --git a/src/unit_tests/models/graph/max_cut.rs b/src/unit_tests/models/graph/max_cut.rs index 957394c62..dcfadc6e6 100644 --- a/src/unit_tests/models/graph/max_cut.rs +++ b/src/unit_tests/models/graph/max_cut.rs @@ -53,15 +53,6 @@ fn test_edges() { assert_eq!(edges.len(), 2); } -#[test] -fn test_direction() { - use crate::traits::ObjectiveProblem; - use crate::types::ExtremumSense; - - let problem = MaxCut::<_, i32>::unweighted(SimpleGraph::new(2, vec![(0, 1)])); - assert_eq!(problem.direction(), ExtremumSense::Maximize); -} - #[test] fn test_new() { let problem = MaxCut::new(SimpleGraph::new(3, vec![(0, 1), (1, 2)]), vec![5, 10]); diff --git a/src/unit_tests/models/graph/maximal_is.rs b/src/unit_tests/models/graph/maximal_is.rs index 77566f897..06f2b3566 100644 --- a/src/unit_tests/models/graph/maximal_is.rs +++ b/src/unit_tests/models/graph/maximal_is.rs @@ -66,15 +66,6 @@ fn test_is_maximal_independent_set_function() { assert!(!is_maximal_independent_set(&graph, &[true, true, false])); // Not independent } -#[test] -fn test_direction() { - use crate::traits::ObjectiveProblem; - use crate::types::ExtremumSense; - - let problem = MaximalIS::new(SimpleGraph::new(2, vec![(0, 1)]), vec![1i32; 2]); - assert_eq!(problem.direction(), ExtremumSense::Maximize); -} - #[test] fn test_weights() { let problem = MaximalIS::new(SimpleGraph::new(3, vec![(0, 1)]), vec![1i32; 3]); diff --git a/src/unit_tests/models/graph/maximum_clique.rs b/src/unit_tests/models/graph/maximum_clique.rs index 196899d91..625d33b10 100644 --- a/src/unit_tests/models/graph/maximum_clique.rs +++ b/src/unit_tests/models/graph/maximum_clique.rs @@ -166,15 +166,6 @@ fn test_is_clique_function() { )); // Adjacent pair } -#[test] -fn test_direction() { - use crate::traits::ObjectiveProblem; - use crate::types::ExtremumSense; - - let problem = MaximumClique::new(SimpleGraph::new(3, vec![(0, 1)]), vec![1i32; 3]); - assert_eq!(problem.direction(), ExtremumSense::Maximize); -} - #[test] fn test_edges() { let problem = MaximumClique::new(SimpleGraph::new(4, vec![(0, 1), (2, 3)]), vec![1i32; 4]); @@ -254,8 +245,7 @@ fn test_complete_graph() { #[test] fn test_clique_problem() { - use crate::traits::{ObjectiveProblem, Problem}; - use crate::types::ExtremumSense; + use crate::traits::Problem; // Triangle graph: all pairs connected let p = MaximumClique::new( @@ -267,7 +257,6 @@ fn test_clique_problem() { assert_eq!(p.evaluate(&[1, 1, 1]), Max(Some(3))); // Valid clique: select just vertex 0 assert_eq!(p.evaluate(&[1, 0, 0]), Max(Some(1))); - assert_eq!(p.direction(), ExtremumSense::Maximize); } #[test] diff --git a/src/unit_tests/models/graph/maximum_independent_set.rs b/src/unit_tests/models/graph/maximum_independent_set.rs index 82ece958e..4362cfc39 100644 --- a/src/unit_tests/models/graph/maximum_independent_set.rs +++ b/src/unit_tests/models/graph/maximum_independent_set.rs @@ -1,8 +1,7 @@ use super::*; use crate::solvers::BruteForce; use crate::topology::SimpleGraph; -use crate::traits::{ObjectiveProblem, Problem}; -use crate::types::ExtremumSense; +use crate::traits::Problem; include!("../../jl_helpers.rs"); #[test] @@ -64,12 +63,6 @@ fn test_is_independent_set_function() { )); } -#[test] -fn test_direction() { - let problem = MaximumIndependentSet::new(SimpleGraph::new(3, vec![(0, 1)]), vec![1i32; 3]); - assert_eq!(problem.direction(), ExtremumSense::Maximize); -} - #[test] fn test_edges() { let problem = diff --git a/src/unit_tests/models/graph/maximum_matching.rs b/src/unit_tests/models/graph/maximum_matching.rs index 9c358150e..17c170e8c 100644 --- a/src/unit_tests/models/graph/maximum_matching.rs +++ b/src/unit_tests/models/graph/maximum_matching.rs @@ -1,8 +1,8 @@ use super::*; use crate::solvers::BruteForce; use crate::topology::SimpleGraph; -use crate::traits::{ObjectiveProblem, Problem}; -use crate::types::{ExtremumSense, Max}; +use crate::traits::Problem; +use crate::types::Max; include!("../../jl_helpers.rs"); #[test] @@ -58,12 +58,6 @@ fn test_is_matching_function() { assert!(is_matching(&graph, &[false, false, false])); // Empty is valid } -#[test] -fn test_direction() { - let problem = MaximumMatching::<_, i32>::unit_weights(SimpleGraph::new(2, vec![(0, 1)])); - assert_eq!(problem.direction(), ExtremumSense::Maximize); -} - #[test] fn test_empty_graph() { let problem = MaximumMatching::<_, i32>::unit_weights(SimpleGraph::new(3, vec![])); 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 20577117b..a538cde62 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,7 +1,8 @@ use super::*; use crate::solvers::BruteForce; use crate::topology::SimpleGraph; -use crate::traits::{Problem, WitnessProblem}; +use crate::traits::Problem; +use crate::types::Aggregate; /// Build the example instance from issue #228: /// 8 vertices, 12 edges, s=0, t=7, B=5 @@ -209,9 +210,7 @@ fn test_minimumcutintoboundedsets_solver_no_solution_issue_instance() { ); } -// Verify WitnessProblem marker trait is implemented #[test] -fn test_minimumcutintoboundedsets_is_satisfaction_problem() { - fn assert_satisfaction() {} - assert_satisfaction::>(); +fn test_minimumcutintoboundedsets_supports_witnesses() { + assert!( as Problem>::Value::supports_witnesses()); } diff --git a/src/unit_tests/models/graph/minimum_dominating_set.rs b/src/unit_tests/models/graph/minimum_dominating_set.rs index 562265717..a1665a701 100644 --- a/src/unit_tests/models/graph/minimum_dominating_set.rs +++ b/src/unit_tests/models/graph/minimum_dominating_set.rs @@ -1,8 +1,7 @@ use super::*; use crate::solvers::BruteForce; use crate::topology::SimpleGraph; -use crate::traits::{ObjectiveProblem, Problem}; -use crate::types::ExtremumSense; +use crate::traits::Problem; include!("../../jl_helpers.rs"); #[test] @@ -60,12 +59,6 @@ fn test_is_dominating_set_function() { assert!(!is_dominating_set(&graph, &[false, false, false, false])); } -#[test] -fn test_direction() { - let problem = MinimumDominatingSet::new(SimpleGraph::new(2, vec![(0, 1)]), vec![1i32; 2]); - assert_eq!(problem.direction(), ExtremumSense::Minimize); -} - #[test] fn test_isolated_vertex() { // Isolated vertex must be in dominating set 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 33cda723e..c402935ac 100644 --- a/src/unit_tests/models/graph/minimum_dummy_activities_pert.rs +++ b/src/unit_tests/models/graph/minimum_dummy_activities_pert.rs @@ -1,8 +1,8 @@ use super::*; use crate::solvers::BruteForce; use crate::topology::DirectedGraph; -use crate::traits::{ObjectiveProblem, Problem}; -use crate::types::{ExtremumSense, Min}; +use crate::traits::Problem; +use crate::types::Min; fn issue_graph() -> DirectedGraph { DirectedGraph::new(6, vec![(0, 2), (0, 3), (1, 3), (1, 4), (2, 5)]) @@ -48,7 +48,6 @@ fn test_minimum_dummy_activities_pert_rejects_cyclic_input() { fn test_minimum_dummy_activities_pert_issue_example() { let problem = issue_problem(); let config = config_for_merges(&problem, &[(0, 2), (1, 4), (2, 5)]); - assert_eq!(problem.direction(), ExtremumSense::Minimize); assert_eq!(problem.evaluate(&config), Min(Some(2))); assert!(problem.is_valid_solution(&config)); } 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 864552127..1ede2865b 100644 --- a/src/unit_tests/models/graph/minimum_feedback_arc_set.rs +++ b/src/unit_tests/models/graph/minimum_feedback_arc_set.rs @@ -1,8 +1,7 @@ use super::*; use crate::solvers::BruteForce; use crate::topology::DirectedGraph; -use crate::traits::{ObjectiveProblem, Problem}; -use crate::types::ExtremumSense; +use crate::traits::Problem; #[test] fn test_minimum_feedback_arc_set_creation() { @@ -28,13 +27,6 @@ fn test_minimum_feedback_arc_set_creation() { assert!(problem.dims().iter().all(|&d| d == 2)); } -#[test] -fn test_minimum_feedback_arc_set_direction() { - let graph = DirectedGraph::new(3, vec![(0, 1), (1, 2), (2, 0)]); - let problem = MinimumFeedbackArcSet::new(graph, vec![1i32; 3]); - assert_eq!(problem.direction(), ExtremumSense::Minimize); -} - #[test] fn test_minimum_feedback_arc_set_evaluation_valid() { // Simple cycle: 0->1->2->0 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 3ccc42ddc..00fd26274 100644 --- a/src/unit_tests/models/graph/minimum_feedback_vertex_set.rs +++ b/src/unit_tests/models/graph/minimum_feedback_vertex_set.rs @@ -2,8 +2,7 @@ use super::is_feedback_vertex_set; use crate::models::graph::MinimumFeedbackVertexSet; use crate::solvers::BruteForce; use crate::topology::DirectedGraph; -use crate::traits::{ObjectiveProblem, Problem}; -use crate::types::ExtremumSense; +use crate::traits::Problem; /// Build the 9-vertex, 15-arc example from the issue. /// @@ -59,13 +58,6 @@ fn test_minimum_feedback_vertex_set_basic() { ); } -#[test] -fn test_minimum_feedback_vertex_set_direction() { - let graph = DirectedGraph::new(3, vec![(0, 1), (1, 2), (2, 0)]); - let problem = MinimumFeedbackVertexSet::new(graph, vec![1i32; 3]); - assert_eq!(problem.direction(), ExtremumSense::Minimize); -} - #[test] fn test_minimum_feedback_vertex_set_serialization() { let graph = example_graph(); diff --git a/src/unit_tests/models/graph/minimum_multiway_cut.rs b/src/unit_tests/models/graph/minimum_multiway_cut.rs index 85f5a0a3c..9cbbe5511 100644 --- a/src/unit_tests/models/graph/minimum_multiway_cut.rs +++ b/src/unit_tests/models/graph/minimum_multiway_cut.rs @@ -1,8 +1,8 @@ use super::*; use crate::solvers::BruteForce; use crate::topology::SimpleGraph; -use crate::traits::{ObjectiveProblem, Problem}; -use crate::types::{ExtremumSense, Min}; +use crate::traits::Problem; +use crate::types::Min; #[test] fn test_minimummultiwaycut_creation() { @@ -39,13 +39,6 @@ fn test_minimummultiwaycut_evaluate_invalid() { assert_eq!(result, Min(None)); } -#[test] -fn test_minimummultiwaycut_direction() { - let graph = SimpleGraph::new(3, vec![(0, 1), (1, 2)]); - let problem = MinimumMultiwayCut::new(graph, vec![0, 2], vec![1i32, 1]); - assert_eq!(problem.direction(), ExtremumSense::Minimize); -} - #[test] fn test_minimummultiwaycut_brute_force() { // Issue example: optimal cut has weight 8 diff --git a/src/unit_tests/models/graph/minimum_sum_multicenter.rs b/src/unit_tests/models/graph/minimum_sum_multicenter.rs index d6d3338ba..fdf833111 100644 --- a/src/unit_tests/models/graph/minimum_sum_multicenter.rs +++ b/src/unit_tests/models/graph/minimum_sum_multicenter.rs @@ -1,8 +1,7 @@ use super::*; use crate::solvers::BruteForce; use crate::topology::SimpleGraph; -use crate::traits::{ObjectiveProblem, Problem}; -use crate::types::ExtremumSense; +use crate::traits::Problem; #[test] fn test_min_sum_multicenter_creation() { @@ -24,13 +23,6 @@ fn test_min_sum_multicenter_size_getters() { assert_eq!(problem.num_centers(), 2); } -#[test] -fn test_min_sum_multicenter_direction() { - let graph = SimpleGraph::new(3, vec![(0, 1), (1, 2)]); - let problem = MinimumSumMulticenter::new(graph, vec![1i32; 3], vec![1i32; 2], 1); - assert_eq!(problem.direction(), ExtremumSense::Minimize); -} - #[test] fn test_min_sum_multicenter_evaluate_path() { // Path: 0-1-2, unit weights and lengths, K=1 diff --git a/src/unit_tests/models/graph/minimum_vertex_cover.rs b/src/unit_tests/models/graph/minimum_vertex_cover.rs index b4310fce0..39f052644 100644 --- a/src/unit_tests/models/graph/minimum_vertex_cover.rs +++ b/src/unit_tests/models/graph/minimum_vertex_cover.rs @@ -1,8 +1,7 @@ use super::*; use crate::solvers::BruteForce; use crate::topology::SimpleGraph; -use crate::traits::{ObjectiveProblem, Problem}; -use crate::types::ExtremumSense; +use crate::traits::Problem; include!("../../jl_helpers.rs"); #[test] @@ -42,12 +41,6 @@ fn test_is_vertex_cover_function() { )); } -#[test] -fn test_direction() { - let problem = MinimumVertexCover::new(SimpleGraph::new(3, vec![(0, 1)]), vec![1i32; 3]); - assert_eq!(problem.direction(), ExtremumSense::Minimize); -} - #[test] fn test_complement_relationship() { // For a graph, if S is an independent set, then V\S is a vertex cover 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 81355127e..2f6974355 100644 --- a/src/unit_tests/models/graph/partial_feedback_edge_set.rs +++ b/src/unit_tests/models/graph/partial_feedback_edge_set.rs @@ -95,11 +95,11 @@ fn test_partial_feedback_edge_set_solver_yes_and_no_instances() { let solver = BruteForce::new(); let yes_problem = yes_instance(); - let solution = solver.find_satisfying(&yes_problem).unwrap(); + let solution = solver.find_witness(&yes_problem).unwrap(); assert!(yes_problem.evaluate(&solution)); let no_problem = no_instance(); - assert!(solver.find_satisfying(&no_problem).is_none()); + assert!(solver.find_witness(&no_problem).is_none()); } #[test] @@ -108,7 +108,7 @@ fn test_partial_feedback_edge_set_paper_example() { let config = select_edges(problem.graph(), &[(0, 2), (2, 3), (3, 4)]); assert!(problem.evaluate(&config)); - let satisfying = BruteForce::new().find_all_satisfying(&problem); + let satisfying = BruteForce::new().find_all_witnesses(&problem); assert_eq!(satisfying.len(), 5); assert!(satisfying.iter().any(|candidate| candidate == &config)); } 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 87cbab8c0..751cde06e 100644 --- a/src/unit_tests/models/graph/path_constrained_network_flow.rs +++ b/src/unit_tests/models/graph/path_constrained_network_flow.rs @@ -86,7 +86,7 @@ fn test_path_constrained_network_flow_solver_yes_and_no() { let satisfying = solver.find_all_witnesses(&yes); assert_eq!(satisfying.len(), 2); - assert!(satisfying.iter().all(|config| yes.evaluate(config))); + assert!(satisfying.iter().all(|config| yes.evaluate(config).0)); assert!(solver.find_witness(&no).is_none()); } diff --git a/src/unit_tests/models/graph/rural_postman.rs b/src/unit_tests/models/graph/rural_postman.rs index 9f9364f78..bc872b669 100644 --- a/src/unit_tests/models/graph/rural_postman.rs +++ b/src/unit_tests/models/graph/rural_postman.rs @@ -193,7 +193,7 @@ fn test_rural_postman_find_all_witnesses() { } #[test] -fn test_rural_postman_find_all_satisfying_empty() { +fn test_rural_postman_find_all_witnesses_empty() { // Issue #248 instance 2: required edges {0,1} and {4,5} are far apart // Minimum circuit cost ≥ 8 > B=4 let graph = SimpleGraph::new( diff --git a/src/unit_tests/models/graph/spin_glass.rs b/src/unit_tests/models/graph/spin_glass.rs index 239d00c1b..e5ff4fdea 100644 --- a/src/unit_tests/models/graph/spin_glass.rs +++ b/src/unit_tests/models/graph/spin_glass.rs @@ -1,7 +1,6 @@ use super::*; use crate::solvers::BruteForce; -use crate::traits::{ObjectiveProblem, Problem}; -use crate::types::ExtremumSense; +use crate::traits::Problem; include!("../../jl_helpers.rs"); #[test] @@ -67,12 +66,6 @@ fn test_compute_energy_with_fields() { assert_eq!(problem.compute_energy(&[-1, 1]), -2.0); // -1 - 1 = -2 } -#[test] -fn test_direction() { - let problem = SpinGlass::::without_fields(2, vec![]); - assert_eq!(problem.direction(), ExtremumSense::Minimize); -} - #[test] fn test_num_variables() { let problem = SpinGlass::::without_fields(5, vec![]); diff --git a/src/unit_tests/models/graph/steiner_tree.rs b/src/unit_tests/models/graph/steiner_tree.rs index 98d525843..00cd8d505 100644 --- a/src/unit_tests/models/graph/steiner_tree.rs +++ b/src/unit_tests/models/graph/steiner_tree.rs @@ -1,10 +1,5 @@ use super::*; -use crate::{ - solvers::BruteForce, - topology::SimpleGraph, - traits::{ObjectiveProblem, Problem}, - types::ExtremumSense, -}; +use crate::{solvers::BruteForce, topology::SimpleGraph, traits::Problem}; /// Issue #122 example: 5 vertices, 7 edges, terminals {0, 2, 4}. /// Edges in order: (0,1)=2, (0,3)=5, (1,2)=2, (1,3)=1, (2,3)=5, (2,4)=6, (3,4)=1 @@ -34,12 +29,6 @@ fn test_steiner_tree_rejects_duplicate_terminals() { let _ = SteinerTree::new(graph, vec![1, 1], vec![0, 0]); } -#[test] -fn test_steiner_tree_direction() { - let problem = example_instance(); - assert_eq!(problem.direction(), ExtremumSense::Minimize); -} - #[test] fn test_steiner_tree_size_getters() { let problem = example_instance(); diff --git a/src/unit_tests/models/graph/steiner_tree_in_graphs.rs b/src/unit_tests/models/graph/steiner_tree_in_graphs.rs index f84dee7a1..cf09155cc 100644 --- a/src/unit_tests/models/graph/steiner_tree_in_graphs.rs +++ b/src/unit_tests/models/graph/steiner_tree_in_graphs.rs @@ -1,8 +1,7 @@ use super::*; use crate::solvers::BruteForce; use crate::topology::SimpleGraph; -use crate::traits::{ObjectiveProblem, Problem}; -use crate::types::ExtremumSense; +use crate::traits::Problem; #[test] fn test_steiner_tree_creation() { @@ -47,13 +46,6 @@ fn test_steiner_tree_evaluation() { assert!(!result.is_valid()); } -#[test] -fn test_steiner_tree_direction() { - let graph = SimpleGraph::new(3, vec![(0, 1), (1, 2)]); - let problem = SteinerTreeInGraphs::new(graph, vec![0, 2], vec![1i32; 2]); - assert_eq!(problem.direction(), ExtremumSense::Minimize); -} - #[test] fn test_steiner_tree_solver() { // Diamond graph: diff --git a/src/unit_tests/models/graph/traveling_salesman.rs b/src/unit_tests/models/graph/traveling_salesman.rs index 4c8380387..1dc55c774 100644 --- a/src/unit_tests/models/graph/traveling_salesman.rs +++ b/src/unit_tests/models/graph/traveling_salesman.rs @@ -1,8 +1,8 @@ use super::*; use crate::solvers::BruteForce; use crate::topology::SimpleGraph; -use crate::traits::{ObjectiveProblem, Problem}; -use crate::types::{ExtremumSense, Min}; +use crate::traits::Problem; +use crate::types::Min; fn k4_tsp() -> TravelingSalesman { TravelingSalesman::new( @@ -139,15 +139,6 @@ fn test_brute_force_bipartite_no_solution() { assert!(solutions.is_empty()); } -#[test] -fn test_direction() { - let problem = TravelingSalesman::<_, i32>::unit_weights(SimpleGraph::new( - 3, - vec![(0, 1), (1, 2), (0, 2)], - )); - assert_eq!(problem.direction(), ExtremumSense::Minimize); -} - #[test] fn test_problem_name() { assert_eq!( diff --git a/src/unit_tests/models/misc/bin_packing.rs b/src/unit_tests/models/misc/bin_packing.rs index a661d9bd5..cb1c7568f 100644 --- a/src/unit_tests/models/misc/bin_packing.rs +++ b/src/unit_tests/models/misc/bin_packing.rs @@ -1,7 +1,6 @@ use super::*; use crate::solvers::BruteForce; -use crate::traits::{ObjectiveProblem, Problem}; -use crate::types::ExtremumSense; +use crate::traits::Problem; #[test] fn test_bin_packing_creation() { @@ -14,12 +13,6 @@ fn test_bin_packing_creation() { assert!(problem.dims().iter().all(|&d| d == 6)); } -#[test] -fn test_bin_packing_direction() { - let problem = BinPacking::new(vec![1, 2, 3], 5); - assert_eq!(problem.direction(), ExtremumSense::Minimize); -} - #[test] fn test_bin_packing_evaluate_valid() { // 6 items, capacity 10, sizes [6, 6, 5, 5, 4, 4] diff --git a/src/unit_tests/models/misc/factoring.rs b/src/unit_tests/models/misc/factoring.rs index 25f4b1c42..61deada65 100644 --- a/src/unit_tests/models/misc/factoring.rs +++ b/src/unit_tests/models/misc/factoring.rs @@ -1,7 +1,6 @@ use super::*; use crate::solvers::BruteForce; -use crate::traits::{ObjectiveProblem, Problem}; -use crate::types::ExtremumSense; +use crate::traits::Problem; include!("../../jl_helpers.rs"); #[test] @@ -50,12 +49,6 @@ fn test_is_factoring_function() { assert!(!is_factoring(6, 2, 2)); } -#[test] -fn test_direction() { - let problem = Factoring::new(2, 2, 6); - assert_eq!(problem.direction(), ExtremumSense::Minimize); -} - #[test] fn test_is_valid_factorization() { let problem = Factoring::new(2, 2, 6); diff --git a/src/unit_tests/models/misc/flow_shop_scheduling.rs b/src/unit_tests/models/misc/flow_shop_scheduling.rs index d99880207..916368a70 100644 --- a/src/unit_tests/models/misc/flow_shop_scheduling.rs +++ b/src/unit_tests/models/misc/flow_shop_scheduling.rs @@ -178,7 +178,7 @@ fn test_flow_shop_scheduling_find_all_witnesses() { } #[test] -fn test_flow_shop_scheduling_find_all_satisfying_empty() { +fn test_flow_shop_scheduling_find_all_witnesses_empty() { // 2 machines, 2 symmetric jobs [5,5], deadline 10 // Both orderings give makespan 15 > 10 let problem = FlowShopScheduling::new(2, vec![vec![5, 5], vec![5, 5]], 10); diff --git a/src/unit_tests/models/misc/grouping_by_swapping.rs b/src/unit_tests/models/misc/grouping_by_swapping.rs index 0162db136..1436988be 100644 --- a/src/unit_tests/models/misc/grouping_by_swapping.rs +++ b/src/unit_tests/models/misc/grouping_by_swapping.rs @@ -60,16 +60,16 @@ fn test_grouping_by_swapping_bruteforce_yes_and_no() { let solver = BruteForce::new(); let satisfying = solver - .find_satisfying(&yes_problem) + .find_witness(&yes_problem) .expect("expected a satisfying 3-swap sequence"); assert!(yes_problem.evaluate(&satisfying)); assert!(solver - .find_all_satisfying(&yes_problem) + .find_all_witnesses(&yes_problem) .iter() .any(|config| config == &vec![2, 1, 3])); - assert!(solver.find_satisfying(&no_problem).is_none()); - assert!(solver.find_all_satisfying(&no_problem).is_empty()); + assert!(solver.find_witness(&no_problem).is_none()); + assert!(solver.find_all_witnesses(&no_problem).is_empty()); } #[test] @@ -79,10 +79,10 @@ fn test_grouping_by_swapping_paper_example() { let solver = BruteForce::new(); assert!(solver - .find_all_satisfying(&problem) + .find_all_witnesses(&problem) .iter() .any(|config| config == &vec![2, 1, 3, 5, 5])); - assert!(solver.find_satisfying(&issue_two_swap_instance()).is_none()); + assert!(solver.find_witness(&issue_two_swap_instance()).is_none()); } #[test] diff --git a/src/unit_tests/models/misc/knapsack.rs b/src/unit_tests/models/misc/knapsack.rs index 12028ce0b..ec75b7077 100644 --- a/src/unit_tests/models/misc/knapsack.rs +++ b/src/unit_tests/models/misc/knapsack.rs @@ -1,7 +1,6 @@ use super::*; use crate::solvers::BruteForce; -use crate::traits::{ObjectiveProblem, Problem}; -use crate::types::ExtremumSense; +use crate::traits::Problem; #[test] fn test_knapsack_basic() { @@ -11,7 +10,6 @@ fn test_knapsack_basic() { assert_eq!(problem.values(), &[3, 4, 5, 7]); assert_eq!(problem.capacity(), 7); assert_eq!(problem.dims(), vec![2; 4]); - assert_eq!(problem.direction(), ExtremumSense::Maximize); assert_eq!(::NAME, "Knapsack"); assert_eq!(::variant(), vec![]); } diff --git a/src/unit_tests/models/misc/longest_common_subsequence.rs b/src/unit_tests/models/misc/longest_common_subsequence.rs index 325438387..ef4a52a87 100644 --- a/src/unit_tests/models/misc/longest_common_subsequence.rs +++ b/src/unit_tests/models/misc/longest_common_subsequence.rs @@ -95,7 +95,7 @@ fn test_lcs_bruteforce_no() { } #[test] -fn test_lcs_find_all_satisfying_contains_issue_witness() { +fn test_lcs_find_all_witnesses_contains_issue_witness() { let problem = issue_yes_instance(); let solver = BruteForce::new(); let satisfying = solver.find_all_witnesses(&problem); diff --git a/src/unit_tests/models/misc/minimum_tardiness_sequencing.rs b/src/unit_tests/models/misc/minimum_tardiness_sequencing.rs index 6e1c0b1f1..fdcf29b64 100644 --- a/src/unit_tests/models/misc/minimum_tardiness_sequencing.rs +++ b/src/unit_tests/models/misc/minimum_tardiness_sequencing.rs @@ -1,7 +1,6 @@ use super::*; use crate::solvers::BruteForce; -use crate::traits::{ObjectiveProblem, Problem}; -use crate::types::ExtremumSense; +use crate::traits::Problem; #[test] fn test_minimum_tardiness_sequencing_basic() { @@ -15,7 +14,6 @@ fn test_minimum_tardiness_sequencing_basic() { assert_eq!(problem.precedences(), &[(0, 3), (1, 3), (1, 4), (2, 4)]); assert_eq!(problem.num_precedences(), 4); assert_eq!(problem.dims(), vec![5, 4, 3, 2, 1]); - assert_eq!(problem.direction(), ExtremumSense::Minimize); assert_eq!( ::NAME, "MinimumTardinessSequencing" diff --git a/src/unit_tests/models/misc/multiprocessor_scheduling.rs b/src/unit_tests/models/misc/multiprocessor_scheduling.rs index f8853869e..bbc5ff3d8 100644 --- a/src/unit_tests/models/misc/multiprocessor_scheduling.rs +++ b/src/unit_tests/models/misc/multiprocessor_scheduling.rs @@ -123,7 +123,7 @@ fn test_multiprocessor_scheduling_find_all_witnesses() { } #[test] -fn test_multiprocessor_scheduling_find_all_satisfying_empty() { +fn test_multiprocessor_scheduling_find_all_witnesses_empty() { // Same instance but deadline 9: total=20, need each processor ≤ 9, // but 20 > 2*9 = 18, so impossible let problem = MultiprocessorScheduling::new(vec![4, 5, 3, 2, 6], 2, 9); diff --git a/src/unit_tests/models/misc/paintshop.rs b/src/unit_tests/models/misc/paintshop.rs index 72695b1bf..6469f3118 100644 --- a/src/unit_tests/models/misc/paintshop.rs +++ b/src/unit_tests/models/misc/paintshop.rs @@ -1,7 +1,6 @@ use super::*; use crate::solvers::BruteForce; -use crate::traits::{ObjectiveProblem, Problem}; -use crate::types::ExtremumSense; +use crate::traits::Problem; include!("../../jl_helpers.rs"); #[test] @@ -55,12 +54,6 @@ fn test_count_paint_switches_function() { assert_eq!(count_paint_switches(&[0, 1, 0, 1]), 3); } -#[test] -fn test_direction() { - let problem = PaintShop::new(vec!["a", "a"]); - assert_eq!(problem.direction(), ExtremumSense::Minimize); -} - #[test] fn test_single_car() { let problem = PaintShop::new(vec!["a", "a"]); diff --git a/src/unit_tests/models/misc/partially_ordered_knapsack.rs b/src/unit_tests/models/misc/partially_ordered_knapsack.rs index 0e72472ec..9c8f907f2 100644 --- a/src/unit_tests/models/misc/partially_ordered_knapsack.rs +++ b/src/unit_tests/models/misc/partially_ordered_knapsack.rs @@ -1,7 +1,6 @@ use super::*; use crate::solvers::BruteForce; -use crate::traits::{ObjectiveProblem, Problem}; -use crate::types::ExtremumSense; +use crate::traits::Problem; /// Helper: create the example instance from the issue. /// Items: a=0, b=1, c=2, d=3, e=4, f=5 @@ -30,7 +29,6 @@ fn test_partially_ordered_knapsack_basic() { ); assert_eq!(problem.capacity(), 11); assert_eq!(problem.dims(), vec![2; 6]); - assert_eq!(problem.direction(), ExtremumSense::Maximize); assert_eq!( ::NAME, "PartiallyOrderedKnapsack" 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 df350b592..19ae0cab4 100644 --- a/src/unit_tests/models/misc/scheduling_with_individual_deadlines.rs +++ b/src/unit_tests/models/misc/scheduling_with_individual_deadlines.rs @@ -77,7 +77,7 @@ fn test_scheduling_with_individual_deadlines_evaluate_handles_huge_sparse_deadli let result = std::panic::catch_unwind(|| problem.evaluate(&[0])); - assert!(matches!(result, Ok(true))); + assert!(matches!(result, Ok(crate::types::Or(true)))); } #[test] 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 d8e5b18d6..1c0339215 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,7 +1,7 @@ use super::*; use crate::solvers::BruteForce; -use crate::traits::{ObjectiveProblem, Problem}; -use crate::types::{ExtremumSense, Min}; +use crate::traits::Problem; +use crate::types::Min; #[test] fn test_sequencing_to_minimize_weighted_completion_time_basic() { @@ -18,7 +18,6 @@ fn test_sequencing_to_minimize_weighted_completion_time_basic() { assert_eq!(problem.num_precedences(), 2); assert_eq!(problem.total_processing_time(), 9); assert_eq!(problem.dims(), vec![5, 4, 3, 2, 1]); - assert_eq!(problem.direction(), ExtremumSense::Minimize); assert_eq!( ::NAME, "SequencingToMinimizeWeightedCompletionTime" diff --git a/src/unit_tests/models/misc/sequencing_within_intervals.rs b/src/unit_tests/models/misc/sequencing_within_intervals.rs index a2a67bade..a9a60ac2f 100644 --- a/src/unit_tests/models/misc/sequencing_within_intervals.rs +++ b/src/unit_tests/models/misc/sequencing_within_intervals.rs @@ -168,7 +168,7 @@ fn test_sequencing_within_intervals_find_all_witnesses() { } #[test] -fn test_sequencing_within_intervals_find_all_satisfying_empty() { +fn test_sequencing_within_intervals_find_all_witnesses_empty() { // Two tasks that must both use time [0,2), impossible without overlap let problem = SequencingWithinIntervals::new(vec![0, 0], vec![2, 2], vec![2, 2]); let solver = BruteForce::new(); diff --git a/src/unit_tests/models/misc/shortest_common_supersequence.rs b/src/unit_tests/models/misc/shortest_common_supersequence.rs index fd2b2cdb0..4bceb8e1a 100644 --- a/src/unit_tests/models/misc/shortest_common_supersequence.rs +++ b/src/unit_tests/models/misc/shortest_common_supersequence.rs @@ -142,7 +142,7 @@ fn test_shortestcommonsupersequence_find_all_witnesses() { } #[test] -fn test_shortestcommonsupersequence_find_all_satisfying_empty() { +fn test_shortestcommonsupersequence_find_all_witnesses_empty() { // Issue #412 instance 3: all 6 permutations of {a,b,c}, bound 5 // Minimum SCS length is 7, so bound 5 is infeasible let problem = ShortestCommonSupersequence::new( diff --git a/src/unit_tests/models/set/maximum_set_packing.rs b/src/unit_tests/models/set/maximum_set_packing.rs index 42ccc0ce3..405d55d1b 100644 --- a/src/unit_tests/models/set/maximum_set_packing.rs +++ b/src/unit_tests/models/set/maximum_set_packing.rs @@ -1,7 +1,7 @@ use super::*; use crate::solvers::BruteForce; -use crate::traits::{ObjectiveProblem, Problem}; -use crate::types::{ExtremumSense, Max}; +use crate::traits::Problem; +use crate::types::Max; include!("../../jl_helpers.rs"); #[test] @@ -46,12 +46,6 @@ fn test_is_set_packing_function() { assert!(is_set_packing(&sets, &[false, false, false])); // Empty is valid } -#[test] -fn test_direction() { - let problem = MaximumSetPacking::::new(vec![vec![0, 1]]); - assert_eq!(problem.direction(), ExtremumSense::Maximize); -} - #[test] fn test_empty_sets() { let problem = MaximumSetPacking::::new(vec![]); diff --git a/src/unit_tests/models/set/minimum_cardinality_key.rs b/src/unit_tests/models/set/minimum_cardinality_key.rs index aa0e742f1..833e3a42b 100644 --- a/src/unit_tests/models/set/minimum_cardinality_key.rs +++ b/src/unit_tests/models/set/minimum_cardinality_key.rs @@ -74,7 +74,7 @@ fn test_minimum_cardinality_key_solver() { assert!(!solutions.is_empty()); assert!(solution_set.contains(&vec![1, 1, 0, 0, 0, 0])); - assert!(solutions.iter().all(|sol| problem.evaluate(sol))); + assert!(solutions.iter().all(|sol| problem.evaluate(sol).0)); } #[test] @@ -138,5 +138,5 @@ fn test_minimum_cardinality_key_paper_example() { let solution_set: HashSet> = solutions.iter().cloned().collect(); assert!(solution_set.contains(&solution)); // All returned solutions must be valid. - assert!(solutions.iter().all(|sol| problem.evaluate(sol))); + assert!(solutions.iter().all(|sol| problem.evaluate(sol).0)); } diff --git a/src/unit_tests/models/set/minimum_hitting_set.rs b/src/unit_tests/models/set/minimum_hitting_set.rs index 5a05fbe56..576f39b44 100644 --- a/src/unit_tests/models/set/minimum_hitting_set.rs +++ b/src/unit_tests/models/set/minimum_hitting_set.rs @@ -1,8 +1,8 @@ use super::*; use crate::registry::declared_size_fields; use crate::solvers::BruteForce; -use crate::traits::{ObjectiveProblem, Problem}; -use crate::types::{ExtremumSense, Min}; +use crate::traits::Problem; +use crate::types::Min; use std::collections::HashSet; fn issue_example_problem() -> MinimumHittingSet { @@ -111,12 +111,6 @@ fn test_minimum_hitting_set_paper_example_consistency() { assert_eq!(problem.evaluate(&issue_example_config()), Min(Some(3))); } -#[test] -fn test_minimum_hitting_set_direction() { - let problem = MinimumHittingSet::new(3, vec![vec![0, 1], vec![1, 2]]); - assert_eq!(problem.direction(), ExtremumSense::Minimize); -} - #[test] fn test_minimum_hitting_set_declares_problem_size_fields() { let fields: HashSet<&'static str> = declared_size_fields("MinimumHittingSet") diff --git a/src/unit_tests/models/set/minimum_set_covering.rs b/src/unit_tests/models/set/minimum_set_covering.rs index c950a9cb8..c218fc56d 100644 --- a/src/unit_tests/models/set/minimum_set_covering.rs +++ b/src/unit_tests/models/set/minimum_set_covering.rs @@ -1,7 +1,7 @@ use super::*; use crate::solvers::BruteForce; -use crate::traits::{ObjectiveProblem, Problem}; -use crate::types::{ExtremumSense, Min}; +use crate::traits::Problem; +use crate::types::Min; include!("../../jl_helpers.rs"); #[test] @@ -52,12 +52,6 @@ fn test_get_set() { assert_eq!(problem.get_set(2), None); } -#[test] -fn test_direction() { - let problem = MinimumSetCovering::::new(2, vec![vec![0, 1]]); - assert_eq!(problem.direction(), ExtremumSense::Minimize); -} - #[test] fn test_empty_universe() { let problem = MinimumSetCovering::::new(0, vec![]); diff --git a/src/unit_tests/models/set/set_basis.rs b/src/unit_tests/models/set/set_basis.rs index 3a696b455..ff4bb3a27 100644 --- a/src/unit_tests/models/set/set_basis.rs +++ b/src/unit_tests/models/set/set_basis.rs @@ -55,7 +55,9 @@ fn test_set_basis_solver() { assert_eq!(solutions.len(), 12); assert_eq!(solution_set.len(), 12); assert!(solution_set.contains(&canonical_solution())); - assert!(solutions.iter().all(|solution| problem.evaluate(solution))); + assert!(solutions + .iter() + .all(|solution| problem.evaluate(solution).0)); } #[test] diff --git a/src/unit_tests/reduction_graph.rs b/src/unit_tests/reduction_graph.rs index bea6dc7fd..ff9a14967 100644 --- a/src/unit_tests/reduction_graph.rs +++ b/src/unit_tests/reduction_graph.rs @@ -30,19 +30,6 @@ fn test_reduction_graph_discovers_registered_reductions() { assert!(graph.has_direct_reduction_by_name("Satisfiability", "MaximumIndependentSet")); } -#[test] -fn test_bidirectional_reductions() { - let graph = ReductionGraph::new(); - - // IS <-> VC should both be registered - assert!(graph.has_direct_reduction_by_name("MaximumIndependentSet", "MinimumVertexCover")); - assert!(graph.has_direct_reduction_by_name("MinimumVertexCover", "MaximumIndependentSet")); - - // MaxCut <-> SpinGlass should both be registered - assert!(graph.has_direct_reduction_by_name("MaxCut", "SpinGlass")); - assert!(graph.has_direct_reduction_by_name("SpinGlass", "MaxCut")); -} - // ---- Path finding (by name) ---- #[test] @@ -268,40 +255,6 @@ fn test_no_path_exists() { assert!(paths.is_empty()); } -#[test] -fn test_bidirectional_paths() { - let graph = ReductionGraph::new(); - let is_var = - ReductionGraph::variant_to_map(&MaximumIndependentSet::::variant()); - let vc_var = ReductionGraph::variant_to_map(&MinimumVertexCover::::variant()); - let sg_var = ReductionGraph::variant_to_map(&SpinGlass::::variant()); - let qubo_var = ReductionGraph::variant_to_map(&QUBO::::variant()); - - assert!(!graph - .find_all_paths( - "MaximumIndependentSet", - &is_var, - "MinimumVertexCover", - &vc_var - ) - .is_empty()); - assert!(!graph - .find_all_paths( - "MinimumVertexCover", - &vc_var, - "MaximumIndependentSet", - &is_var - ) - .is_empty()); - - assert!(!graph - .find_all_paths("SpinGlass", &sg_var, "QUBO", &qubo_var) - .is_empty()); - assert!(!graph - .find_all_paths("QUBO", &qubo_var, "SpinGlass", &sg_var) - .is_empty()); -} - // ---- Display ---- #[test] @@ -711,3 +664,65 @@ fn find_best_entry_accepts_exact_source_and_target_variant() { "Should find exact match on both source and target variant" ); } + +#[test] +fn test_has_direct_reduction_mode_witness() { + let graph = ReductionGraph::new(); + + // MIS -> MVC is witness-only, so Witness mode should find it + assert!(graph + .has_direct_reduction_mode::, MinimumVertexCover>( + ReductionMode::Witness, + )); +} + +#[test] +fn test_has_direct_reduction_by_name_mode() { + let graph = ReductionGraph::new(); + + assert!(graph.has_direct_reduction_by_name_mode( + "MaximumIndependentSet", + "MinimumVertexCover", + ReductionMode::Witness, + )); + + // Aggregate mode should not find witness-only edges + assert!(!graph.has_direct_reduction_by_name_mode( + "MaximumIndependentSet", + "MinimumVertexCover", + ReductionMode::Aggregate, + )); +} + +#[test] +fn test_find_all_paths_mode_witness() { + let graph = ReductionGraph::new(); + let src = ReductionGraph::variant_to_map(&MaximumIndependentSet::::variant()); + let dst = ReductionGraph::variant_to_map(&MinimumVertexCover::::variant()); + + let paths = graph.find_all_paths_mode( + "MaximumIndependentSet", + &src, + "MinimumVertexCover", + &dst, + ReductionMode::Witness, + ); + assert!(!paths.is_empty()); +} + +#[test] +fn test_find_all_paths_mode_aggregate_rejects_witness_only() { + let graph = ReductionGraph::new(); + let src = ReductionGraph::variant_to_map(&MaximumIndependentSet::::variant()); + let dst = ReductionGraph::variant_to_map(&MinimumVertexCover::::variant()); + + // MIS -> MVC is witness-only, so aggregate mode should find no paths + let paths = graph.find_all_paths_mode( + "MaximumIndependentSet", + &src, + "MinimumVertexCover", + &dst, + ReductionMode::Aggregate, + ); + assert!(paths.is_empty()); +} diff --git a/src/unit_tests/registry/dispatch.rs b/src/unit_tests/registry/dispatch.rs index 727916a53..45ef05151 100644 --- a/src/unit_tests/registry/dispatch.rs +++ b/src/unit_tests/registry/dispatch.rs @@ -90,11 +90,11 @@ fn test_loaded_dyn_problem_delegates_to_value_and_witness_fns() { solve_subset_sum_witness, ); - assert_eq!(loaded.solve_brute_force_value(), "true"); + assert_eq!(loaded.solve_brute_force_value(), "Or(true)"); let solved = loaded .solve_brute_force_witness() .expect("expected satisfying solution"); - assert_eq!(solved.1, "true"); + assert_eq!(solved.1, "Or(true)"); assert_eq!(solved.0.len(), 3); } @@ -171,9 +171,9 @@ fn test_load_dyn_solves_subset_sum() { ) .unwrap(); - assert_eq!(loaded.solve_brute_force_value(), "true"); + assert_eq!(loaded.solve_brute_force_value(), "Or(true)"); let solved = loaded.solve_brute_force_witness().unwrap(); - assert_eq!(solved.1, "true"); + assert_eq!(solved.1, "Or(true)"); } #[test] diff --git a/src/unit_tests/rules/coloring_ilp.rs b/src/unit_tests/rules/coloring_ilp.rs index 3bbee8eff..f9cbfa0b9 100644 --- a/src/unit_tests/rules/coloring_ilp.rs +++ b/src/unit_tests/rules/coloring_ilp.rs @@ -53,7 +53,7 @@ fn test_coloring_to_ilp_closed_loop() { let bf = BruteForce::new(); let ilp_solver = ILPSolver::new(); - // Solve with brute force on original problem - use find_all_satisfying for satisfaction problems + // Solve with brute force on original problem - use find_all_witnesses for satisfaction problems let bf_solutions = bf.find_all_witnesses(&problem); assert!( !bf_solutions.is_empty(), diff --git a/src/unit_tests/rules/graph.rs b/src/unit_tests/rules/graph.rs index db64b6fdb..9687a19ec 100644 --- a/src/unit_tests/rules/graph.rs +++ b/src/unit_tests/rules/graph.rs @@ -637,32 +637,6 @@ fn test_reduction_path_methods() { assert!(path.target().unwrap().contains("MinimumVertexCover")); } -#[test] -fn test_bidirectional_paths() { - let graph = ReductionGraph::new(); - let is_var = - ReductionGraph::variant_to_map(&MaximumIndependentSet::::variant()); - let vc_var = ReductionGraph::variant_to_map(&MinimumVertexCover::::variant()); - - // Forward path - let forward = graph.find_all_paths( - "MaximumIndependentSet", - &is_var, - "MinimumVertexCover", - &vc_var, - ); - assert!(!forward.is_empty()); - - // Backward path - let backward = graph.find_all_paths( - "MinimumVertexCover", - &vc_var, - "MaximumIndependentSet", - &is_var, - ); - assert!(!backward.is_empty()); -} - #[test] fn test_to_json() { let graph = ReductionGraph::new(); diff --git a/src/unit_tests/rules/registry.rs b/src/unit_tests/rules/registry.rs index 474f88cb6..21ba8dde5 100644 --- a/src/unit_tests/rules/registry.rs +++ b/src/unit_tests/rules/registry.rs @@ -1,5 +1,6 @@ use super::*; use crate::expr::Expr; +use crate::rules::registry::EdgeCapabilities; use std::path::Path; /// Dummy reduce_fn for unit tests that don't exercise runtime reduction. @@ -445,3 +446,32 @@ fn repo_reductions_use_overhead_only_attribute() { offenders, ); } + +#[test] +fn test_edge_capabilities_constructors() { + let wo = EdgeCapabilities::witness_only(); + assert!(wo.witness); + assert!(!wo.aggregate); + + let ao = EdgeCapabilities::aggregate_only(); + assert!(!ao.witness); + assert!(ao.aggregate); + + let both = EdgeCapabilities::both(); + assert!(both.witness); + assert!(both.aggregate); +} + +#[test] +fn test_edge_capabilities_default_is_witness_only() { + let default = EdgeCapabilities::default(); + assert_eq!(default, EdgeCapabilities::witness_only()); +} + +#[test] +fn test_edge_capabilities_serde_roundtrip() { + let caps = EdgeCapabilities::both(); + let json = serde_json::to_string(&caps).unwrap(); + let back: EdgeCapabilities = serde_json::from_str(&json).unwrap(); + assert_eq!(caps, back); +} diff --git a/src/unit_tests/rules/sat_coloring.rs b/src/unit_tests/rules/sat_coloring.rs index a5cba5f87..929bb7651 100644 --- a/src/unit_tests/rules/sat_coloring.rs +++ b/src/unit_tests/rules/sat_coloring.rs @@ -73,7 +73,7 @@ fn test_unsatisfiable_formula() { let reduction = ReduceTo::>::reduce_to(&sat); let coloring = reduction.target_problem(); - // Solve the coloring problem - use find_all_satisfying since KColoring is a satisfaction problem + // Solve the coloring problem - use find_all_witnesses since KColoring is a satisfaction problem let solver = BruteForce::new(); let solutions = solver.find_all_witnesses(coloring); diff --git a/src/unit_tests/rules/sat_ksat.rs b/src/unit_tests/rules/sat_ksat.rs index cca0c1c9b..3c20a3c05 100644 --- a/src/unit_tests/rules/sat_ksat.rs +++ b/src/unit_tests/rules/sat_ksat.rs @@ -117,7 +117,7 @@ fn test_sat_to_ksat_closed_loop() { let reduction = ReduceTo::>::reduce_to(&sat); let ksat = reduction.target_problem(); - // Solve both problems - use find_all_satisfying for satisfaction problems + // Solve both problems - use find_all_witnesses for satisfaction problems let solver = BruteForce::new(); let sat_solutions = solver.find_all_witnesses(&sat); @@ -146,7 +146,7 @@ fn test_sat_to_3sat_solution_extraction() { let reduction = ReduceTo::>::reduce_to(&sat); let ksat = reduction.target_problem(); - // Solve K-SAT - use find_all_satisfying for satisfaction problems + // Solve K-SAT - use find_all_witnesses for satisfaction problems let solver = BruteForce::new(); let ksat_solutions = solver.find_all_witnesses(ksat); @@ -208,7 +208,7 @@ fn test_roundtrip_sat_3sat_sat() { let to_sat = ReduceTo::::reduce_to(ksat); let final_sat = to_sat.target_problem(); - // Solve all three - use find_all_satisfying for satisfaction problems + // Solve all three - use find_all_witnesses for satisfaction problems let solver = BruteForce::new(); let orig_solutions = solver.find_all_witnesses(&original_sat); @@ -286,7 +286,7 @@ fn test_mixed_clause_sizes() { assert_eq!(clause.len(), 3); } - // Verify satisfiability is preserved - use find_all_satisfying for satisfaction problems + // Verify satisfiability is preserved - use find_all_witnesses for satisfaction problems assert_satisfaction_round_trip_from_satisfaction_target( &sat, &reduction, diff --git a/src/unit_tests/solvers/brute_force.rs b/src/unit_tests/solvers/brute_force.rs index 11800481b..75d31d717 100644 --- a/src/unit_tests/solvers/brute_force.rs +++ b/src/unit_tests/solvers/brute_force.rs @@ -233,3 +233,37 @@ fn test_solver_with_real_sat() { assert!(problem.evaluate(sol)); } } + +#[test] +fn test_solve_with_witnesses_max() { + let problem = MaxSumProblem { + weights: vec![1, 2, 3], + }; + let solver = BruteForce::new(); + + let (value, witnesses) = solver.solve_with_witnesses(&problem); + assert_eq!(value, Max(Some(6))); + assert_eq!(witnesses, vec![vec![1, 1, 1]]); +} + +#[test] +fn test_solve_with_witnesses_sum_returns_empty() { + let problem = SumProblem { + weights: vec![1, 2], + }; + let solver = BruteForce::new(); + + let (value, witnesses) = solver.solve_with_witnesses(&problem); + assert_eq!(value, Sum(6)); // 0+0 + 0+2 + 1+0 + 1+2 = 6 + assert!(witnesses.is_empty()); +} + +#[test] +fn test_solver_trait_solve() { + let problem = MaxSumProblem { + weights: vec![1, 2, 3], + }; + let solver = BruteForce::new(); + + assert_eq!(Solver::solve(&solver, &problem), Max(Some(6))); +} diff --git a/src/unit_tests/trait_consistency.rs b/src/unit_tests/trait_consistency.rs index 70beb0d12..7e0fbba89 100644 --- a/src/unit_tests/trait_consistency.rs +++ b/src/unit_tests/trait_consistency.rs @@ -22,7 +22,6 @@ fn check_problem_trait(problem: &P, name: &str) { ); } } - #[test] fn test_all_problems_implement_trait_correctly() { check_problem_trait( @@ -237,79 +236,3 @@ fn test_all_problems_implement_trait_correctly() { "ConsecutiveOnesSubmatrix", ); } - -#[test] -fn test_direction() { - use crate::traits::ObjectiveProblem; - use crate::types::ExtremumSense; - - // Minimization problems - assert_eq!( - MinimumVertexCover::new(SimpleGraph::new(2, vec![(0, 1)]), vec![1i32; 2]).direction(), - ExtremumSense::Minimize - ); - assert_eq!( - MinimumDominatingSet::new(SimpleGraph::new(2, vec![(0, 1)]), vec![1i32; 2]).direction(), - ExtremumSense::Minimize - ); - assert_eq!( - MinimumSetCovering::::new(2, vec![vec![0, 1]]).direction(), - ExtremumSense::Minimize - ); - assert_eq!( - PaintShop::new(vec!["a", "a"]).direction(), - ExtremumSense::Minimize - ); - assert_eq!( - QUBO::from_matrix(vec![vec![1.0]]).direction(), - ExtremumSense::Minimize - ); - assert_eq!( - SpinGlass::new(1, vec![], vec![0.0]).direction(), - ExtremumSense::Minimize - ); - assert_eq!( - BMF::new(vec![vec![true]], 1).direction(), - ExtremumSense::Minimize - ); - assert_eq!(Factoring::new(6, 2, 2).direction(), ExtremumSense::Minimize); - assert_eq!( - BicliqueCover::new(BipartiteGraph::new(2, 2, vec![(0, 0)]), 1).direction(), - ExtremumSense::Minimize - ); - assert_eq!( - QuadraticAssignment::new(vec![vec![0, 1], vec![1, 0]], vec![vec![0, 1], vec![1, 0]]) - .direction(), - ExtremumSense::Minimize - ); - - // Maximization problems - assert_eq!( - MaximumIndependentSet::new(SimpleGraph::new(2, vec![(0, 1)]), vec![1i32; 2]).direction(), - ExtremumSense::Maximize - ); - assert_eq!( - MaximalIS::new(SimpleGraph::new(2, vec![(0, 1)]), vec![1i32; 2]).direction(), - ExtremumSense::Maximize - ); - assert_eq!( - MaxCut::new(SimpleGraph::new(2, vec![(0, 1)]), vec![1i32]).direction(), - ExtremumSense::Maximize - ); - assert_eq!( - MaximumMatching::new(SimpleGraph::new(2, vec![(0, 1)]), vec![1i32]).direction(), - ExtremumSense::Maximize - ); - assert_eq!( - MaximumSetPacking::::new(vec![vec![0]]).direction(), - ExtremumSense::Maximize - ); - assert_eq!( - MaximumClique::new(SimpleGraph::new(2, vec![(0, 1)]), vec![1i32; 2]).direction(), - ExtremumSense::Maximize - ); - assert_eq!( - PartiallyOrderedKnapsack::new(vec![2, 3], vec![3, 2], vec![(0, 1)], 5).direction(), - ExtremumSense::Maximize - ); -} diff --git a/src/unit_tests/types.rs b/src/unit_tests/types.rs index 47524d7fa..e0f9d01f9 100644 --- a/src/unit_tests/types.rs +++ b/src/unit_tests/types.rs @@ -1,4 +1,5 @@ use super::*; +use crate::types::Aggregate; #[test] fn test_max_identity_and_combine() { @@ -155,16 +156,6 @@ fn test_one_json() { assert_eq!(parsed, vec![One, One]); } -#[test] -fn test_direction() { - let max_dir = ExtremumSense::Maximize; - let min_dir = ExtremumSense::Minimize; - - assert_eq!(max_dir, ExtremumSense::Maximize); - assert_eq!(min_dir, ExtremumSense::Minimize); - assert_ne!(max_dir, min_dir); -} - #[test] fn test_problem_size() { let ps = ProblemSize::new(vec![("vertices", 10), ("edges", 20)]); @@ -226,3 +217,97 @@ fn test_weight_element_f64() { let neg: f64 = -2.5; assert_eq!(neg.to_sum(), -2.5); } + +#[test] +fn test_extremum_aggregate_identity_and_combine() { + // identity is Maximize(None) + let id = Extremum::::identity(); + assert_eq!(id.sense, ExtremumSense::Maximize); + assert_eq!(id.value, None); + + // None + Some => Some (takes rhs sense) + let combined = Extremum::::identity().combine(Extremum::maximize(Some(5))); + assert_eq!(combined, Extremum::maximize(Some(5))); + + // Some + None => Some (keeps lhs sense) + let combined = Extremum::minimize(Some(3)).combine(Extremum::::identity()); + assert_eq!(combined, Extremum::minimize(Some(3))); + + // Maximize: keeps the larger + let combined = Extremum::maximize(Some(3)).combine(Extremum::maximize(Some(7))); + assert_eq!(combined, Extremum::maximize(Some(7))); + let combined = Extremum::maximize(Some(7)).combine(Extremum::maximize(Some(3))); + assert_eq!(combined, Extremum::maximize(Some(7))); + + // Minimize: keeps the smaller + let combined = Extremum::minimize(Some(3)).combine(Extremum::minimize(Some(7))); + assert_eq!(combined, Extremum::minimize(Some(3))); + let combined = Extremum::minimize(Some(7)).combine(Extremum::minimize(Some(3))); + assert_eq!(combined, Extremum::minimize(Some(3))); +} + +#[test] +fn test_extremum_witness_hooks() { + assert!(Extremum::::supports_witnesses()); + + // Matching value and sense -> contributes + assert!(Extremum::contributes_to_witnesses( + &Extremum::maximize(Some(10)), + &Extremum::maximize(Some(10)), + )); + + // Different value -> does not contribute + assert!(!Extremum::contributes_to_witnesses( + &Extremum::maximize(Some(5)), + &Extremum::maximize(Some(10)), + )); + + // None config -> does not contribute + assert!(!Extremum::contributes_to_witnesses( + &Extremum::::maximize(None), + &Extremum::maximize(Some(10)), + )); +} + +#[test] +fn test_extremum_display() { + assert_eq!(format!("{}", Extremum::maximize(Some(42))), "Max(42)"); + assert_eq!(format!("{}", Extremum::::maximize(None)), "Max(None)"); + assert_eq!(format!("{}", Extremum::minimize(Some(7))), "Min(7)"); + assert_eq!(format!("{}", Extremum::::minimize(None)), "Min(None)"); +} + +#[test] +#[should_panic(expected = "called unwrap on invalid Extremum value")] +fn test_extremum_unwrap_panics() { + Extremum::::minimize(None).unwrap(); +} + +#[test] +fn test_max_display() { + assert_eq!(format!("{}", Max(Some(42))), "Max(42)"); + assert_eq!(format!("{}", Max::(None)), "Max(None)"); +} + +#[test] +fn test_min_display() { + assert_eq!(format!("{}", Min(Some(7))), "Min(7)"); + 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)"); +} diff --git a/tests/suites/integration.rs b/tests/suites/integration.rs index 126dae9a1..9a27e15df 100644 --- a/tests/suites/integration.rs +++ b/tests/suites/integration.rs @@ -59,7 +59,7 @@ mod all_problems_solvable { fn test_coloring_solvable() { let problem = KColoring::::new(SimpleGraph::new(3, vec![(0, 1), (1, 2)])); let solver = BruteForce::new(); - // KColoring returns bool, so we can use find_all_satisfying + // KColoring uses the witness-capable `Or` aggregate, so all witnesses are valid colorings. let satisfying = solver.find_all_witnesses(&problem); assert!(!satisfying.is_empty()); for sol in &satisfying { @@ -148,7 +148,7 @@ mod all_problems_solvable { let solver = BruteForce::new(); let satisfying = solver.find_all_witnesses(&problem); assert_eq!(satisfying, vec![vec![0, 0, 1]]); - assert!(satisfying.iter().all(|config| problem.evaluate(config))); + assert!(satisfying.iter().all(|config| problem.evaluate(config).0)); } #[test] @@ -157,13 +157,13 @@ mod all_problems_solvable { 3, vec![CNFClause::new(vec![1, 2]), CNFClause::new(vec![-1, 3])], ); - // Satisfiability returns bool, find satisfying configs manually + // Satisfiability uses `Or`, so any config with `evaluate(config).0` is a witness. let dims = problem.dims(); let all_configs: Vec> = problemreductions::config::DimsIterator::new(dims.clone()).collect(); let satisfying: Vec> = all_configs .into_iter() - .filter(|config| problem.evaluate(config)) + .filter(|config| problem.evaluate(config).0) .collect(); assert!(!satisfying.is_empty()); for sol in &satisfying { @@ -222,13 +222,13 @@ mod all_problems_solvable { BooleanExpr::and(vec![BooleanExpr::var("x"), BooleanExpr::var("y")]), )]); let problem = CircuitSAT::new(circuit); - // CircuitSAT returns bool + // CircuitSAT also uses `Or`, so witness enumeration lines up with configs where `.0` is true. let dims = problem.dims(); let all_configs: Vec> = problemreductions::config::DimsIterator::new(dims.clone()).collect(); let satisfying: Vec> = all_configs .into_iter() - .filter(|config| problem.evaluate(config)) + .filter(|config| problem.evaluate(config).0) .collect(); assert!(!satisfying.is_empty()); for sol in &satisfying { @@ -435,7 +435,7 @@ mod edge_cases { problemreductions::config::DimsIterator::new(dims.clone()).collect(); let satisfying: Vec> = all_configs .into_iter() - .filter(|config| problem.evaluate(config)) + .filter(|config| problem.evaluate(config).0) .collect(); // (x1 OR NOT x2) is satisfied by 3 of 4 assignments @@ -536,7 +536,7 @@ mod weighted_problems { problemreductions::config::DimsIterator::new(dims.clone()).collect(); let satisfying: Vec> = all_configs .into_iter() - .filter(|config| problem.evaluate(config)) + .filter(|config| problem.evaluate(config).0) .collect(); // Can't satisfy both - no solution satisfies all clauses From 96fc035fd8590f6481e192611e3e208ebcaedef3 Mon Sep 17 00:00:00 2001 From: GiggleLiu Date: Mon, 23 Mar 2026 17:35:00 +0800 Subject: [PATCH 13/18] update - clean up --- docs/src/design.md | 32 +- docs/src/static/reduction-workflow-dark.svg | 253 +++--- docs/src/static/reduction-workflow.svg | 253 +++--- docs/src/static/reduction-workflow.typ | 2 +- docs/src/static/trait-hierarchy-dark.svg | 888 +++++++++----------- docs/src/static/trait-hierarchy.svg | 888 +++++++++----------- docs/src/static/trait-hierarchy.typ | 46 +- problemreductions-cli/src/mcp/tools.rs | 2 +- 8 files changed, 1117 insertions(+), 1247 deletions(-) diff --git a/docs/src/design.md b/docs/src/design.md index 3fff7a7cb..7f709edfc 100644 --- a/docs/src/design.md +++ b/docs/src/design.md @@ -25,29 +25,25 @@ This guide covers the library internals for contributors. ## Problem Model -Every problem implements `Problem`. Optimization problems additionally implement `OptimizationProblem`; satisfaction problems implement `SatisfactionProblem`. +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. ```rust,ignore trait Problem: Clone { const NAME: &'static str; // e.g., "MaximumIndependentSet" - type Metric: Clone; // SolutionSize or bool + type Value: Clone; // e.g., Max, Or, Sum fn dims(&self) -> Vec; // config space per variable - fn evaluate(&self, config: &[usize]) -> Self::Metric; + fn evaluate(&self, config: &[usize]) -> Self::Value; fn variant() -> Vec<(&'static str, &'static str)>; // e.g., [("graph", "SimpleGraph"), ("weight", "i32")] fn num_variables(&self) -> usize; // default: dims().len() + fn problem_type() -> ProblemType; // default: registry lookup by NAME } - -trait OptimizationProblem: Problem> { - type Value: PartialOrd + Clone; // e.g., i32, f64 - fn direction(&self) -> Direction; // Maximize or Minimize -} - -trait SatisfactionProblem: Problem {} // marker trait ``` -- **`Problem`** — the base trait. Every problem declares a `NAME` (e.g., `"MaximumIndependentSet"`). The solver explores the configuration space defined by `dims()` and scores each configuration with `evaluate()`. For example, a 4-vertex MIS has `dims() = [2, 2, 2, 2]` (each vertex is selected or not); `evaluate(&[1, 0, 1, 0])` returns `Valid(2)` if vertices 0 and 2 form an independent set, or `Invalid` if they share an edge. Each problem also provides inherent getter methods (e.g., `num_vertices()`, `num_edges()`) used by reduction overhead expressions. -- **`OptimizationProblem`** — extends `Problem` with a comparable `Value` type and a `direction()` (`Maximize` or `Minimize`). -- **`SatisfactionProblem`** — constrains `Metric = bool`: `true` if all constraints are satisfied, `false` otherwise. +- **`Problem`** — the base trait. Every problem declares a `NAME` (e.g., `"MaximumIndependentSet"`). The solver explores the configuration space defined by `dims()` and scores each configuration with `evaluate()`. For example, a 4-vertex MIS has `dims() = [2, 2, 2, 2]` (each vertex is selected or not); `evaluate(&[1, 0, 1, 0])` returns `Max(Some(2))` if vertices 0 and 2 form an independent set, or `Max(None)` if they share an edge. Each problem also provides inherent getter methods (e.g., `num_vertices()`, `num_edges()`) used by reduction overhead expressions. +- **Witness-capable objective problems** — typically use `Max`, `Min`, or `Extremum` as `Value`. +- **Witness-capable feasibility problems** — typically use `Or`. +- **Aggregate-only problems** — use fold values such as `Sum` or `And`; these solve to a value but do not admit representative witness configurations. +- **Common aggregate wrappers** — `Max`, `Min`, `Sum`, `Or`, `And`, `Extremum`, `ExtremumSense`. ## Variant System @@ -315,15 +311,17 @@ Solvers implement the `Solver` trait: ```rust,ignore pub trait Solver { - fn find_best(&self, problem: &P) -> Option>; - fn find_satisfying>(&self, problem: &P) -> Option>; + fn solve

(&self, problem: &P) -> P::Value + where + P: Problem, + P::Value: Aggregate; } ``` | Solver | Description | |--------|-------------| -| **BruteForce** | Enumerates all configurations. Also provides `find_all_best()` and `find_all_satisfying()`. Used for testing and verification. | -| **ILPSolver** | Enabled by default (`ilp` feature). Uses HiGHS via `good_lp`. Also provides `solve_reduced()` for problems that implement `ReduceTo`. | +| **BruteForce** | Enumerates all configurations. `solve()` works for any aggregate problem; `find_witness()`, `find_all_witnesses()`, and `solve_with_witnesses()` are available when `P::Value` supports witnesses. Used for testing and verification. | +| **ILPSolver** | Enabled by default. Solves ILP instances directly with HiGHS via `good_lp`. Also provides `solve_reduced()` for witness-capable problems that implement `ReduceTo>`. | ## JSON Serialization diff --git a/docs/src/static/reduction-workflow-dark.svg b/docs/src/static/reduction-workflow-dark.svg index b04e1c2cc..8e97a4ad2 100644 --- a/docs/src/static/reduction-workflow-dark.svg +++ b/docs/src/static/reduction-workflow-dark.svg @@ -38,10 +38,10 @@ - + - + @@ -53,10 +53,13 @@ - - - - + + + + + + + @@ -115,29 +118,29 @@ - - - - - - - - + + + + + + + + - - - - - - - - - - - - - + + + + + + + + + + + + + @@ -158,29 +161,29 @@ - - - - - - - - + + + + + + + + - - - - - - - - - - - - - + + + + + + + + + + + + + @@ -201,29 +204,29 @@ - - - - - - - - - + + + + + + + + + - - - - - - - - - - - - + + + + + + + + + + + + @@ -244,32 +247,32 @@ - - - - - - - - - + + + + + + + + + - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + @@ -318,8 +321,8 @@ - - + + @@ -333,94 +336,94 @@ - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + diff --git a/docs/src/static/reduction-workflow.svg b/docs/src/static/reduction-workflow.svg index 1254938ab..334c2c182 100644 --- a/docs/src/static/reduction-workflow.svg +++ b/docs/src/static/reduction-workflow.svg @@ -38,10 +38,10 @@ - + - + @@ -53,10 +53,13 @@ - - - - + + + + + + + @@ -115,29 +118,29 @@ - - - - - - - - + + + + + + + + - - - - - - - - - - - - - + + + + + + + + + + + + + @@ -158,29 +161,29 @@ - - - - - - - - + + + + + + + + - - - - - - - - - - - - - + + + + + + + + + + + + + @@ -201,29 +204,29 @@ - - - - - - - - - + + + + + + + + + - - - - - - - - - - - - + + + + + + + + + + + + @@ -244,32 +247,32 @@ - - - - - - - - - + + + + + + + + + - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + @@ -318,8 +321,8 @@ - - + + @@ -333,94 +336,94 @@ - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + diff --git a/docs/src/static/reduction-workflow.typ b/docs/src/static/reduction-workflow.typ index cbb5358ff..df911fb8b 100644 --- a/docs/src/static/reduction-workflow.typ +++ b/docs/src/static/reduction-workflow.typ @@ -27,7 +27,7 @@ // 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_best`], 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), ) } diff --git a/docs/src/static/trait-hierarchy-dark.svg b/docs/src/static/trait-hierarchy-dark.svg index e6df22f04..e932783a4 100644 --- a/docs/src/static/trait-hierarchy-dark.svg +++ b/docs/src/static/trait-hierarchy-dark.svg @@ -1,29 +1,43 @@ - + - - - - + + + + - + - - - - - - - + + + + + + + + + + + + + + + + + + + + + @@ -33,26 +47,29 @@ - - - - + + + + - + - - - - - - - + + + + + + + + + + @@ -62,29 +79,29 @@ - - + + - + - - - - - - - - - - - - + + + + + + + + + + + + @@ -107,18 +124,17 @@ - - - - - - - - - - - - + + + + + + + + + + + @@ -164,12 +180,11 @@ - - - - - - + + + + + @@ -210,202 +225,143 @@ - - + + - + - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -415,77 +371,99 @@ - - + + - + - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + - - - - - - - - - + + + + + + + + + + + + + + - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + @@ -501,52 +479,94 @@ - + + + + + + + + + + + + + - - + + - - + + + + + - + + + + - + - - + + + + + + + + + + + + + + - + + + + + + + + + + + + + - + - + - + - + - + - + - + - + - + @@ -591,24 +611,18 @@ - - - - - - - - - + + + @@ -624,113 +638,119 @@ - - - - - - - - - - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + + + + - - + + - - + + - - + + - - + + + + + - - + + + + + - - + + + + + + + + + + + - - - - - + + - - + + - - + + @@ -738,92 +758,8 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + diff --git a/docs/src/static/trait-hierarchy.svg b/docs/src/static/trait-hierarchy.svg index e1e9bc3a8..571ca9c90 100644 --- a/docs/src/static/trait-hierarchy.svg +++ b/docs/src/static/trait-hierarchy.svg @@ -1,29 +1,43 @@ - + - - - - + + + + - + - - - - - - - + + + + + + + + + + + + + + + + + + + + + @@ -33,26 +47,29 @@ - - - - + + + + - + - - - - - - - + + + + + + + + + + @@ -62,29 +79,29 @@ - - + + - + - - - - - - - - - - - - + + + + + + + + + + + + @@ -107,18 +124,17 @@ - - - - - - - - - - - - + + + + + + + + + + + @@ -164,12 +180,11 @@ - - - - - - + + + + + @@ -210,202 +225,143 @@ - - + + - + - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -415,77 +371,99 @@ - - + + - + - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + - - - - - - - - - + + + + + + + + + + + + + + - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + @@ -501,52 +479,94 @@ - + + + + + + + + + + + + + - - + + - - + + + + + - + + + + - + - - + + + + + + + + + + + + + + - + + + + + + + + + + + + + - + - + - + - + - + - + - + - + - + @@ -591,24 +611,18 @@ - - - - - - - - - + + + @@ -624,113 +638,119 @@ - - - - - - - - - - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + + + + - - + + - - + + - - + + - - + + + + + - - + + + + + - - + + + + + + + + + + + - - - - - + + - - + + - - + + @@ -738,92 +758,8 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + diff --git a/docs/src/static/trait-hierarchy.typ b/docs/src/static/trait-hierarchy.typ index 74e6691e2..c23dd50dc 100644 --- a/docs/src/static/trait-hierarchy.typ +++ b/docs/src/static/trait-hierarchy.typ @@ -24,49 +24,43 @@ spacing: (8mm, 12mm), // Problem trait (top center) - node((0.5, 0), box(width: 55mm, align(left)[ + node((0.6, 0), box(width: 55mm, align(left)[ #strong[trait Problem]\ #text(size: 8pt, fill: secondary)[ `const NAME: &str`\ - `type Metric: Clone`\ + `type Value: Clone`\ `fn dims() -> Vec`\ - `fn evaluate(&config) -> Metric`\ + `fn evaluate(&config) -> Value`\ `fn variant() -> Vec<(&str, &str)>` ] ]), fill: trait-fill, corner-radius: 6pt, inset: 10pt, name: ), - // OptimizationProblem trait (bottom left) + // Aggregate trait (bottom left) node((0, 1), box(width: 55mm, align(left)[ - #strong[trait OptimizationProblem]\ + #strong[trait Aggregate]\ #text(size: 8pt, fill: secondary)[ - `type Value: PartialOrd + Clone`\ - `fn direction() -> Direction`\ - #text(style: "italic")[requires `Metric = SolutionSize`] - - #strong[SolutionSize\]\ - #text(size: 8pt, fill: secondary)[`Valid(T) | Invalid`] - - #strong[Direction]\ - #text(size: 8pt, fill: secondary)[`Maximize | Minimize`] - + `fn identity() -> Self`\ + `fn combine(self, other) -> Self`\ + `fn supports_witnesses() -> bool`\ + `fn contributes_to_witnesses(...)` ] - ]), fill: trait-fill, corner-radius: 6pt, inset: 10pt, name: ), + ]), fill: trait-fill, corner-radius: 6pt, inset: 10pt, name: ), - // SatisfactionProblem trait (bottom right) - node((1.2, 1), box(width: 42mm, align(left)[ - #strong[trait SatisfactionProblem]\ + // Common value types (bottom right) + node((1.25, 1), box(width: 48mm, align(left)[ + #strong[Common Value Types]\ #text(size: 8pt, fill: secondary)[ - #text(style: "italic")[marker trait]\ - #text(style: "italic")[requires `Metric = bool`] + `Max | Min | Extremum`\ + `Or | Sum | And`\ + #text(style: "italic")[used as `Problem::Value`] ] - ]), fill: trait-fill, corner-radius: 6pt, inset: 10pt, name: ), + ]), fill: type-fill, corner-radius: 6pt, inset: 10pt, name: ), - // Inheritance arrows - edge(, , "->", label: text(size: 8pt)[extends], label-side: left, label-fill: none), - edge(, , "->", label: text(size: 8pt)[extends], label-side: right, label-fill: none), + // Conceptual relationships + edge(, , "->", label: text(size: 8pt)[solver-bound on `Value`], label-side: left, label-fill: none), + edge(, , "->", label: text(size: 8pt)[implements], label-side: right, label-fill: none), ) } #let standalone-dark = sys.inputs.at("dark", default: "false") == "true" #trait-hierarchy(dark: standalone-dark) - diff --git a/problemreductions-cli/src/mcp/tools.rs b/problemreductions-cli/src/mcp/tools.rs index 8454efbbe..b53f3d6dd 100644 --- a/problemreductions-cli/src/mcp/tools.rs +++ b/problemreductions-cli/src/mcp/tools.rs @@ -41,7 +41,7 @@ pub struct NeighborsParams { pub problem: String, #[schemars(description = "Number of hops to explore (default: 1)")] pub hops: Option, - #[schemars(description = "ExtremumSense: out (default), in, or both")] + #[schemars(description = "Traversal direction: out (default), in, or both")] pub direction: Option, } From 76ca8e4c06b46f197780eadca7cfe89ac9f50f9d Mon Sep 17 00:00:00 2001 From: GiggleLiu Date: Mon, 23 Mar 2026 18:43:56 +0800 Subject: [PATCH 14/18] fix: remove legacy Valid/Invalid formatting, update stale module descriptions Replace legacy Valid(...)/Invalid display strings with direct aggregate wrapper names (Max, Min, Or, Sum). Update export_module_graph.rs to reflect the new trait hierarchy. Add doc comment on EdgeCapabilities default. Co-Authored-By: Claude Opus 4.6 (1M context) --- .claude/CLAUDE.md | 2 +- docs/src/cli.md | 10 +++--- docs/src/getting-started.md | 4 +-- examples/export_module_graph.rs | 37 ++++++++++++----------- problemreductions-cli/src/test_support.rs | 4 +-- problemreductions-cli/tests/cli_tests.rs | 2 +- src/registry/dyn_problem.rs | 26 ++++------------ src/rules/registry.rs | 2 ++ src/unit_tests/example_db.rs | 33 +++++++++++--------- src/unit_tests/models/algebraic/bmf.rs | 2 +- src/unit_tests/registry/dispatch.rs | 12 ++++---- 11 files changed, 64 insertions(+), 70 deletions(-) diff --git a/.claude/CLAUDE.md b/.claude/CLAUDE.md index 3212b9fbd..f90fd11b2 100644 --- a/.claude/CLAUDE.md +++ b/.claude/CLAUDE.md @@ -130,7 +130,7 @@ Max, Min, Sum, Or, And, Extremum, ExtremumSense - `Solver::solve()` computes the aggregate value for any `Problem` whose `Value` implements `Aggregate` - `BruteForce::find_witness()` / `find_all_witnesses()` recover witnesses only when `P::Value::supports_witnesses()` - `ReductionResult` provides `target_problem()` and `extract_solution()` for witness/config workflows; `AggregateReductionResult` provides `extract_value()` for aggregate/value workflows -- CLI-facing dynamic formatting preserves legacy `Valid(...)` / `Invalid` strings for `Max` / `Min` aggregates; other aggregates keep their wrapper names (for example `Or(true)` or `Sum(56)`) +- 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), `i32`, `f64` — all implement `WeightElement` trait - `WeightElement` trait: `type Sum: NumericSize` + `fn to_sum(&self)` — converts weight to a summable numeric type diff --git a/docs/src/cli.md b/docs/src/cli.md index 452532cf9..129ec7c52 100644 --- a/docs/src/cli.md +++ b/docs/src/cli.md @@ -85,7 +85,7 @@ pred solve problem.json --solver brute-force # LengthBoundedDisjointPaths currently needs brute-force pred solve lbdp.json --solver brute-force -# Evaluate a specific configuration (shows Valid(N) or Invalid) +# Evaluate a specific configuration (shows the aggregate value, e.g. Max(2) or Min(None)) pred evaluate problem.json --config 1,0,1,0 # Reduce to another problem type and solve via brute-force @@ -442,7 +442,7 @@ Evaluate a configuration against a problem instance: ```bash $ pred evaluate problem.json --config 1,0,1,0 -Valid(2) +Max(2) ``` Stdin is supported with `-`: @@ -527,7 +527,7 @@ $ pred solve problem.json Problem: MaximumIndependentSet (reduced to ILP) Solver: ilp Solution: [1, 0, 0, 1] -Evaluation: Valid(2) +Evaluation: Max(2) ``` Solve a reduction bundle (from `pred reduce`): @@ -537,9 +537,9 @@ $ pred solve reduced.json --solver brute-force Source: MaximumIndependentSet Target: QUBO (solved with brute-force) Target solution: [0, 1, 0, 1] -Target evaluation: Valid(-2.0) +Target evaluation: Min(-2.0) Source solution: [0, 1, 0, 1] -Source evaluation: Valid(2) +Source evaluation: Max(2) ``` > **Note:** The ILP solver requires a reduction path from the target problem to ILP. diff --git a/docs/src/getting-started.md b/docs/src/getting-started.md index 2cae2206a..39d691ebf 100644 --- a/docs/src/getting-started.md +++ b/docs/src/getting-started.md @@ -91,12 +91,12 @@ configuration space. ```rust,ignore let solution = reduction.extract_solution(&ilp_solution); let metric = problem.evaluate(&solution); -println!("Packing solution: {:?} -> size {:?}", solution, metric); +println!("Packing solution: {:?} -> size {}", solution, metric); assert!(metric.is_valid()); ``` ```text -Packing solution: [1, 0, 1, 1] -> size Valid(3) +Packing solution: [1, 0, 1, 1] -> size Max(3) ``` For convenience, `ILPSolver::solve_reduced` combines reduce + solve + extract diff --git a/examples/export_module_graph.rs b/examples/export_module_graph.rs index 6245dc211..f1959d21a 100644 --- a/examples/export_module_graph.rs +++ b/examples/export_module_graph.rs @@ -116,28 +116,19 @@ fn main() { "trait", "Core trait for all computational problems", ), - ( - "OptimizationProblem", - "trait", - "Extension trait for optimization problems", - ), - ( - "SatisfactionProblem", - "trait", - "Marker trait for satisfaction problems", - ), ], ), ( "types", "core", &[ - ( - "SolutionSize", - "enum", - "Metric for optimization: Valid(T) or Invalid", - ), - ("Direction", "enum", "Maximize or Minimize"), + ("Aggregate", "trait", "Trait for aggregate value types"), + ("Max", "struct", "Maximum aggregate wrapper"), + ("Min", "struct", "Minimum aggregate wrapper"), + ("Sum", "struct", "Summation aggregate wrapper"), + ("Or", "struct", "Existential (logical or) aggregate"), + ("And", "struct", "Universal (logical and) aggregate"), + ("Extremum", "struct", "Runtime max/min aggregate"), ("One", "struct", "Unit weight marker type"), ("WeightElement", "trait", "Trait for weight types"), ], @@ -172,13 +163,23 @@ fn main() { ( "ReduceTo", "trait", - "Trait for reducing one problem to another", + "Trait for witness/config reductions", ), ( "ReductionResult", "trait", "Result of a reduction with solution extraction", ), + ( + "ReduceToAggregate", + "trait", + "Trait for aggregate/value reductions", + ), + ( + "AggregateReductionResult", + "trait", + "Result of a reduction with value extraction", + ), ], ), ( @@ -203,7 +204,7 @@ fn main() { ( "Solver", "trait", - "Solver trait for optimization and satisfaction", + "Solver trait for aggregate value computation", ), ], ), diff --git a/problemreductions-cli/src/test_support.rs b/problemreductions-cli/src/test_support.rs index 2a5b971ac..81d3d33c8 100644 --- a/problemreductions-cli/src/test_support.rs +++ b/problemreductions-cli/src/test_support.rs @@ -98,7 +98,7 @@ impl AggregateReductionResult for AggregateValueToIlpReduction { fn solve_value

(any: &dyn Any) -> String where P: Problem + Serialize + 'static, - P::Value: problemreductions::types::Aggregate, + P::Value: problemreductions::types::Aggregate + std::fmt::Display, { let problem = any .downcast_ref::

() @@ -110,7 +110,7 @@ where fn solve_witness

(any: &dyn Any) -> Option<(Vec, String)> where P: Problem + Serialize + 'static, - P::Value: problemreductions::types::Aggregate, + P::Value: problemreductions::types::Aggregate + std::fmt::Display, { let problem = any.downcast_ref::

()?; let solver = BruteForce::new(); diff --git a/problemreductions-cli/tests/cli_tests.rs b/problemreductions-cli/tests/cli_tests.rs index 7a95af09b..54212e5fb 100644 --- a/problemreductions-cli/tests/cli_tests.rs +++ b/problemreductions-cli/tests/cli_tests.rs @@ -8226,7 +8226,7 @@ fn test_create_weighted_mis_round_trips_into_solve() { ); let stdout = String::from_utf8(solve_output.stdout).unwrap(); let json: serde_json::Value = serde_json::from_str(&stdout).unwrap(); - assert_eq!(json["evaluation"], "Valid(5)"); + assert_eq!(json["evaluation"], "Max(5)"); } #[test] diff --git a/src/registry/dyn_problem.rs b/src/registry/dyn_problem.rs index 00faf39f3..19483463a 100644 --- a/src/registry/dyn_problem.rs +++ b/src/registry/dyn_problem.rs @@ -8,28 +8,14 @@ use crate::traits::Problem; /// Format a metric for CLI- and registry-facing dynamic dispatch. /// -/// Optimization aggregates migrated from `Valid/Invalid` to `Max/Min`, but the -/// dynamic CLI surface still exposes the legacy `Valid(...)` / `Invalid` -/// strings. Preserve that presentation here so higher layers do not leak the -/// aggregate internals. +/// Dynamic formatting uses the aggregate display form directly, so optimization +/// metrics appear as `Max(...)` / `Min(...)` alongside aggregate-only values +/// such as `Or(true)` or `Sum(56)`. pub fn format_metric(metric: &T) -> String where - T: fmt::Debug + Serialize, + T: fmt::Display, { - let debug = format!("{metric:?}"); - - match debug.as_str() { - "Max(None)" | "Min(None)" => return "Invalid".to_string(), - _ => {} - } - - if debug.starts_with("Max(Some(") || debug.starts_with("Min(Some(") { - let value = serde_json::to_value(metric).expect("serialize metric failed"); - let inner = serde_json::to_string(&value).expect("serialize metric inner value failed"); - return format!("Valid({inner})"); - } - - debug + metric.to_string() } /// Type-erased problem interface for dynamic dispatch. @@ -57,7 +43,7 @@ pub trait DynProblem: Any { impl DynProblem for T where T: Problem + Serialize + 'static, - T::Value: fmt::Debug + Serialize, + T::Value: fmt::Display + Serialize, { fn evaluate_dyn(&self, config: &[usize]) -> String { format_metric(&self.evaluate(config)) diff --git a/src/rules/registry.rs b/src/rules/registry.rs index 55043e2b3..00bd892a7 100644 --- a/src/rules/registry.rs +++ b/src/rules/registry.rs @@ -119,6 +119,8 @@ impl EdgeCapabilities { } } +/// Defaults to `witness_only()` — the conservative choice for edges registered +/// via `#[reduction]`, which are witness/config reductions. impl Default for EdgeCapabilities { fn default() -> Self { Self::witness_only() diff --git a/src/unit_tests/example_db.rs b/src/unit_tests/example_db.rs index 6a3036fdb..34c34e45e 100644 --- a/src/unit_tests/example_db.rs +++ b/src/unit_tests/example_db.rs @@ -553,28 +553,33 @@ fn rule_specs_solution_pairs_are_consistent() { pair.target_config.len(), target.dims_dyn().len() ); - // Verify configs produce non-Invalid / non-false evaluations + // Verify configs produce feasible witness-capable evaluations. + let source_eval = source.evaluate_dyn(&pair.source_config); + let target_eval = target.evaluate_dyn(&pair.target_config); let source_val = source.evaluate_json(&pair.source_config); - let target_val = target.evaluate_json(&pair.target_config); assert_ne!( - source_val, - serde_json::json!("Invalid"), - "Rule {label}: source_config evaluates to Invalid" + source_eval, "Max(None)", + "Rule {label}: source_config evaluates to Max(None)" ); assert_ne!( - target_val, - serde_json::json!("Invalid"), - "Rule {label}: target_config evaluates to Invalid" + source_eval, "Min(None)", + "Rule {label}: source_config evaluates to Min(None)" ); assert_ne!( - source_val, - serde_json::json!(false), - "Rule {label}: source_config evaluates to false" + source_eval, "Or(false)", + "Rule {label}: source_config evaluates to Or(false)" ); assert_ne!( - target_val, - serde_json::json!(false), - "Rule {label}: target_config evaluates to false" + 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)" ); // Round-trip: extract_solution(target_config) must produce a valid // source config with the same evaluation value diff --git a/src/unit_tests/models/algebraic/bmf.rs b/src/unit_tests/models/algebraic/bmf.rs index edcd60a43..30542d52a 100644 --- a/src/unit_tests/models/algebraic/bmf.rs +++ b/src/unit_tests/models/algebraic/bmf.rs @@ -220,7 +220,7 @@ fn test_jl_parity_evaluation() { let config = jl_parse_config(&eval["config"]); let result = problem.evaluate(&config); let jl_size = eval["size"].as_i64().unwrap() as i32; - // BMF always returns Valid(hamming_distance) + // BMF always returns Min(hamming_distance). assert_eq!( result, Min(Some(jl_size)), diff --git a/src/unit_tests/registry/dispatch.rs b/src/unit_tests/registry/dispatch.rs index 45ef05151..5737b07e9 100644 --- a/src/unit_tests/registry/dispatch.rs +++ b/src/unit_tests/registry/dispatch.rs @@ -73,12 +73,12 @@ fn test_dyn_problem_blanket_impl_exposes_problem_metadata() { } #[test] -fn test_dyn_problem_formats_optimization_values_as_legacy_valid_invalid() { +fn test_dyn_problem_formats_optimization_values_as_max_min() { let problem = MaximumIndependentSet::new(SimpleGraph::new(3, vec![(0, 1)]), vec![1i32; 3]); let dyn_problem: &dyn DynProblem = &problem; - assert_eq!(dyn_problem.evaluate_dyn(&[1, 0, 1]), "Valid(2)"); - assert_eq!(dyn_problem.evaluate_dyn(&[1, 1, 0]), "Invalid"); + assert_eq!(dyn_problem.evaluate_dyn(&[1, 0, 1]), "Max(2)"); + assert_eq!(dyn_problem.evaluate_dyn(&[1, 1, 0]), "Max(None)"); } #[test] @@ -113,7 +113,7 @@ fn loaded_dyn_problem_returns_none_for_aggregate_only_witness() { } #[test] -fn test_load_dyn_formats_optimization_solve_values_as_legacy_valid_invalid() { +fn test_load_dyn_formats_optimization_solve_values_as_max_min() { let problem = MinimumVertexCover::new(SimpleGraph::new(3, vec![(0, 1), (1, 2)]), vec![1i32; 3]); let variant = BTreeMap::from([ ("graph".to_string(), "SimpleGraph".to_string()), @@ -126,9 +126,9 @@ fn test_load_dyn_formats_optimization_solve_values_as_legacy_valid_invalid() { ) .unwrap(); - assert_eq!(loaded.solve_brute_force_value(), "Valid(1)"); + assert_eq!(loaded.solve_brute_force_value(), "Min(1)"); let solved = loaded.solve_brute_force_witness().unwrap(); - assert_eq!(solved.1, "Valid(1)"); + assert_eq!(solved.1, "Min(1)"); } #[test] From ca67f55fad29c385d380820978e83c5d1ff59068 Mon Sep 17 00:00:00 2001 From: GiggleLiu Date: Mon, 23 Mar 2026 19:04:00 +0800 Subject: [PATCH 15/18] test: improve coverage for aggregate infrastructure Add tests for: - SolveViaReductionError Display/Error impls - ILPSolver::solve_dyn, supports_direct_dyn, try_solve_via_reduction - solve_via_reduction with nonexistent problem - format_metric direct usage - LoadedDynProblem backward-compat solve and Debug - AggregateReductionChain single-step path rejection - Aggregate chain with witness-only edge returns None Re-export SolveViaReductionError from ilp module. Co-Authored-By: Claude Opus 4.6 (1M context) --- src/solvers/ilp/mod.rs | 1 + src/unit_tests/registry/dispatch.rs | 50 +++++++++++ src/unit_tests/rules/graph.rs | 59 +++++++++++++ src/unit_tests/solvers/ilp/solver.rs | 124 +++++++++++++++++++++++++++ 4 files changed, 234 insertions(+) diff --git a/src/solvers/ilp/mod.rs b/src/solvers/ilp/mod.rs index f23f70ff2..b09109814 100644 --- a/src/solvers/ilp/mod.rs +++ b/src/solvers/ilp/mod.rs @@ -24,3 +24,4 @@ mod solver; pub use solver::ILPSolver; +pub use solver::SolveViaReductionError; diff --git a/src/unit_tests/registry/dispatch.rs b/src/unit_tests/registry/dispatch.rs index 5737b07e9..471d6f957 100644 --- a/src/unit_tests/registry/dispatch.rs +++ b/src/unit_tests/registry/dispatch.rs @@ -217,3 +217,53 @@ fn test_serialize_any_rejects_partial_variant() { let partial = BTreeMap::from([("graph".to_string(), "SimpleGraph".to_string())]); assert!(serialize_any("MaximumIndependentSet", &partial, &problem as &dyn Any).is_none()); } + +#[test] +fn test_format_metric_uses_display() { + use crate::registry::dyn_problem::format_metric; + use crate::types::{Max, Min, Or}; + assert_eq!(format_metric(&Max(Some(42))), "Max(42)"); + 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] +fn test_loaded_dyn_problem_backward_compat_solve() { + let problem = MaximumIndependentSet::new(SimpleGraph::new(3, vec![(0, 1)]), vec![1i32; 3]); + let variant = BTreeMap::from([ + ("graph".to_string(), "SimpleGraph".to_string()), + ("weight".to_string(), "i32".to_string()), + ]); + let loaded = load_dyn( + "MaximumIndependentSet", + &variant, + serde_json::to_value(&problem).unwrap(), + ) + .unwrap(); + // solve_brute_force() is the backward-compatible alias for solve_brute_force_witness() + let result = loaded.solve_brute_force(); + assert!(result.is_some()); + let (config, eval) = result.unwrap(); + assert!(!config.is_empty()); + assert!(eval.starts_with("Max(")); +} + +#[test] +fn test_loaded_dyn_problem_debug() { + let problem = MaximumIndependentSet::new(SimpleGraph::new(3, vec![(0, 1)]), vec![1i32; 3]); + let variant = BTreeMap::from([ + ("graph".to_string(), "SimpleGraph".to_string()), + ("weight".to_string(), "i32".to_string()), + ]); + let loaded = load_dyn( + "MaximumIndependentSet", + &variant, + serde_json::to_value(&problem).unwrap(), + ) + .unwrap(); + let debug = format!("{:?}", loaded); + assert!(debug.contains("LoadedDynProblem")); + assert!(debug.contains("MaximumIndependentSet")); +} diff --git a/src/unit_tests/rules/graph.rs b/src/unit_tests/rules/graph.rs index 9687a19ec..ee1fb0933 100644 --- a/src/unit_tests/rules/graph.rs +++ b/src/unit_tests/rules/graph.rs @@ -455,6 +455,65 @@ fn natural_edge_supports_both_modes() { assert_eq!(chain.extract_value_dyn(json!(7)), json!(7)); } +#[test] +fn reduce_aggregate_along_path_rejects_single_step_path() { + let source_variant = BTreeMap::new(); + let graph = build_two_node_graph( + AggregateChainSource::NAME, + source_variant.clone(), + AggregateChainMiddle::NAME, + BTreeMap::new(), + ReductionEdgeData { + overhead: crate::rules::registry::ReductionOverhead::default(), + reduce_fn: None, + reduce_aggregate_fn: Some(reduce_source_to_middle_aggregate), + capabilities: EdgeCapabilities::aggregate_only(), + }, + ); + let single_step_path = ReductionPath { + steps: vec![ReductionStep { + name: AggregateChainSource::NAME.to_string(), + variant: source_variant, + }], + }; + assert!(graph + .reduce_aggregate_along_path(&single_step_path, &AggregateChainSource as &dyn Any) + .is_none()); +} + +#[test] +fn reduce_aggregate_returns_none_for_witness_only_edge() { + let source_variant = BTreeMap::new(); + let target_variant = BTreeMap::new(); + let graph = build_two_node_graph( + AggregateChainSource::NAME, + source_variant.clone(), + AggregateChainMiddle::NAME, + target_variant.clone(), + ReductionEdgeData { + overhead: crate::rules::registry::ReductionOverhead::default(), + reduce_fn: Some(reduce_source_to_middle_witness), + reduce_aggregate_fn: None, + capabilities: EdgeCapabilities::witness_only(), + }, + ); + let path = ReductionPath { + steps: vec![ + ReductionStep { + name: AggregateChainSource::NAME.to_string(), + variant: source_variant, + }, + ReductionStep { + name: AggregateChainMiddle::NAME.to_string(), + variant: target_variant, + }, + ], + }; + assert!(graph + .reduce_aggregate_along_path(&path, &AggregateChainSource as &dyn Any) + .is_none()); +} + #[test] fn test_find_indirect_path() { let graph = ReductionGraph::new(); diff --git a/src/unit_tests/solvers/ilp/solver.rs b/src/unit_tests/solvers/ilp/solver.rs index 7d395969e..02f527ede 100644 --- a/src/unit_tests/solvers/ilp/solver.rs +++ b/src/unit_tests/solvers/ilp/solver.rs @@ -251,3 +251,127 @@ fn test_ilp_with_time_limit() { let solution = solver.solve(&ilp); assert!(solution.is_some()); } + +#[test] +fn test_ilp_solve_via_reduction_success() { + use crate::models::graph::MaximumIndependentSet; + use crate::topology::SimpleGraph; + use std::collections::BTreeMap; + + let solver = ILPSolver::new(); + let problem = MaximumIndependentSet::new(SimpleGraph::new(3, vec![(0, 1)]), vec![1i32; 3]); + let variant = BTreeMap::from([ + ("graph".to_string(), "SimpleGraph".to_string()), + ("weight".to_string(), "i32".to_string()), + ]); + let result = solver.try_solve_via_reduction("MaximumIndependentSet", &variant, &problem); + assert!(result.is_ok()); + let sol = result.unwrap(); + let eval = problem.evaluate(&sol); + assert!(eval.is_valid()); +} + +#[test] +fn test_ilp_solve_via_reduction_no_path() { + use std::collections::BTreeMap; + + // Use a problem name that doesn't exist in the graph + let solver = ILPSolver::new(); + let ilp = ILP::::new( + 2, + vec![LinearConstraint::le(vec![(0, 1.0), (1, 1.0)], 1.0)], + vec![(0, 1.0)], + ObjectiveSense::Maximize, + ); + // solve_via_reduction on an ILP itself should succeed directly + let result = solver.try_solve_via_reduction( + "ILP", + &BTreeMap::from([("type".to_string(), "bool".to_string())]), + &ilp, + ); + assert!(result.is_ok()); +} + +#[test] +fn test_ilp_solve_dyn_bool() { + let solver = ILPSolver::new(); + let ilp = ILP::::new( + 2, + vec![LinearConstraint::le(vec![(0, 1.0), (1, 1.0)], 1.0)], + vec![(0, 1.0), (1, 2.0)], + ObjectiveSense::Maximize, + ); + let result = solver.solve_dyn(&ilp as &dyn std::any::Any); + assert!(result.is_some()); +} + +#[test] +fn test_ilp_solve_dyn_i32() { + let solver = ILPSolver::new(); + let ilp = ILP::::new( + 2, + vec![LinearConstraint::le(vec![(0, 1.0)], 3.0)], + vec![(0, 1.0), (1, 1.0)], + ObjectiveSense::Maximize, + ); + let result = solver.solve_dyn(&ilp as &dyn std::any::Any); + assert!(result.is_some()); +} + +#[test] +fn test_ilp_solve_dyn_unknown_type_returns_none() { + let solver = ILPSolver::new(); + let not_ilp: i32 = 42; + let result = solver.solve_dyn(¬_ilp as &dyn std::any::Any); + assert!(result.is_none()); +} + +#[test] +fn test_ilp_supports_direct_dyn() { + let solver = ILPSolver::new(); + let ilp_bool = ILP::::empty(); + let ilp_i32 = ILP::::new(1, vec![], vec![], ObjectiveSense::Maximize); + let not_ilp: i32 = 42; + + assert!(solver.supports_direct_dyn(&ilp_bool as &dyn std::any::Any)); + assert!(solver.supports_direct_dyn(&ilp_i32 as &dyn std::any::Any)); + assert!(!solver.supports_direct_dyn(¬_ilp as &dyn std::any::Any)); +} + +#[test] +fn test_solve_via_reduction_error_display() { + use crate::solvers::ilp::SolveViaReductionError; + + let err = SolveViaReductionError::WitnessPathRequired { + name: "Foo".to_string(), + }; + assert!(err.to_string().contains("witness-capable")); + assert!(err.to_string().contains("Foo")); + + let err = SolveViaReductionError::NoReductionPath { + name: "Bar".to_string(), + }; + assert!(err.to_string().contains("No reduction path")); + assert!(err.to_string().contains("Bar")); + + let err = SolveViaReductionError::NoSolution { + name: "Baz".to_string(), + }; + assert!(err.to_string().contains("no solution")); + assert!(err.to_string().contains("Baz")); + + // std::error::Error is implemented + let _: &dyn std::error::Error = &err; +} + +#[test] +fn test_solve_via_reduction_returns_none_for_no_path() { + let solver = ILPSolver::new(); + let not_ilp: i32 = 42; + let result = solver.solve_via_reduction( + "NonexistentProblem", + &std::collections::BTreeMap::new(), + ¬_ilp as &dyn std::any::Any, + ); + assert!(result.is_none()); +} From 0c32e70b54bf3c7ec1023eb9767e5d4ceac4eef3 Mon Sep 17 00:00:00 2001 From: GiggleLiu Date: Mon, 23 Mar 2026 19:16:24 +0800 Subject: [PATCH 16/18] fix: update CLI tests from Valid(...) to Max(...) format Three CLI tests still asserted stdout.contains("Valid") after the aggregate formatting change. Update to match the new Max(...) output. Co-Authored-By: Claude Opus 4.6 (1M context) --- problemreductions-cli/tests/cli_tests.rs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/problemreductions-cli/tests/cli_tests.rs b/problemreductions-cli/tests/cli_tests.rs index 54212e5fb..35befb32c 100644 --- a/problemreductions-cli/tests/cli_tests.rs +++ b/problemreductions-cli/tests/cli_tests.rs @@ -387,7 +387,7 @@ fn test_evaluate() { String::from_utf8_lossy(&output.stderr) ); let stdout = String::from_utf8(output.stdout).unwrap(); - assert!(stdout.contains("Valid")); + assert!(stdout.contains("Max(2)"), "stdout: {stdout}"); std::fs::remove_file(&tmp).ok(); } @@ -2235,7 +2235,7 @@ fn test_create_then_evaluate() { String::from_utf8_lossy(&eval_output.stderr) ); let stdout = String::from_utf8(eval_output.stdout).unwrap(); - assert!(stdout.contains("Valid")); + assert!(stdout.contains("Max(2)"), "stdout: {stdout}"); std::fs::remove_file(&problem_file).ok(); } @@ -5756,8 +5756,8 @@ fn test_create_pipe_to_evaluate() { ); let stdout = String::from_utf8(eval_result.stdout).unwrap(); assert!( - stdout.contains("Valid"), - "stdout should contain Valid, got: {stdout}" + stdout.contains("Max("), + "stdout should contain Max(...), got: {stdout}" ); } From 3960e90027e1c8a81f6fc800ee721586f915c50f Mon Sep 17 00:00:00 2001 From: GiggleLiu Date: Mon, 23 Mar 2026 19:22:25 +0800 Subject: [PATCH 17/18] ci: add --workspace to test job for parity with coverage The Test job was running without --workspace, which could miss CLI integration test failures that the Coverage job (which uses --workspace) would catch. Co-Authored-By: Claude Opus 4.6 (1M context) --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index d05d6903f..65adf7b6d 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -37,7 +37,7 @@ jobs: run: cargo build --features ilp-highs --verbose - name: Run tests - run: cargo test --features "ilp-highs example-db" --verbose + run: cargo test --features "ilp-highs example-db" --workspace --verbose - name: Run doc tests run: cargo test --doc --features ilp-highs --verbose From 2796743f55dd1b3836152bac98613f06934bf07d Mon Sep 17 00:00:00 2001 From: GiggleLiu Date: Mon, 23 Mar 2026 19:30:43 +0800 Subject: [PATCH 18/18] clean up implementation plan --- ...03-13-quantified-boolean-formulas-model.md | 99 ---- ...-generalized-aggregation-implementation.md | 452 ------------------ 2 files changed, 551 deletions(-) delete mode 100644 docs/plans/2026-03-13-quantified-boolean-formulas-model.md delete mode 100644 docs/plans/2026-03-23-generalized-aggregation-implementation.md diff --git a/docs/plans/2026-03-13-quantified-boolean-formulas-model.md b/docs/plans/2026-03-13-quantified-boolean-formulas-model.md deleted file mode 100644 index 767650d0a..000000000 --- a/docs/plans/2026-03-13-quantified-boolean-formulas-model.md +++ /dev/null @@ -1,99 +0,0 @@ -# Plan: Add QuantifiedBooleanFormulas Model - -**Issue:** #571 — [Model] QuantifiedBooleanFormulas(qbf)(*) -**Skill:** add-model - -## Information Checklist - -| # | Item | Value | -|---|------|-------| -| 1 | Problem name | `QuantifiedBooleanFormulas` | -| 2 | Mathematical definition | Given a fully quantified Boolean formula F=(Q_1 u_1)...(Q_n u_n)E where each Q_i is ∀ or ∃ and E is a CNF formula, determine whether F is true | -| 3 | Problem type | Satisfaction (Metric = bool) | -| 4 | Type parameters | None | -| 5 | Struct fields | `num_vars: usize`, `quantifiers: Vec`, `clauses: Vec` | -| 6 | Configuration space | `vec![2; num_vars]` — each variable is 0 or 1 | -| 7 | Feasibility check | A config represents a full assignment; evaluate returns true iff the formula is true under that assignment (ignoring quantifier semantics in evaluate — quantifier semantics are captured by the brute-force solver's game-tree search) | -| 8 | Objective function | `bool` — satisfied or not under the given assignment | -| 9 | Best known exact algorithm | O(2^n) brute-force game-tree evaluation (Stockmeyer & Meyer, 1973); complexity string: `"2^num_vars"` | -| 10 | Solving strategy | BruteForce works — but needs special handling: `find_satisfying` must find a *witnessing assignment* for the existential variables such that for all universal variable assignments, E is satisfied. The `evaluate()` method just checks if a single full assignment satisfies the CNF matrix E (standard SAT-like evaluation). | -| 11 | Category | `formula` | - -## Design Decisions - -### evaluate() Semantics -Following the check-issue comment's analysis, `evaluate()` will treat the config as a full assignment and check whether the CNF matrix E is satisfied. This is consistent with how the `Problem` trait works (a single config → metric). The quantifier semantics are implicit: a QBF is TRUE iff there exists an assignment to existential variables such that for ALL universal variable assignments, E evaluates to true. The brute-force solver enumerates all 2^n assignments and returns any satisfying one. - -### Quantifier Enum -Define a `Quantifier` enum with `Exists` and `ForAll` variants, serializable with serde. - -### Reusing CNFClause -Reuse the existing `CNFClause` type from `sat.rs` (1-indexed signed integers). - -## Steps - -### Step 1: Implement the model (`src/models/formula/qbf.rs`) - -1. Define `Quantifier` enum: `{ Exists, ForAll }` with `Debug, Clone, PartialEq, Eq, Serialize, Deserialize` -2. Define `QuantifiedBooleanFormulas` struct with fields: `num_vars`, `quantifiers`, `clauses` -3. Add `inventory::submit!` for `ProblemSchemaEntry` -4. Constructor: `new(num_vars, quantifiers, clauses)` with assertion that `quantifiers.len() == num_vars` -5. Getter methods: `num_vars()`, `num_clauses()`, `quantifiers()`, `clauses()` -6. Implement `Problem` trait: - - `NAME = "QuantifiedBooleanFormulas"` - - `Metric = bool` - - `dims() = vec![2; num_vars]` - - `evaluate(config)` — convert to bool assignment, check if all clauses are satisfied (same as SAT) - - `variant() = variant_params![]` -7. Implement `SatisfactionProblem` (marker trait) -8. Add `declare_variants!` with complexity `"2^num_vars"` -9. Add `is_true(&self) -> bool` method that implements proper QBF game-tree evaluation (recursive minimax) -10. Link test file: `#[cfg(test)] #[path = "../../unit_tests/models/formula/qbf.rs"] mod tests;` - -### Step 2: Register the model - -1. `src/models/formula/mod.rs` — add `pub(crate) mod qbf;` and `pub use qbf::{QuantifiedBooleanFormulas, Quantifier};` -2. `src/models/mod.rs` — add `QuantifiedBooleanFormulas, Quantifier` to the formula re-export line -3. `src/lib.rs` prelude — add `QuantifiedBooleanFormulas` to the formula prelude exports - -### Step 3: Register in CLI - -1. `problemreductions-cli/src/dispatch.rs`: - - Add import for `QuantifiedBooleanFormulas` - - Add `"QuantifiedBooleanFormulas" => deser_sat::(data)` in `load_problem()` - - Add `"QuantifiedBooleanFormulas" => try_ser::(any)` in `serialize_any_problem()` -2. `problemreductions-cli/src/problem_name.rs`: - - Add `"qbf" | "quantifiedbooleanformulas" => "QuantifiedBooleanFormulas".to_string()` in `resolve_alias()` - - Add `("QBF", "QuantifiedBooleanFormulas")` to `ALIASES` array - -### Step 4: Add CLI creation support - -1. `problemreductions-cli/src/commands/create.rs`: - - Add `"QuantifiedBooleanFormulas"` match arm: parse `--num-vars`, `--clauses`, and a new `--quantifiers` flag - - Add to `example_for()`: `"QuantifiedBooleanFormulas" => "--num-vars 3 --clauses \"1,2;-1,3\" --quantifiers \"E,A,E\""` -2. `problemreductions-cli/src/cli.rs`: - - Add `--quantifiers` flag to `CreateArgs`: `pub quantifiers: Option` - - Update `all_data_flags_empty()` to include `args.quantifiers.is_none()` - - Add QBF to "Flags by problem type" table - -### Step 5: Write unit tests (`src/unit_tests/models/formula/qbf.rs`) - -1. `test_quantifier_creation` — verify Quantifier enum -2. `test_qbf_creation` — construct instance, verify dimensions -3. `test_qbf_evaluate` — verify evaluate() on valid/invalid assignments -4. `test_qbf_is_true` — verify game-tree evaluation for known true/false instances -5. `test_qbf_solver` — verify brute-force solver finds satisfying assignments -6. `test_qbf_serialization` — round-trip serde test -7. `test_qbf_trivial` — empty formula, all-exists (reduces to SAT) - -### Step 6: Document in paper - -Add problem-def entry in `docs/paper/reductions.typ`: -- Add display name: `"QuantifiedBooleanFormulas": [Quantified Boolean Formulas (QBF)]` -- Add `#problem-def("QuantifiedBooleanFormulas")[...]` with formal definition and background - -### Step 7: Verify - -```bash -make test clippy fmt-check -``` diff --git a/docs/plans/2026-03-23-generalized-aggregation-implementation.md b/docs/plans/2026-03-23-generalized-aggregation-implementation.md deleted file mode 100644 index 248076808..000000000 --- a/docs/plans/2026-03-23-generalized-aggregation-implementation.md +++ /dev/null @@ -1,452 +0,0 @@ -# Generalized Aggregation Implementation Plan - -> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task. - -**Goal:** Replace the current optimization/satisfaction split with aggregate values, preserve witness workflows where they are meaningful, and make reduction-path search capability-aware via `EdgeCapabilities`. - -**Architecture:** Implement the refactor in layers. First add aggregate primitives and generic solver semantics, then split dynamic value solve from witness solve, then extend the reduction graph with aggregate reductions and capability-aware path search. After the infrastructure is stable, migrate the existing model/test surface mechanically from `Metric` to `Value`. - -**Tech Stack:** Rust, inventory-based registry dispatch, proc macros in `problemreductions-macros`, petgraph-based reduction graph, Cargo tests, repository `make` targets. - ---- - -### Task 1: Add aggregate core types - -**Files:** -- Modify: `src/types.rs` -- Test: `src/unit_tests/types.rs` - -**Step 1: Write the failing tests** - -Add focused tests for: - -- `Max::::identity()` and `combine()` -- `Min::::identity()` and `combine()` -- `Sum::::identity()` and `combine()` -- `Or::identity()` / `combine()` -- `And::identity()` / `combine()` -- witness defaults for `Sum` and `And` -- witness hooks for `Max`, `Min`, and `Or` - -Example test skeleton: - -```rust -#[test] -fn test_max_identity_and_combine() { - assert_eq!(Max::::identity(), Max(None)); - assert_eq!(Max(Some(7)).combine(Max(Some(3))), Max(Some(7))); -} -``` - -**Step 2: Run test to verify it fails** - -Run: `cargo test test_max_identity_and_combine --lib` -Expected: FAIL with missing `Max` / `Aggregate` definitions - -**Step 3: Write minimal implementation** - -In `src/types.rs`: - -- add `Aggregate` -- add `Max`, `Min`, `Sum`, `Or`, `And` -- add `Display` impls for user-facing formatting -- keep witness hooks on `Aggregate` with safe defaults - -**Step 4: Run tests to verify they pass** - -Run: `cargo test test_max_identity_and_combine --lib` -Expected: PASS - -Run: `cargo test test_sum_identity_and_combine --lib` -Expected: PASS - -**Step 5: Commit** - -```bash -git add src/types.rs src/unit_tests/types.rs -git commit -m "refactor: add aggregate value primitives" -``` - -### Task 2: Rewrite `Problem` and `BruteForce` around aggregate values - -**Files:** -- Modify: `src/traits.rs` -- Modify: `src/solvers/mod.rs` -- Modify: `src/solvers/brute_force.rs` -- Test: `src/unit_tests/traits.rs` -- Test: `src/unit_tests/solvers/brute_force.rs` - -**Step 1: Write the failing tests** - -Update the focused trait and solver tests to use: - -- `type Value` -- `Solver::solve()` -- `BruteForce::find_witness()` -- `BruteForce::find_all_witnesses()` - -Example solver test skeleton: - -```rust -#[test] -fn test_solver_solves_max_value() { - let solver = BruteForce::new(); - let total = solver.solve(&problem); - assert_eq!(total, Max(Some(6))); -} -``` - -**Step 2: Run test to verify it fails** - -Run: `cargo test test_solver_solves_max_value --lib` -Expected: FAIL because `Problem::Value` / `Solver::solve` do not exist yet - -**Step 3: Write minimal implementation** - -In the listed files: - -- rename `Metric` to `Value` -- remove `OptimizationProblem` / `SatisfactionProblem` -- make `Solver` expose only `solve` -- implement witness helpers in `BruteForce` using `Aggregate::contributes_to_witnesses` - -**Step 4: Run tests to verify they pass** - -Run: `cargo test test_solver_solves_max_value --lib` -Expected: PASS - -Run: `cargo test test_solver_find_witness --lib` -Expected: PASS - -**Step 5: Commit** - -```bash -git add src/traits.rs src/solvers/mod.rs src/solvers/brute_force.rs src/unit_tests/traits.rs src/unit_tests/solvers/brute_force.rs -git commit -m "refactor: unify core solving on aggregate values" -``` - -### Task 3: Split dynamic value solving from dynamic witness solving - -**Files:** -- Modify: `src/registry/dyn_problem.rs` -- Modify: `src/registry/variant.rs` -- Modify: `problemreductions-macros/src/lib.rs` -- Test: `src/unit_tests/registry/dispatch.rs` - -**Step 1: Write the failing tests** - -Add or update tests to cover: - -- `LoadedDynProblem::solve_brute_force_value()` -- `LoadedDynProblem::solve_brute_force_witness()` -- witness solve returns `None` for an aggregate-only dummy problem - -Example test skeleton: - -```rust -#[test] -fn loaded_dyn_problem_returns_none_for_aggregate_only_witness() { - let loaded = LoadedDynProblem::new(Box::new(problem), solve_value, solve_witness); - assert!(loaded.solve_brute_force_witness().is_none()); -} -``` - -**Step 2: Run test to verify it fails** - -Run: `cargo test loaded_dyn_problem_returns_none_for_aggregate_only_witness --lib` -Expected: FAIL because `solve_brute_force_value` / `solve_brute_force_witness` do not exist - -**Step 3: Write minimal implementation** - -In the listed files: - -- replace `SolveFn` with `SolveValueFn` and `SolveWitnessFn` -- store both function pointers on `VariantEntry` -- generate both closures in `declare_variants!` -- have the generated witness closure call `BruteForce::find_witness()` - -**Step 4: Run tests to verify they pass** - -Run: `cargo test loaded_dyn_problem_returns_none_for_aggregate_only_witness --lib` -Expected: PASS - -Run: `cargo test test_load_problem_alias_uses_registry_dispatch --lib` -Expected: PASS - -**Step 5: Commit** - -```bash -git add src/registry/dyn_problem.rs src/registry/variant.rs problemreductions-macros/src/lib.rs src/unit_tests/registry/dispatch.rs -git commit -m "refactor: split dynamic value solve from witness solve" -``` - -### Task 4: Add aggregate reduction traits and runtime chain support - -**Files:** -- Modify: `src/rules/traits.rs` -- Modify: `src/rules/registry.rs` -- Modify: `src/rules/graph.rs` -- Test: `src/unit_tests/rules/traits.rs` -- Test: `src/unit_tests/rules/registry.rs` -- Test: `src/unit_tests/rules/graph.rs` - -**Step 1: Write the failing tests** - -Add tests for: - -- `AggregateReductionResult::extract_value` -- type-erased `DynAggregateReductionResult` -- aggregate chain execution over a tiny dummy two-step path - -Example skeleton: - -```rust -#[test] -fn test_aggregate_reduction_chain_extracts_value_backwards() { - assert_eq!(chain.extract_value_dyn(json!(7)), json!(3)); -} -``` - -**Step 2: Run test to verify it fails** - -Run: `cargo test test_aggregate_reduction_chain_extracts_value_backwards --lib` -Expected: FAIL because aggregate reduction traits and chain types do not exist - -**Step 3: Write minimal implementation** - -In the listed files: - -- add `AggregateReductionResult` -- add `ReduceToAggregate` -- add `DynAggregateReductionResult` -- add aggregate-chain execution alongside the existing witness chain - -**Step 4: Run tests to verify they pass** - -Run: `cargo test test_aggregate_reduction_chain_extracts_value_backwards --lib` -Expected: PASS - -**Step 5: Commit** - -```bash -git add src/rules/traits.rs src/rules/registry.rs src/rules/graph.rs src/unit_tests/rules/traits.rs src/unit_tests/rules/registry.rs src/unit_tests/rules/graph.rs -git commit -m "refactor: add aggregate reduction execution" -``` - -### Task 5: Introduce `EdgeCapabilities` and capability-aware path search - -**Files:** -- Modify: `src/rules/registry.rs` -- Modify: `src/rules/graph.rs` -- Test: `src/unit_tests/reduction_graph.rs` -- Test: `src/unit_tests/rules/graph.rs` - -**Step 1: Write the failing tests** - -Add pathfinding tests that prove: - -- witness search ignores aggregate-only edges -- aggregate search ignores witness-only edges -- natural subtype edges remain usable in both modes - -Example skeleton: - -```rust -#[test] -fn witness_path_search_rejects_aggregate_only_edge() { - assert!(graph.find_cheapest_path(..., ReductionMode::Witness, ...).is_none()); -} -``` - -**Step 2: Run test to verify it fails** - -Run: `cargo test witness_path_search_rejects_aggregate_only_edge --lib` -Expected: FAIL because `EdgeCapabilities` / `ReductionMode` do not exist - -**Step 3: Write minimal implementation** - -In the listed files: - -- add `EdgeCapabilities` -- store capabilities on `ReductionEntry` and edge data -- thread `ReductionMode` through path search and path execution -- mark natural subtype edges as `{ witness: true, aggregate: true }` - -**Step 4: Run tests to verify they pass** - -Run: `cargo test witness_path_search_rejects_aggregate_only_edge --lib` -Expected: PASS - -Run: `cargo test natural_edge_supports_both_modes --lib` -Expected: PASS - -**Step 5: Commit** - -```bash -git add src/rules/registry.rs src/rules/graph.rs src/unit_tests/reduction_graph.rs src/unit_tests/rules/graph.rs -git commit -m "refactor: make reduction paths capability-aware" -``` - -### Task 6: Update CLI solve/reduce flows and ILP gating - -**Files:** -- Modify: `problemreductions-cli/src/dispatch.rs` -- Modify: `problemreductions-cli/src/commands/solve.rs` -- Modify: `problemreductions-cli/src/commands/reduce.rs` -- Modify: `problemreductions-cli/src/mcp/tools.rs` -- Modify: `src/solvers/ilp/solver.rs` -- Test: `problemreductions-cli/tests/cli_tests.rs` -- Test: `problemreductions-cli/src/mcp/tests.rs` - -**Step 1: Write the failing tests** - -Add tests for: - -- plain `pred solve` on a value-only problem prints evaluation without `Solution:` -- bundle solve rejects aggregate-only paths -- ILP solve rejects aggregate-only source problems with a clear error - -Example CLI assertion skeleton: - -```rust -assert!(stdout.contains("Evaluation: Sum(")); -assert!(!stdout.contains("Solution:")); -``` - -**Step 2: Run test to verify it fails** - -Run: `cargo test test_solve_value_only_problem_omits_solution --package problemreductions-cli` -Expected: FAIL because CLI still assumes every solve returns a config - -**Step 3: Write minimal implementation** - -In the listed files: - -- use `solve_brute_force_value()` for plain problem solves -- print `Solution` only when `solve_brute_force_witness()` succeeds -- make `pred reduce` / bundle solve witness-only -- keep ILP witness-only and improve the user-facing error - -**Step 4: Run tests to verify they pass** - -Run: `cargo test test_solve_value_only_problem_omits_solution --package problemreductions-cli` -Expected: PASS - -Run: `cargo test test_solve_bundle --package problemreductions-cli` -Expected: PASS - -**Step 5: Commit** - -```bash -git add problemreductions-cli/src/dispatch.rs problemreductions-cli/src/commands/solve.rs problemreductions-cli/src/commands/reduce.rs problemreductions-cli/src/mcp/tools.rs src/solvers/ilp/solver.rs problemreductions-cli/tests/cli_tests.rs problemreductions-cli/src/mcp/tests.rs -git commit -m "refactor: separate value solve from witness workflows" -``` - -### Task 7: Mechanically migrate existing models, rules, and tests - -**Files:** -- Modify: files returned by `rg -l 'type Metric|OptimizationProblem|SatisfactionProblem|SolutionSize|Direction|find_best|find_satisfying|find_all_best|find_all_satisfying' src tests problemreductions-cli` - -**Step 1: Write the failing tests** - -Pick one optimization file and one satisfaction file from the `rg` output first and update their adjacent tests before doing the bulk migration. - -Suggested first pair: - -- `src/models/graph/maximum_independent_set.rs` -- `src/models/formula/sat.rs` - -Update their direct test files first: - -- `src/unit_tests/models/graph/maximum_independent_set.rs` -- `src/unit_tests/models/formula/sat.rs` - -**Step 2: Run test to verify it fails** - -Run: `cargo test maximum_independent_set --lib` -Expected: FAIL until the model/test pair is migrated - -Run: `cargo test formula::sat --lib` -Expected: FAIL until the model/test pair is migrated - -**Step 3: Write minimal implementation** - -For each migrated file: - -- `type Metric = ...` -> `type Value = ...` -- `SolutionSize::Valid(x)` -> `Max(Some(x))` or `Min(Some(x))` -- `bool` satisfaction outputs -> `Or(...)` -- `find_best` / `find_all_best` -> `find_witness` / `find_all_witnesses` where appropriate -- `find_satisfying` / `find_all_satisfying` -> same witness helpers - -Then repeat the same mechanical transformation across the remaining `rg` result set in small reviewable batches. - -**Step 4: Run tests to verify they pass** - -Run: `cargo test maximum_independent_set --lib` -Expected: PASS - -Run: `cargo test sat --lib` -Expected: PASS - -After each batch: - -Run: `make test` -Expected: PASS - -**Step 5: Commit** - -```bash -git add src tests problemreductions-cli -git commit -m "refactor: migrate models and tests to aggregate values" -``` - -### Task 8: Final verification and cleanup - -**Files:** -- Modify: any stragglers found by verification commands -- Verify: `docs/plans/2026-03-22-generalized-aggregation-design.md` - -**Step 1: Write the failing checks** - -Use grep-style sweeps to confirm the removed surface is really gone: - -```bash -rg 'OptimizationProblem|SatisfactionProblem|SolutionSize|Direction|type Metric' src tests problemreductions-cli -``` - -**Step 2: Run checks to verify they fail before cleanup** - -Run the command above before the last cleanup pass. -Expected: remaining hits identify unfinished migration work - -**Step 3: Write minimal cleanup** - -- remove the final stale references -- align docs/comments/examples with the new witness/value vocabulary -- verify graph export includes both `witness` and `aggregate` booleans - -**Step 4: Run verification to verify it passes** - -Run: - -```bash -make fmt-check -make test -make clippy -rg 'OptimizationProblem|SatisfactionProblem|SolutionSize|Direction|type Metric' src tests problemreductions-cli -``` - -Expected: - -- `make fmt-check` passes -- `make test` passes -- `make clippy` passes -- final `rg` finds no remaining production-code references - -**Step 5: Commit** - -```bash -git add src tests problemreductions-cli docs/plans/2026-03-22-generalized-aggregation-design.md -git commit -m "refactor: finish generalized aggregation migration" -```