diff --git a/.claude/CLAUDE.md b/.claude/CLAUDE.md index 438584de..22e14a99 100644 --- a/.claude/CLAUDE.md +++ b/.claude/CLAUDE.md @@ -228,22 +228,25 @@ Reduction graph nodes use variant key-value pairs from `Problem::variant()`: ### Numeric Contract -Follow the [numeric types and arithmetic standard](../docs/src/design.md#numeric-types-and-arithmetic) -for every model and reduction. `usize` is reserved for in-memory indices, -collection lengths, and brute-force dimensions; canonical problem parameters -are `u64`; signed mathematical integers use `i64`; and approximate -real values use finite `f64`. Before implementation, identify each numeric -input and domain, each computed total and result type, the largest supported -value, every range/sign-changing conversion, overflow behavior, and whether -arithmetic is exact or approximate. Use `TryFrom` at range boundaries and -checked arithmetic for derived values that may overflow. Rust construction, -serde, CLI, and MCP must enforce the same range. +Follow the [numeric contract](../docs/src/design.md#numeric-types-and-arithmetic) +for models and reductions. Identify numeric domains, stored result types, +conversion boundaries, and overflow behavior before implementation. Rust +construction, serde, CLI, and MCP must enforce the same range. Issue contributors provide the mathematical definition, domains, and constraints; implementers derive the Rust representation. Do not require issue authors to choose implementation types or add implementation-specific numeric fields to issue templates. Changes to issue templates require user approval. +### Reduction and Solver Boundary + +Follow the [arithmetic and round-trip policy](../docs/src/design.md#arithmetic): +integer rules and exact conversions preserve exact values; floating-point rules +accept ordinary `f64` rounding. + +In ILP tests, only `ILPSolveError::Infeasible` means infeasibility. Other errors +must fail with their details; successful solves must validate extracted witnesses. + ### File Naming - Reduction files: `src/rules/_.rs` (e.g., `maximumindependentset_qubo.rs`) - Model files: `src/models//.rs` — category is by input structure: `graph/` (graph input), `formula/` (boolean formula/circuit), `set/` (universe + subsets), `algebraic/` (matrix/linear system/lattice), `misc/` (other) diff --git a/docs/paper/reductions.typ b/docs/paper/reductions.typ index ee531965..fe120d24 100644 --- a/docs/paper/reductions.typ +++ b/docs/paper/reductions.typ @@ -16911,7 +16911,7 @@ The following table shows concrete target-variable counts for example instances, _Correctness._ The binary-to-integer embedding changes no mathematical expression. For bounded integer variables, every $x_i in [L_i,U_i]$ has a truncated binary representation, and every binary assignment decodes inside that interval; substitution preserves all constraints and objective values. Exact conversion preserves every stored coefficient, so it constructs the same formal linear objective and constraints over the same integer variables. - _Solution extraction._ Binary-to-integer and coefficient conversions preserve the assignment; coefficient conversion additionally checks the assignment against the source integer ILP. Binary encoding returns $x_i = L_i + sum_j w_(i j)y_(i j)$. + _Solution extraction._ Binary-to-integer and coefficient conversions preserve the assignment after the standard target-solution validation. Numerical solver accuracy is independent of the mathematical coefficient conversion. Binary encoding returns $x_i = L_i + sum_j w_(i j)y_(i j)$. ] #let hc_hp = load-example("HamiltonianCircuit", "HamiltonianPath") @@ -19075,9 +19075,9 @@ The following table shows concrete target-variable counts for example instances, ($arrow.r.double$) Given a perfect matching $M'$, form one ABCD group for every source triple. If $m_l in M'$, combine $u_l$ with the unique first-occurrence $B$, $C$, and $D$ items of coordinates $(a_l, b_l, c_l)$; otherwise combine $u_l$ with the corresponding later-occurrence dummy items. Because $r = 32 q$ prevents carries between the $r$, $r^2$, $r^3$, and $r^4$ digits, every such group sums to $T_1$, so the tagged instance has a 4-partition. For each tagged 4-set choose any two members $a_i, a_j$ and let the other two be $a_k, a_l$. Then ${w_i, w_j, u_(i j)}$ and ${w_k, w_l, u'_(i j)}$ both sum to $B$. Every pairing gadget not used this way joins one filler in a triple ${u_(i j), u'_(i j), 20 T_2}$. Hence the produced 3-Partition instance is feasible. - ($arrow.l.double$) In any feasible target solution every number lies strictly between $B / 4$ and $B / 2$, so the partition really is into triples. Modulo 4, regular numbers are congruent to 1, pairing numbers to 2, and fillers to 0. Therefore every triple is either of type $(1, 1, 2)$ or $(0, 2, 2)$. The $(0, 2, 2)$ triples identify the unused pairing gadgets, leaving a family of $(1, 1, 2)$ triples that reconstructs a 4-partition of the tagged numbers. Since $1 + 2 + 4 + 8 equiv 15 mod 16$, every recovered tagged 4-set contains exactly one former $A$-, $B$-, $C$-, and $D$-item. The carry-free base-$r$ encoding then forces each ABCD group to be either a real group (all first occurrences) or a dummy group (all later occurrences). The real groups pick exactly $q$ source triples, one for each coordinate of $W$, $X$, and $Y$, so they form a perfect 3-dimensional matching. + ($arrow.l.double$) In any feasible target solution every number lies strictly between $B / 4$ and $B / 2$, so the partition really is into triples. Modulo 4, regular numbers are congruent to 1, pairing numbers to 2, and fillers to 0. Therefore every triple is either of type $(1, 1, 2)$ or $(0, 2, 2)$. First normalize the $(0, 2, 2)$ triples as in @garey1979: if a filler shares a triple with pairing elements $p, q$, exchange $q$ with the original mate of $p$. Both have the same size, since every original pair sums to $44 T_2 + 4 = B - 20 T_2$, so both affected triples remain valid. Each exchange fixes a filler triple without disturbing a previously fixed one. After normalization, every remaining original pair occurs in two $(1, 1, 2)$ triples. Their four actual regular elements sum to $2 B - (44 T_2 + 4) = 84 T_2 + 4$, so the corresponding tagged numbers sum to $T_2$. These disjoint four-sets reconstruct a 4-partition. Since $1 + 2 + 4 + 8 equiv 15 mod 16$, every recovered tagged 4-set contains exactly one former $A$-, $B$-, $C$-, and $D$-item. The carry-free base-$r$ encoding then forces each ABCD group to be either a real group (all first occurrences) or a dummy group (all later occurrences). The real groups pick exactly $q$ source triples, one for each coordinate of $W$, $X$, and $Y$, so they form a perfect 3-dimensional matching. - _Solution extraction._ Reverse the 4-Partition $arrow.r$ 3-Partition gadget by pairing each triple containing some $u_(i j)$ with the unique triple containing the matching $u'_(i j)$. This recovers the tagged 4-set. Undo the mod-16 tags to obtain one ABCD group, discard every dummy group whose $B$, $C$, and $D$ items are not first occurrences, and read the selected source triple from the surviving $A$-item. + _Solution extraction._ Normalize filler triples by the equal-size exchanges above, maintaining each element's current group and position. Then pair the remaining triples containing original mates $u_(i j), u'_(i j)$ and collect their four actual regular elements; their indices need not equal the indices used to construct the pairing gadget. The normalization and pairing take linear time in the target element count. Undo the mod-16 tags to obtain one ABCD group, discard every dummy group whose $B$, $C$, and $D$ items are not first occurrences, and read the selected source triple from the surviving $A$-item. ] #let tdm_ilp = load-example("ThreeDimensionalMatching", "ILP") diff --git a/docs/src/design.md b/docs/src/design.md index 54ed4f47..66a794f8 100644 --- a/docs/src/design.md +++ b/docs/src/design.md @@ -93,20 +93,25 @@ SpinGlass couplings and its objective result use `i64`, while the temporary temporary calculations are also outside the contract, but numeric fields written into its target model must follow the target model's numeric format. -Weight variants are `One`, `i64`, and `f64`, with `One ⊂ i64 ⊂ f64`. -`i64 → f64` is a fallible reduction using a checked conversion in -`±(2^53-1)`, not `as f64`. +Supported weight variants are `One`, `i64`, and `f64`. ### Arithmetic -- Keep arithmetic in the declared type. Exact values use checked `i64` - operations; approximate values use finite `f64` operations. -- Constructors and reductions reject an arithmetic step that would overflow - `i64` when producing a stored field. They do not cap every magnitude at - `2^53-1`. `evaluate()` never widens, wraps, saturates, or silently - approximates. -- Do not promote an `i64` calculation to `i128`, `BigInt`, or `BigUint` to - accept a larger instance. +- Integer rules and exact type conversions preserve exact values within their + supported domains; overflow or an unsupported exact conversion is an error. + Keep integer arithmetic in its declared type: do not widen, wrap, saturate, + or approximate it to accept a larger instance. +- Floating-point rules implement mathematically equivalent transformations + using ordinary `f64` arithmetic and accept its rounding. Check overflow and + non-finite results; do not deliberately discard small nonzero coefficients. + Machine rounding may change the set of optimal solutions for some inputs. +- Preserve representation and witness-structure checks. Do not add exact + arithmetic merely to detect floating-point rounding, or reject a reduction + because a backend may struggle to solve it. + +Keep ordinary round-trip tests. When results differ, distinguish errors in +formulas, floating-point target construction, and solving. Investigate concrete +failures rather than adding defenses for every possible numerical discrepancy. ### Boundaries @@ -409,7 +414,7 @@ proved infeasibility, and `Err` reports an operational failure. | Solver | Description | |--------|-------------| | **BruteForce** | Enumerates a registered finite search space and returns an optimal or satisfying solution. Used for testing and verification. | -| **ILPSolver** | Executes a problem's registered ILP pipeline. Each pipeline terminates at `ILP` or `ILP`, which is solved by HiGHS via `good_lp`. | +| **ILPSolver** | Executes a problem's registered ILP pipeline. Each pipeline terminates at a native `ILP` with bool/i64 variables and i64/f64 coefficients, solved by the shared HiGHS adapter via `good_lp`. | ILP results are optimal or infeasible according to HiGHS numerical tolerances; zero MIP gaps do not imply mathematical exactness. Integer extraction rounds @@ -424,6 +429,36 @@ When an ILP target witness misses a source decision threshold, the solver returns `ILPSolveError::UnresolvedDecision`, not infeasibility: the witness alone cannot prove that no qualifying source solution exists. +### ILP execution boundary + +`ILPSolver::solve

() -> Result` remains the public +entry point. Registry lookup, concrete-terminal dispatch, and reduction-chain +extraction live in the orchestration layer. Integer pipelines stop at their +integer ILP instead of constructing a float-coefficient ILP as an extra step. +Explicit coefficient conversions are ordinary registered `ReduceTo` rules in +`rules/ilp_i64_ilp_f64.rs`. They preserve the formal mathematical problem within +the supported exact-conversion range and extract assignments unchanged after +standard target validation. + +The internal `HighsAdapter` borrows an `ILP` and returns its existing +`Vec` solution representation. It builds the backend model, executes it, +checks returned integer values, and validates constraints and objective +arithmetic against the original ILP. It does not inspect variant names, query +registrations, or extract solutions for source problems. Coefficient conversion +is an adapter-local capability; the public `ILPCoefficient` trait is unchanged. +The existing exact-integer transport limits and float-model tolerances remain +in effect. Validating a witness is not an independent optimality certificate; +solver-reported optimality retains its existing numerical contract. + +Adapter errors remain internal and map to the existing public `ILPSolveError` +variants. Rust return types, solver configuration, and CLI/JSON/MCP outcome +formats remain unchanged; reported reduction paths now end at native ILPs. +An unsupported integer coefficient is now reported through the existing +`InexactTransport` error at the adapter boundary instead of a cast-reduction +error. Bounds use that same existing transport error. An integer assignment +that violates the original ILP is rejected as `InvalidSolution` by the adapter, +rather than failing later during coefficient-cast extraction. + ## JSON Serialization All problem types support JSON serialization via serde: diff --git a/src/rules/closestsubstring_ilp.rs b/src/rules/closestsubstring_ilp.rs index 65416c54..46ec279a 100644 --- a/src/rules/closestsubstring_ilp.rs +++ b/src/rules/closestsubstring_ilp.rs @@ -166,11 +166,8 @@ impl ReduceTo> for ClosestSubstring { } // Tight upper bound on R: the worst-case Hamming distance over a - // length-ell window is at most ell. Added as a single-term `<=` - // constraint so the solver's bound-tightening pass (which scans for - // exactly this pattern) picks it up. Without this, R defaults to the - // full i64 domain, which severely degrades HiGHS performance even on - // tiny instances. + // length-ell window is at most ell. Restricting R to this range + // preserves every optimal solution. constraints.push(LinearConstraint::le(vec![(r_idx, 1)], ell_i64)); // Window-choice constraints: exactly one window per input string. diff --git a/src/rules/ilp_casts.rs b/src/rules/ilp_casts.rs deleted file mode 100644 index 586fe660..00000000 --- a/src/rules/ilp_casts.rs +++ /dev/null @@ -1,110 +0,0 @@ -//! Numeric variant reductions for ILP. - -use crate::models::algebraic::{Comparison, LinearConstraint, VariableDomain, ILP}; -use crate::reduction; -use crate::rules::{ReduceTo, ReductionError, ReductionResult}; -use crate::types::i64_to_exact_f64; - -#[derive(Debug, Clone)] -pub struct ReductionILPToFloat { - source: ILP, - target: ILP, -} - -impl ReductionILPToFloat { - fn new(source: &ILP) -> Result { - let convert = |coefficient: i64| { - i64_to_exact_f64(coefficient) - .map_err(ReductionError::inexact_float_conversion::, ILP>) - }; - let constraints = source - .constraints() - .iter() - .map(|constraint| { - let terms = constraint - .terms() - .iter() - .map(|&(variable, coefficient)| Ok((variable, convert(coefficient)?))) - .collect::, ReductionError>>()?; - let rhs = convert(constraint.rhs())?; - Ok(match constraint.comparison() { - Comparison::Le => LinearConstraint::le(terms, rhs), - Comparison::Ge => LinearConstraint::ge(terms, rhs), - Comparison::Eq => LinearConstraint::eq(terms, rhs), - }) - }) - .collect::, ReductionError>>()?; - let objective = source - .objective() - .iter() - .map(|&(variable, coefficient)| Ok((variable, convert(coefficient)?))) - .collect::, ReductionError>>()?; - let target = ILP::with_variables( - source.variables().to_vec(), - constraints, - objective, - source.sense(), - ) - .map_err(ReductionError::construction::, ILP>)?; - Ok(Self { - source: source.clone(), - target, - }) - } -} - -impl ReductionResult for ReductionILPToFloat { - type Source = ILP; - type Target = ILP; - - fn target_problem(&self) -> &Self::Target { - &self.target - } - - fn extract_solution( - &self, - target_solution: &::Solution, - ) -> crate::rules::ExtractionResult<::Solution> { - crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; - if !self.source.is_feasible(target_solution)? { - return Err(crate::rules::ExtractionError::invalid( - "the floating-point assignment violates the source integer ILP", - )); - } - Ok(target_solution.clone()) - } -} - -#[reduction( - transform = exact { - num_vars = "num_vars", - num_constraints = "num_constraints", - num_nonzeros = "num_nonzeros", - }, -)] -impl ReduceTo> for ILP { - type Result = ReductionILPToFloat; - - fn reduce_to(&self) -> Result { - ReductionILPToFloat::new(self) - } -} - -#[reduction( - transform = exact { - num_vars = "num_vars", - num_constraints = "num_constraints", - num_nonzeros = "num_nonzeros", - }, -)] -impl ReduceTo> for ILP { - type Result = ReductionILPToFloat; - - fn reduce_to(&self) -> Result { - ReductionILPToFloat::new(self) - } -} - -#[cfg(test)] -#[path = "../unit_tests/rules/ilp_casts.rs"] -mod tests; diff --git a/src/rules/ilp_i64_ilp_f64.rs b/src/rules/ilp_i64_ilp_f64.rs new file mode 100644 index 00000000..8098a115 --- /dev/null +++ b/src/rules/ilp_i64_ilp_f64.rs @@ -0,0 +1,86 @@ +//! Exact integer-to-floating coefficient reductions for ILP. +//! +//! Preserve variable domains and every formal linear expression by converting +//! coefficients and right-hand sides exactly within the supported numeric range. +//! Solution extraction is the identity map. Numerical backend capabilities are +//! independent of this reduction. + +use crate::models::algebraic::{Comparison, LinearConstraint, VariableDomain, ILP}; +use crate::reduction; +use crate::rules::{ReduceTo, ReductionError, VariantReductionResult}; +use crate::types::i64_to_exact_f64; + +pub type ReductionILPToFloat = VariantReductionResult, ILP>; + +fn reduce_coefficients( + source: &ILP, +) -> Result, ReductionError> { + let convert = |coefficient: i64| { + i64_to_exact_f64(coefficient) + .map_err(ReductionError::inexact_float_conversion::, ILP>) + }; + let constraints = source + .constraints() + .iter() + .map(|constraint| { + let terms = constraint + .terms() + .iter() + .map(|&(variable, coefficient)| Ok((variable, convert(coefficient)?))) + .collect::, ReductionError>>()?; + let rhs = convert(constraint.rhs())?; + Ok(match constraint.comparison() { + Comparison::Le => LinearConstraint::le(terms, rhs), + Comparison::Ge => LinearConstraint::ge(terms, rhs), + Comparison::Eq => LinearConstraint::eq(terms, rhs), + }) + }) + .collect::, ReductionError>>()?; + let objective = source + .objective() + .iter() + .map(|&(variable, coefficient)| Ok((variable, convert(coefficient)?))) + .collect::, ReductionError>>()?; + let target = ILP::with_variables( + source.variables().to_vec(), + constraints, + objective, + source.sense(), + ) + .map_err(ReductionError::construction::, ILP>)?; + Ok(VariantReductionResult::new(target)) +} + +#[reduction( + transform = exact { + num_vars = "num_vars", + num_constraints = "num_constraints", + num_nonzeros = "num_nonzeros", + }, +)] +impl ReduceTo> for ILP { + type Result = ReductionILPToFloat; + + fn reduce_to(&self) -> Result { + reduce_coefficients(self) + } +} + +#[reduction( + transform = exact { + num_vars = "num_vars", + num_constraints = "num_constraints", + num_nonzeros = "num_nonzeros", + }, +)] +impl ReduceTo> for ILP { + type Result = ReductionILPToFloat; + + fn reduce_to(&self) -> Result { + reduce_coefficients(self) + } +} + +#[cfg(test)] +#[path = "../unit_tests/rules/ilp_i64_ilp_f64.rs"] +mod tests; diff --git a/src/rules/mod.rs b/src/rules/mod.rs index 2b7d66b7..4912ee3d 100644 --- a/src/rules/mod.rs +++ b/src/rules/mod.rs @@ -43,8 +43,8 @@ pub(crate) mod hamiltoniancircuit_travelingsalesman; pub(crate) mod hamiltonianpath_degreeconstrainedspanningtree; pub(crate) mod hamiltonianpath_isomorphicspanningtree; pub(crate) mod hamiltonianpathbetweentwovertices_longestpath; -pub(crate) mod ilp_casts; pub(crate) mod ilp_i64_ilp_bool; +pub(crate) mod ilp_i64_ilp_f64; pub(crate) mod integerknapsack_ilp; pub(crate) mod kclique_balancedcompletebipartitesubgraph; pub(crate) mod kclique_conjunctivebooleanquery; diff --git a/src/rules/spinglass_qubo.rs b/src/rules/spinglass_qubo.rs index 1a6a900a..2c0c3a8f 100644 --- a/src/rules/spinglass_qubo.rs +++ b/src/rules/spinglass_qubo.rs @@ -66,7 +66,7 @@ impl ReduceTo> for QUBO { for i in 0..n { for j in i..n { let q = matrix[i][j]; - if q.abs() < 1e-10 { + if q == 0.0 { continue; } @@ -77,7 +77,7 @@ impl ReduceTo> for QUBO { // Off-diagonal: Q_ij * x_i * x_j // J_ij contribution let j_ij = q / 4.0; - if j_ij.abs() > 1e-10 { + if j_ij != 0.0 { interactions.push(((i, j), j_ij)); } // h_i and h_j contributions diff --git a/src/rules/threedimensionalmatching_threepartition.rs b/src/rules/threedimensionalmatching_threepartition.rs index 03a6ce03..434b37ac 100644 --- a/src/rules/threedimensionalmatching_threepartition.rs +++ b/src/rules/threedimensionalmatching_threepartition.rs @@ -9,7 +9,6 @@ use crate::models::misc::ThreePartition; use crate::models::set::ThreeDimensionalMatching; use crate::reduction; use crate::rules::traits::{ReduceTo, ReductionResult}; -use std::collections::HashMap; #[derive(Debug, Clone, Copy)] enum Step2Item { @@ -33,18 +32,6 @@ enum Step2Item { }, } -#[derive(Debug, Clone, Copy)] -enum PairingKind { - U, - UPrime, -} - -#[derive(Debug, Default, Clone, Copy)] -struct PairUsage { - saw_u: bool, - uprime_regulars: Option<[usize; 2]>, -} - /// Result of reducing ThreeDimensionalMatching to ThreePartition. #[derive(Debug, Clone)] pub struct ReductionThreeDimensionalMatchingToThreePartition { @@ -67,27 +54,6 @@ impl ReductionThreeDimensionalMatchingToThreePartition { self.pairing_start() + 2 * self.pair_keys.len() } - fn classify_target_element(&self, element_index: usize) -> TargetElement { - if element_index < self.num_regulars() { - return TargetElement::Regular { - step2_index: element_index, - }; - } - - if element_index < self.filler_start() { - let pairing_offset = element_index - self.pairing_start(); - let pair_index = pairing_offset / 2; - let kind = if pairing_offset.is_multiple_of(2) { - PairingKind::U - } else { - PairingKind::UPrime - }; - return TargetElement::Pairing { pair_index, kind }; - } - - TargetElement::Filler - } - fn decode_real_group(&self, step2_group: [usize; 4]) -> Option { let mut a_item = None; let mut b_item = None; @@ -143,6 +109,8 @@ impl ReductionThreeDimensionalMatchingToThreePartition { #[cfg(test)] fn build_target_witness(&self, source_solution: &[usize]) -> Vec { + use std::collections::HashMap; + let mut a_indices = vec![0usize; self.num_source_triples]; let mut first_b_by_w = HashMap::new(); let mut first_c_by_x = HashMap::new(); @@ -298,98 +266,62 @@ impl ReductionResult for ReductionThreeDimensionalMatchingToThreePartition { &self, target_solution: &::Solution, ) -> crate::rules::ExtractionResult<::Solution> { - crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; - - Ok({ - let mut groups = vec![Vec::new(); self.target.num_groups()]; - for (element_index, &group_index) in target_solution.iter().enumerate() { - groups[group_index].push(element_index); - } - - let mut pair_usage: HashMap<(usize, usize), PairUsage> = HashMap::new(); - - for members in groups.into_iter().filter(|members| !members.is_empty()) { - let mut regulars = Vec::new(); - let mut pairing = None; - let mut has_filler = false; - - for element_index in members { - match self.classify_target_element(element_index) { - TargetElement::Regular { step2_index } => regulars.push(step2_index), - TargetElement::Pairing { pair_index, kind } => { - pairing = Some((pair_index, kind)) - } - TargetElement::Filler => has_filler = true, - } - } - - if has_filler || regulars.len() != 2 { - continue; - } + if !crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)? + .0 + { + return Err(crate::rules::ExtractionError::invalid( + "the target assignment is not a valid 3-partition", + )); + } - let Some((pair_index, kind)) = pairing else { - continue; - }; + let mut groups = vec![Vec::with_capacity(3); self.target.num_groups()]; + let mut positions = Vec::with_capacity(target_solution.len()); + for (element, &group) in target_solution.iter().enumerate() { + positions.push((group, groups[group].len())); + groups[group].push(element); + } - let pair_key = self.pair_keys[pair_index]; - let regular_pair = sorted_pair(regulars[0], regulars[1]); - let usage = pair_usage.entry(pair_key).or_default(); + // Garey--Johnson's reverse construction first normalizes filler triples. + // Each has two pairing elements whose sum equals that of an original + // U/UPrime pair. Exchange the second element with the first's mate: + // they have equal sizes, so both affected triples remain valid. A mate + // cannot belong to an already normalized filler triple unless it is + // already here, so each iteration permanently normalizes one triple. + // Initial index order puts regulars first and fillers last; exchanges + // only move pairing elements, preserving those positions. + let pairing_start = self.pairing_start(); + for filler in self.filler_start()..target_solution.len() { + let (group, _) = positions[filler]; + let first = groups[group][0]; + let second = groups[group][1]; + let mate = pairing_start + ((first - pairing_start) ^ 1); + let (mate_group, mate_slot) = positions[mate]; + groups[group][1] = mate; + groups[mate_group][mate_slot] = second; + positions[mate] = (group, 1); + positions[second] = (mate_group, mate_slot); + } - match kind { - PairingKind::U => { - if regular_pair == [pair_key.0, pair_key.1] { - usage.saw_u = true; - } - } - PairingKind::UPrime => { - usage.uprime_regulars = Some(regular_pair); - } - } + let mut source_solution = vec![false; self.num_source_triples]; + for first in (pairing_start..self.filler_start()).step_by(2) { + let (left, _) = positions[first]; + if groups[left][0] >= pairing_start { + continue; // This complete pair is used by a filler triple. } - - let mut source_solution = vec![false; self.num_source_triples]; - - for ((left, right), usage) in pair_usage { - let Some(other_two) = usage.uprime_regulars else { - continue; - }; - if !usage.saw_u { - continue; - } - - let mut group = [left, right, other_two[0], other_two[1]]; - group.sort_unstable(); - if group.windows(2).any(|window| window[0] == window[1]) { - continue; - } - - if let Some(source_triple) = self.decode_real_group(group) { - source_solution[source_triple] = true; - } + let (right, _) = positions[first + 1]; + // Use the actual regular elements, not the pair's construction + // indices: equal-valued pairing elements are interchangeable. + let regulars = [ + groups[left][0], + groups[left][1], + groups[right][0], + groups[right][1], + ]; + if let Some(source_triple) = self.decode_real_group(regulars) { + source_solution[source_triple] = true; } - - source_solution - }) - } -} - -#[derive(Debug, Clone, Copy)] -enum TargetElement { - Regular { - step2_index: usize, - }, - Pairing { - pair_index: usize, - kind: PairingKind, - }, - Filler, -} - -fn sorted_pair(a: usize, b: usize) -> [usize; 2] { - if a <= b { - [a, b] - } else { - [b, a] + } + Ok(source_solution) } } diff --git a/src/solvers/ilp/adapter.rs b/src/solvers/ilp/adapter.rs new file mode 100644 index 00000000..425c142e --- /dev/null +++ b/src/solvers/ilp/adapter.rs @@ -0,0 +1,247 @@ +//! Numerical execution of a native ILP through HiGHS. +//! +//! This module knows only ILP data and backend settings. Registry lookup, +//! type-erased dispatch, and reduction-chain extraction belong to the caller. + +use crate::models::algebraic::{Comparison, ILPCoefficient, ObjectiveSense, VariableDomain, ILP}; +use crate::types::{i64_to_exact_f64, ExactI64ToF64Error, MAX_EXACT_F64_INTEGER}; +use good_lp::highs; +use good_lp::solvers::highs::HighsParallelType; +use good_lp::{ + variable, ProblemVariables, ResolutionError, Solution, SolutionStatus, SolverModel, Variable, +}; + +/// Internal errors are mapped to the existing public solver errors by orchestration. +#[derive(Debug, PartialEq, Eq, thiserror::Error)] +pub(crate) enum IlpBackendError { + #[error("the ILP is infeasible")] + Infeasible, + #[error("the ILP objective is unbounded")] + Unbounded, + #[error("the ILP solver reached its time limit before proving optimality")] + Timeout, + #[error("the ILP backend failed: {0}")] + BackendFailure(String), + #[error("the ILP backend returned an invalid rounded solution: {0}")] + InvalidSolution(String), + #[error(transparent)] + InexactTransport(#[from] ExactI64ToF64Error), +} + +/// Backend representation is an execution concern, not a model capability. +pub(crate) trait BackendCoefficient: ILPCoefficient { + fn to_backend_number(self) -> Result; +} +impl BackendCoefficient for i64 { + fn to_backend_number(self) -> Result { + Ok(i64_to_exact_f64(self)?) + } +} +impl BackendCoefficient for f64 { + fn to_backend_number(self) -> Result { + Ok(self) + } +} + +fn classify_backend_error(error: ResolutionError, time_limit: Option) -> IlpBackendError { + match error { + ResolutionError::Infeasible => IlpBackendError::Infeasible, + ResolutionError::Unbounded => IlpBackendError::Unbounded, + ResolutionError::Other("NoSolutionFound") if time_limit.is_some() => { + IlpBackendError::Timeout + } + other => IlpBackendError::BackendFailure(other.to_string()), + } +} + +pub(crate) struct HighsAdapter { + time_limit: Option, +} + +impl HighsAdapter { + pub(crate) fn new(time_limit: Option) -> Self { + Self { time_limit } + } + pub(crate) fn solve(&self, problem: &ILP) -> Result, IlpBackendError> + where + V: VariableDomain, + C: BackendCoefficient, + { + if self + .time_limit + .is_some_and(|seconds| !seconds.is_finite() || seconds < 0.0) + { + return Err(IlpBackendError::BackendFailure( + "time limit must be finite and nonnegative".into(), + )); + } + self.solve_with_objective(problem, problem.objective()) + } + + fn solve_with_objective( + &self, + problem: &ILP, + objective_terms: &[(usize, C)], + ) -> Result, IlpBackendError> + where + V: VariableDomain, + C: BackendCoefficient, + { + let n = problem.num_vars(); + if n == 0 { + return if problem + .is_feasible(&[]) + .map_err(|error| IlpBackendError::InvalidSolution(error.to_string()))? + { + Ok(vec![]) + } else { + Err(IlpBackendError::Infeasible) + }; + } + + let mut vars_builder = ProblemVariables::new(); + let vars: Vec = problem + .variables() + .iter() + .map(|variable_bounds| { + let mut definition = variable().integer(); + if let Some(lower) = variable_bounds.lower_bound() { + definition = definition.min(i64_to_exact_f64(lower)?); + } + if let Some(upper) = variable_bounds.upper_bound() { + definition = definition.max(i64_to_exact_f64(upper)?); + } + Ok(vars_builder.add(definition)) + }) + .collect::>()?; + + let objective = backend_expression(objective_terms, &vars)?; + + // Build the model with objective + let unsolved = match problem.sense() { + ObjectiveSense::Maximize => vars_builder.maximise(&objective), + ObjectiveSense::Minimize => vars_builder.minimise(&objective), + }; + + // Create the solver model + let mut model = { + let mut model = unsolved + .using(highs) + .set_option("random_seed", 0i32) + .set_option("mip_rel_gap", 0.0) + .set_option("mip_abs_gap", 0.0) + .set_parallel(HighsParallelType::Off) + .set_threads(1); + if let Some(seconds) = self.time_limit { + model = model.set_time_limit(seconds); + } + model + }; + + // Add constraints + for constraint in problem.constraints() { + let lhs = backend_expression(constraint.terms(), &vars)?; + let rhs = constraint.rhs().to_backend_number()?; + + // Create the constraint based on comparison type + let good_lp_constraint = match constraint.comparison() { + Comparison::Le => lhs.leq(rhs), + Comparison::Ge => lhs.geq(rhs), + Comparison::Eq => lhs.eq(rhs), + }; + + model = model.with(good_lp_constraint); + } + + // Solve + let solution = match model.solve() { + Ok(solution) => solution, + Err(ResolutionError::Infeasible) + if !objective_terms.is_empty() + && problem.variables().iter().any(|variable| { + variable.lower_bound().is_none() || variable.upper_bound().is_none() + }) => + { + // A zero objective cannot be unbounded, so feasibility distinguishes the two states. + self.solve_with_objective(problem, &[])?; + return Err(IlpBackendError::Unbounded); + } + Err(error) => return Err(classify_backend_error(error, self.time_limit)), + }; + + match solution.status() { + SolutionStatus::Optimal => {} + SolutionStatus::TimeLimit => return Err(IlpBackendError::Timeout), + SolutionStatus::GapLimit => { + return Err(IlpBackendError::BackendFailure( + "the backend stopped at its gap limit before proving optimality".to_string(), + )); + } + } + + decode_and_validate( + problem, + vars.iter().map(|variable| solution.value(*variable)), + ) + } +} + +fn backend_expression( + terms: &[(usize, C)], + variables: &[Variable], +) -> Result { + terms.iter().try_fold( + good_lp::Expression::with_capacity(terms.len()), + |mut expression, &(index, coefficient)| { + expression.add_mul(coefficient.to_backend_number()?, variables[index]); + Ok(expression) + }, + ) +} + +fn decode_and_validate( + problem: &ILP, + values: impl IntoIterator, +) -> Result, IlpBackendError> { + let result = values + .into_iter() + .enumerate() + .map(|(index, value)| { + if !value.is_finite() { + return Err(IlpBackendError::InvalidSolution(format!( + "variable {index} is non-finite" + ))); + } + let rounded = value.round(); + if (value - rounded).abs() > 1e-6 { + return Err(IlpBackendError::InvalidSolution(format!( + "variable {index} has non-integral value {value}" + ))); + } + if rounded.abs() > MAX_EXACT_F64_INTEGER as f64 { + return Err(IlpBackendError::InvalidSolution(format!( + "variable {index} value {rounded} exceeds exact f64 integer transport" + ))); + } + Ok(rounded as i64) + }) + .collect::, _>>()?; + if !problem + .is_feasible(&result) + .map_err(|error| IlpBackendError::InvalidSolution(error.to_string()))? + { + return Err(IlpBackendError::InvalidSolution( + "the rounded assignment violates the ILP; this may be caused by numerical tolerances. \ + Consider tightening the backend's integer feasibility tolerance" + .into(), + )); + } + problem + .evaluate_objective(&result) + .map_err(|error| IlpBackendError::InvalidSolution(error.to_string()))?; + Ok(result) +} + +#[cfg(test)] +#[path = "../../unit_tests/solvers/ilp/adapter.rs"] +mod tests; diff --git a/src/solvers/ilp/mod.rs b/src/solvers/ilp/mod.rs index 55556679..3829e3b8 100644 --- a/src/solvers/ilp/mod.rs +++ b/src/solvers/ilp/mod.rs @@ -1,8 +1,8 @@ //! ILP (Integer Linear Programming) solver module. //! //! This module provides an ILP solver using the HiGHS solver via the `good_lp` crate. -//! It is only available when the `ilp` feature is enabled. +pub(super) mod adapter; mod solver; pub use solver::{ILPSolveError, ILPSolver}; diff --git a/src/solvers/ilp/solver.rs b/src/solvers/ilp/solver.rs index e5325a29..cef116b6 100644 --- a/src/solvers/ilp/solver.rs +++ b/src/solvers/ilp/solver.rs @@ -1,15 +1,9 @@ //! ILP solver implementation using HiGHS. -use crate::models::algebraic::{Comparison, ObjectiveSense, VariableDomain, ILP}; +use super::adapter::{HighsAdapter, IlpBackendError}; use crate::solvers::registry::solver_capability_registry; use crate::solvers::ExactProblemKey; use crate::traits::Problem; -use crate::types::{i64_to_exact_f64, MAX_EXACT_F64_INTEGER}; -use good_lp::highs; -use good_lp::solvers::highs::HighsParallelType; -use good_lp::{ - variable, ProblemVariables, ResolutionError, Solution, SolutionStatus, SolverModel, Variable, -}; /// A failure to produce an ILP solution optimal within backend numerical tolerances. #[derive(Clone, Debug, PartialEq, Eq, thiserror::Error)] @@ -32,7 +26,7 @@ pub enum ILPSolveError { #[error("the ILP backend failed: {0}")] BackendFailure(String), /// Type-erased dispatch received a value other than a supported ILP variant. - #[error("the ILP backend requires bool/i64 variables and f64 coefficients")] + #[error("the ILP backend requires bool/i64 variables and i64/f64 coefficients")] UnsupportedProblemType, /// No ILP pipeline is registered for the exact problem variant. #[error("no ILP pipeline is registered for {0}")] @@ -57,19 +51,24 @@ pub enum ILPSolveError { Reduction(#[from] crate::rules::ReductionError), } -fn classify_backend_error(error: ResolutionError, time_limit: Option) -> ILPSolveError { - match error { - ResolutionError::Infeasible => ILPSolveError::Infeasible, - ResolutionError::Unbounded => ILPSolveError::Unbounded, - ResolutionError::Other("NoSolutionFound") if time_limit.is_some() => ILPSolveError::Timeout, - other => ILPSolveError::BackendFailure(other.to_string()), +// Keep adapter details out of the public error vocabulary. +impl From for ILPSolveError { + fn from(error: IlpBackendError) -> Self { + match error { + IlpBackendError::Infeasible => Self::Infeasible, + IlpBackendError::Unbounded => Self::Unbounded, + IlpBackendError::Timeout => Self::Timeout, + IlpBackendError::BackendFailure(message) => Self::BackendFailure(message), + IlpBackendError::InvalidSolution(message) => Self::InvalidSolution(message), + IlpBackendError::InexactTransport(error) => Self::InexactTransport(error), + } } } /// An ILP solver using the HiGHS backend. /// -/// Registered reductions map a source problem to an `ILP` terminal, -/// which this solver sends to HiGHS before extracting the source solution. +/// Registered reductions map a source problem to its native `ILP` terminal. +/// A shared adapter sends that ILP to HiGHS before source solution extraction. /// Optimality and infeasibility are assessed within HiGHS numerical tolerances. /// Zero MIP gaps do not make floating-point solving mathematically exact. /// @@ -127,172 +126,7 @@ impl ILPSolver { .lookup(&key) .ilp .ok_or_else(|| ILPSolveError::MissingPipeline(key.label()))?; - pipeline.solve_typed(problem, self) - } - - fn solve_backend(&self, problem: &ILP) -> Result, ILPSolveError> - where - V: VariableDomain, - { - self.solve_with_objective(problem, problem.objective()) - } - - fn solve_with_objective( - &self, - problem: &ILP, - objective_terms: &[(usize, f64)], - ) -> Result, ILPSolveError> - where - V: VariableDomain, - { - let n = problem.num_vars(); - if n == 0 { - return if problem - .is_feasible(&[]) - .map_err(|error| ILPSolveError::InvalidSolution(error.to_string()))? - { - Ok(vec![]) - } else { - Err(ILPSolveError::Infeasible) - }; - } - - let mut vars_builder = ProblemVariables::new(); - let vars: Vec = problem - .variables() - .iter() - .map(|variable_bounds| { - let mut definition = variable().integer(); - if let Some(lower) = variable_bounds.lower_bound() { - definition = definition.min(i64_to_exact_f64(lower)?); - } - if let Some(upper) = variable_bounds.upper_bound() { - definition = definition.max(i64_to_exact_f64(upper)?); - } - Ok(vars_builder.add(definition)) - }) - .collect::>()?; - - // Build objective expression - let objective: good_lp::Expression = objective_terms - .iter() - .map(|&(var_idx, coefficient)| coefficient * vars[var_idx]) - .sum(); - - // Build the model with objective - let unsolved = match problem.sense() { - ObjectiveSense::Maximize => vars_builder.maximise(&objective), - ObjectiveSense::Minimize => vars_builder.minimise(&objective), - }; - - // Create the solver model - let mut model = { - let mut model = unsolved - .using(highs) - .set_option("random_seed", 0i32) - .set_option("mip_rel_gap", 0.0) - .set_option("mip_abs_gap", 0.0) - .set_parallel(HighsParallelType::Off) - .set_threads(1); - if let Some(seconds) = self.time_limit { - model = model.set_time_limit(seconds); - } - model - }; - - // Add constraints - for constraint in problem.constraints() { - // Build left-hand side expression - let lhs: good_lp::Expression = constraint - .terms() - .iter() - .map(|&(var_idx, coefficient)| coefficient * vars[var_idx]) - .sum(); - - let rhs = constraint.rhs(); - - // Create the constraint based on comparison type - let good_lp_constraint = match constraint.comparison() { - Comparison::Le => lhs.leq(rhs), - Comparison::Ge => lhs.geq(rhs), - Comparison::Eq => lhs.eq(rhs), - }; - - model = model.with(good_lp_constraint); - } - - // Solve - let solution = match model.solve() { - Ok(solution) => solution, - Err(ResolutionError::Infeasible) - if !objective_terms.is_empty() - && problem.variables().iter().any(|variable| { - variable.lower_bound().is_none() || variable.upper_bound().is_none() - }) => - { - // A zero objective cannot be unbounded, so feasibility distinguishes the two states. - self.solve_with_objective(problem, &[])?; - return Err(ILPSolveError::Unbounded); - } - Err(error) => return Err(classify_backend_error(error, self.time_limit)), - }; - - match solution.status() { - SolutionStatus::Optimal => {} - SolutionStatus::TimeLimit => return Err(ILPSolveError::Timeout), - SolutionStatus::GapLimit => { - return Err(ILPSolveError::BackendFailure( - "the backend stopped at its gap limit before proving optimality".to_string(), - )); - } - } - - let result: Vec = vars - .iter() - .enumerate() - .map(|(index, v)| { - let value = solution.value(*v); - if !value.is_finite() { - return Err(ILPSolveError::InvalidSolution(format!( - "variable {index} is non-finite" - ))); - } - let rounded = value.round(); - if (value - rounded).abs() > 1e-6 { - return Err(ILPSolveError::InvalidSolution(format!( - "variable {index} has non-integral value {value}" - ))); - } - if rounded.abs() > MAX_EXACT_F64_INTEGER as f64 { - return Err(ILPSolveError::InvalidSolution(format!( - "variable {index} value {rounded} exceeds exact f64 integer transport" - ))); - } - Ok(rounded as i64) - }) - .collect::>()?; - - if !problem - .is_feasible(&result) - .map_err(|error| ILPSolveError::InvalidSolution(error.to_string()))? - { - return Err(ILPSolveError::InvalidSolution( - "the rounded assignment violates the ILP".into(), - )); - } - - Ok(result) - } - - /// Solve a type-erased supported ILP variant directly. - pub(crate) fn solve_dyn(&self, any: &dyn std::any::Any) -> Result, ILPSolveError> { - if let Some(ilp) = any.downcast_ref::>() { - return self.solve_backend(ilp); - } - if let Some(ilp) = any.downcast_ref::>() { - return self.solve_backend(ilp); - } - Err(ILPSolveError::UnsupportedProblemType) + pipeline.solve_typed(problem, &HighsAdapter::new(self.time_limit)) } } diff --git a/src/solvers/pipelines.rs b/src/solvers/pipelines.rs index 15e5aa52..d9674cd7 100644 --- a/src/solvers/pipelines.rs +++ b/src/solvers/pipelines.rs @@ -23,12 +23,10 @@ macro_rules! register_ilp_pipeline { register_ilp_pipeline! { ("ILP", [("variable", "bool"), ("coefficient", "i64")]), - ("ILP", [("variable", "bool"), ("coefficient", "f64")]), } register_ilp_pipeline! { ("ILP", [("variable", "i64"), ("coefficient", "i64")]), - ("ILP", [("variable", "i64"), ("coefficient", "f64")]), } register_ilp_pipeline! { @@ -42,118 +40,99 @@ register_ilp_pipeline! { register_ilp_pipeline! { ("AcyclicPartition", [("weight", "i64")]), ("ILP", [("variable", "i64"), ("coefficient", "i64")]), - ("ILP", [("variable", "i64"), ("coefficient", "f64")]), } register_ilp_pipeline! { ("BMF", []), ("ILP", [("variable", "bool"), ("coefficient", "i64")]), - ("ILP", [("variable", "bool"), ("coefficient", "f64")]), } register_ilp_pipeline! { ("BalancedCompleteBipartiteSubgraph", []), ("ILP", [("variable", "bool"), ("coefficient", "i64")]), - ("ILP", [("variable", "bool"), ("coefficient", "f64")]), } register_ilp_pipeline! { ("BicliqueCover", []), ("BMF", []), ("ILP", [("variable", "bool"), ("coefficient", "i64")]), - ("ILP", [("variable", "bool"), ("coefficient", "f64")]), } register_ilp_pipeline! { ("BiconnectivityAugmentation", [("graph", "SimpleGraph"), ("weight", "i64")]), ("ILP", [("variable", "i64"), ("coefficient", "i64")]), - ("ILP", [("variable", "i64"), ("coefficient", "f64")]), } register_ilp_pipeline! { ("BinPacking", [("weight", "i64")]), ("ILP", [("variable", "bool"), ("coefficient", "i64")]), - ("ILP", [("variable", "bool"), ("coefficient", "f64")]), } register_ilp_pipeline! { ("BottleneckTravelingSalesman", []), ("ILP", [("variable", "i64"), ("coefficient", "i64")]), - ("ILP", [("variable", "i64"), ("coefficient", "f64")]), } register_ilp_pipeline! { ("BoundedComponentSpanningForest", [("graph", "SimpleGraph"), ("weight", "i64")]), ("ILP", [("variable", "i64"), ("coefficient", "i64")]), - ("ILP", [("variable", "i64"), ("coefficient", "f64")]), } register_ilp_pipeline! { ("CapacityAssignment", []), ("ILP", [("variable", "bool"), ("coefficient", "i64")]), - ("ILP", [("variable", "bool"), ("coefficient", "f64")]), } register_ilp_pipeline! { ("CircuitSAT", []), ("ILP", [("variable", "bool"), ("coefficient", "i64")]), - ("ILP", [("variable", "bool"), ("coefficient", "f64")]), } register_ilp_pipeline! { ("ClosestString", []), ("ILP", [("variable", "i64"), ("coefficient", "i64")]), - ("ILP", [("variable", "i64"), ("coefficient", "f64")]), } register_ilp_pipeline! { ("ClosestSubstring", []), ("ILP", [("variable", "i64"), ("coefficient", "i64")]), - ("ILP", [("variable", "i64"), ("coefficient", "f64")]), } register_ilp_pipeline! { ("Clustering", []), ("ILP", [("variable", "bool"), ("coefficient", "i64")]), - ("ILP", [("variable", "bool"), ("coefficient", "f64")]), } register_ilp_pipeline! { ("ConsecutiveBlockMinimization", []), ("ILP", [("variable", "bool"), ("coefficient", "i64")]), - ("ILP", [("variable", "bool"), ("coefficient", "f64")]), } register_ilp_pipeline! { ("ConsecutiveOnesMatrixAugmentation", []), ("ILP", [("variable", "bool"), ("coefficient", "i64")]), - ("ILP", [("variable", "bool"), ("coefficient", "f64")]), } register_ilp_pipeline! { ("ConsecutiveOnesSubmatrix", []), ("ILP", [("variable", "bool"), ("coefficient", "i64")]), - ("ILP", [("variable", "bool"), ("coefficient", "f64")]), } register_ilp_pipeline! { ("ConsistencyOfDatabaseFrequencyTables", []), ("ILP", [("variable", "bool"), ("coefficient", "i64")]), - ("ILP", [("variable", "bool"), ("coefficient", "f64")]), } register_ilp_pipeline! { ("DecisionMinimumDominatingSet", [("graph", "SimpleGraph"), ("weight", "One")]), ("MinimumSumMulticenter", [("graph", "SimpleGraph"), ("weight", "i64")]), ("ILP", [("variable", "bool"), ("coefficient", "i64")]), - ("ILP", [("variable", "bool"), ("coefficient", "f64")]), } register_ilp_pipeline! { ("DecisionMinimumDominatingSet", [("graph", "SimpleGraph"), ("weight", "i64")]), ("MinimumDominatingSet", [("graph", "SimpleGraph"), ("weight", "i64")]), ("ILP", [("variable", "bool"), ("coefficient", "i64")]), - ("ILP", [("variable", "bool"), ("coefficient", "f64")]), } register_ilp_pipeline! { @@ -161,50 +140,42 @@ register_ilp_pipeline! { ("MinimumVertexCover", [("graph", "SimpleGraph"), ("weight", "i64")]), ("MinimumSetCovering", [("weight", "i64")]), ("ILP", [("variable", "bool"), ("coefficient", "i64")]), - ("ILP", [("variable", "bool"), ("coefficient", "f64")]), } register_ilp_pipeline! { ("DecisionOptimalLinearArrangement", [("graph", "SimpleGraph")]), ("OptimalLinearArrangement", [("graph", "SimpleGraph")]), ("ILP", [("variable", "i64"), ("coefficient", "i64")]), - ("ILP", [("variable", "i64"), ("coefficient", "f64")]), } register_ilp_pipeline! { ("DirectedHamiltonianPath", []), ("ILP", [("variable", "bool"), ("coefficient", "i64")]), - ("ILP", [("variable", "bool"), ("coefficient", "f64")]), } register_ilp_pipeline! { ("DirectedTwoCommodityIntegralFlow", []), ("ILP", [("variable", "i64"), ("coefficient", "i64")]), - ("ILP", [("variable", "i64"), ("coefficient", "f64")]), } register_ilp_pipeline! { ("DisjointConnectingPaths", [("graph", "SimpleGraph")]), ("ILP", [("variable", "bool"), ("coefficient", "i64")]), - ("ILP", [("variable", "bool"), ("coefficient", "f64")]), } register_ilp_pipeline! { ("EnsembleComputation", []), ("ILP", [("variable", "i64"), ("coefficient", "i64")]), - ("ILP", [("variable", "i64"), ("coefficient", "f64")]), } register_ilp_pipeline! { ("EulerianPath", []), ("ILP", [("variable", "i64"), ("coefficient", "i64")]), - ("ILP", [("variable", "i64"), ("coefficient", "f64")]), } register_ilp_pipeline! { ("ExactCoverBy3Sets", []), ("ILP", [("variable", "bool"), ("coefficient", "i64")]), - ("ILP", [("variable", "bool"), ("coefficient", "f64")]), } register_ilp_pipeline! { @@ -215,44 +186,37 @@ register_ilp_pipeline! { register_ilp_pipeline! { ("Factoring", []), ("ILP", [("variable", "i64"), ("coefficient", "i64")]), - ("ILP", [("variable", "i64"), ("coefficient", "f64")]), } register_ilp_pipeline! { ("FeasibleRegisterAssignment", []), ("ILP", [("variable", "i64"), ("coefficient", "i64")]), - ("ILP", [("variable", "i64"), ("coefficient", "f64")]), } register_ilp_pipeline! { ("FlowShopScheduling", []), ("ILP", [("variable", "i64"), ("coefficient", "i64")]), - ("ILP", [("variable", "i64"), ("coefficient", "f64")]), } register_ilp_pipeline! { ("GraphPartitioning", [("graph", "SimpleGraph")]), ("ILP", [("variable", "bool"), ("coefficient", "i64")]), - ("ILP", [("variable", "bool"), ("coefficient", "f64")]), } register_ilp_pipeline! { ("HamiltonianCircuit", [("graph", "SimpleGraph")]), ("LongestCircuit", [("graph", "SimpleGraph"), ("weight", "i64")]), ("ILP", [("variable", "bool"), ("coefficient", "i64")]), - ("ILP", [("variable", "bool"), ("coefficient", "f64")]), } register_ilp_pipeline! { ("HamiltonianPath", [("graph", "SimpleGraph")]), ("ILP", [("variable", "bool"), ("coefficient", "i64")]), - ("ILP", [("variable", "bool"), ("coefficient", "f64")]), } register_ilp_pipeline! { ("HighlyConnectedDeletion", [("graph", "SimpleGraph")]), ("ILP", [("variable", "bool"), ("coefficient", "i64")]), - ("ILP", [("variable", "bool"), ("coefficient", "f64")]), } // This exact variant also has a customized backend. Default dispatch selects the @@ -261,50 +225,42 @@ register_ilp_pipeline! { ("RootedTreeArrangement", [("graph", "SimpleGraph")]), ("RootedTreeStorageAssignment", []), ("ILP", [("variable", "i64"), ("coefficient", "i64")]), - ("ILP", [("variable", "i64"), ("coefficient", "f64")]), } register_ilp_pipeline! { ("IntegralFlowBundles", []), ("ILP", [("variable", "i64"), ("coefficient", "i64")]), - ("ILP", [("variable", "i64"), ("coefficient", "f64")]), } register_ilp_pipeline! { ("IntegralFlowHomologousArcs", []), ("ILP", [("variable", "i64"), ("coefficient", "i64")]), - ("ILP", [("variable", "i64"), ("coefficient", "f64")]), } register_ilp_pipeline! { ("IntegralFlowWithMultipliers", []), ("ILP", [("variable", "i64"), ("coefficient", "i64")]), - ("ILP", [("variable", "i64"), ("coefficient", "f64")]), } register_ilp_pipeline! { ("IsomorphicSpanningTree", [("graph", "SimpleGraph")]), ("ILP", [("variable", "bool"), ("coefficient", "i64")]), - ("ILP", [("variable", "bool"), ("coefficient", "f64")]), } register_ilp_pipeline! { ("KClique", [("graph", "SimpleGraph")]), ("ILP", [("variable", "bool"), ("coefficient", "i64")]), - ("ILP", [("variable", "bool"), ("coefficient", "f64")]), } register_ilp_pipeline! { ("KColoring", [("graph", "SimpleGraph"), ("k", "KN")]), ("ILP", [("variable", "bool"), ("coefficient", "i64")]), - ("ILP", [("variable", "bool"), ("coefficient", "f64")]), } register_ilp_pipeline! { ("KColoring", [("graph", "SimpleGraph"), ("k", "K3")]), ("Clustering", []), ("ILP", [("variable", "bool"), ("coefficient", "i64")]), - ("ILP", [("variable", "bool"), ("coefficient", "f64")]), } register_ilp_pipeline! { @@ -312,49 +268,41 @@ register_ilp_pipeline! { ("Satisfiability", []), ("NAESatisfiability", []), ("ILP", [("variable", "bool"), ("coefficient", "i64")]), - ("ILP", [("variable", "bool"), ("coefficient", "f64")]), } register_ilp_pipeline! { ("Knapsack", []), ("ILP", [("variable", "bool"), ("coefficient", "i64")]), - ("ILP", [("variable", "bool"), ("coefficient", "f64")]), } register_ilp_pipeline! { ("LengthBoundedDisjointPaths", [("graph", "SimpleGraph")]), ("ILP", [("variable", "bool"), ("coefficient", "i64")]), - ("ILP", [("variable", "bool"), ("coefficient", "f64")]), } register_ilp_pipeline! { ("LongestCircuit", [("graph", "SimpleGraph"), ("weight", "i64")]), ("ILP", [("variable", "bool"), ("coefficient", "i64")]), - ("ILP", [("variable", "bool"), ("coefficient", "f64")]), } register_ilp_pipeline! { ("LongestCommonSubsequence", []), ("ILP", [("variable", "bool"), ("coefficient", "i64")]), - ("ILP", [("variable", "bool"), ("coefficient", "f64")]), } register_ilp_pipeline! { ("LongestPath", [("graph", "SimpleGraph"), ("weight", "i64")]), ("ILP", [("variable", "i64"), ("coefficient", "i64")]), - ("ILP", [("variable", "i64"), ("coefficient", "f64")]), } register_ilp_pipeline! { ("MaximalIS", [("graph", "SimpleGraph"), ("weight", "i64")]), ("ILP", [("variable", "bool"), ("coefficient", "i64")]), - ("ILP", [("variable", "bool"), ("coefficient", "f64")]), } register_ilp_pipeline! { ("Maximum2Satisfiability", []), ("ILP", [("variable", "bool"), ("coefficient", "i64")]), - ("ILP", [("variable", "bool"), ("coefficient", "f64")]), } register_ilp_pipeline! { @@ -363,43 +311,36 @@ register_ilp_pipeline! { ("MaximumIndependentSet", [("graph", "SimpleGraph"), ("weight", "i64")]), ("MaximumSetPacking", [("weight", "i64")]), ("ILP", [("variable", "bool"), ("coefficient", "i64")]), - ("ILP", [("variable", "bool"), ("coefficient", "f64")]), } register_ilp_pipeline! { ("MaximumClique", [("graph", "SimpleGraph"), ("weight", "i64")]), ("ILP", [("variable", "bool"), ("coefficient", "i64")]), - ("ILP", [("variable", "bool"), ("coefficient", "f64")]), } register_ilp_pipeline! { ("MaximumCoKPlex", [("graph", "SimpleGraph"), ("k", "KN"), ("weight", "One")]), ("ILP", [("variable", "bool"), ("coefficient", "i64")]), - ("ILP", [("variable", "bool"), ("coefficient", "f64")]), } register_ilp_pipeline! { ("MaximumCoKPlex", [("graph", "SimpleGraph"), ("k", "KN"), ("weight", "i64")]), ("ILP", [("variable", "bool"), ("coefficient", "i64")]), - ("ILP", [("variable", "bool"), ("coefficient", "f64")]), } register_ilp_pipeline! { ("MaximumCommonEdgeSubgraph", []), ("ILP", [("variable", "bool"), ("coefficient", "i64")]), - ("ILP", [("variable", "bool"), ("coefficient", "f64")]), } register_ilp_pipeline! { ("MaximumContactMapOverlap", []), ("ILP", [("variable", "bool"), ("coefficient", "i64")]), - ("ILP", [("variable", "bool"), ("coefficient", "f64")]), } register_ilp_pipeline! { ("MaximumDomaticNumber", [("graph", "SimpleGraph")]), ("ILP", [("variable", "bool"), ("coefficient", "i64")]), - ("ILP", [("variable", "bool"), ("coefficient", "f64")]), } register_ilp_pipeline! { @@ -410,7 +351,6 @@ register_ilp_pipeline! { register_ilp_pipeline! { ("MaximumEdgeWeightedKClique", [("weight", "i64")]), ("ILP", [("variable", "bool"), ("coefficient", "i64")]), - ("ILP", [("variable", "bool"), ("coefficient", "f64")]), } register_ilp_pipeline! { @@ -418,7 +358,6 @@ register_ilp_pipeline! { ("MaximumIndependentSet", [("graph", "SimpleGraph"), ("weight", "i64")]), ("MaximumSetPacking", [("weight", "i64")]), ("ILP", [("variable", "bool"), ("coefficient", "i64")]), - ("ILP", [("variable", "bool"), ("coefficient", "f64")]), } register_ilp_pipeline! { @@ -428,14 +367,12 @@ register_ilp_pipeline! { ("MaximumIndependentSet", [("graph", "SimpleGraph"), ("weight", "i64")]), ("MaximumClique", [("graph", "SimpleGraph"), ("weight", "i64")]), ("ILP", [("variable", "bool"), ("coefficient", "i64")]), - ("ILP", [("variable", "bool"), ("coefficient", "f64")]), } register_ilp_pipeline! { ("MaximumIndependentSet", [("graph", "SimpleGraph"), ("weight", "i64")]), ("MaximumSetPacking", [("weight", "i64")]), ("ILP", [("variable", "bool"), ("coefficient", "i64")]), - ("ILP", [("variable", "bool"), ("coefficient", "f64")]), } register_ilp_pipeline! { @@ -444,7 +381,6 @@ register_ilp_pipeline! { ("MaximumIndependentSet", [("graph", "SimpleGraph"), ("weight", "i64")]), ("MaximumClique", [("graph", "SimpleGraph"), ("weight", "i64")]), ("ILP", [("variable", "bool"), ("coefficient", "i64")]), - ("ILP", [("variable", "bool"), ("coefficient", "f64")]), } register_ilp_pipeline! { @@ -453,7 +389,6 @@ register_ilp_pipeline! { ("MaximumIndependentSet", [("graph", "SimpleGraph"), ("weight", "i64")]), ("MaximumClique", [("graph", "SimpleGraph"), ("weight", "i64")]), ("ILP", [("variable", "bool"), ("coefficient", "i64")]), - ("ILP", [("variable", "bool"), ("coefficient", "f64")]), } register_ilp_pipeline! { @@ -462,7 +397,6 @@ register_ilp_pipeline! { ("MaximumIndependentSet", [("graph", "SimpleGraph"), ("weight", "i64")]), ("MaximumClique", [("graph", "SimpleGraph"), ("weight", "i64")]), ("ILP", [("variable", "bool"), ("coefficient", "i64")]), - ("ILP", [("variable", "bool"), ("coefficient", "f64")]), } register_ilp_pipeline! { @@ -470,32 +404,27 @@ register_ilp_pipeline! { ("MaximumIndependentSet", [("graph", "SimpleGraph"), ("weight", "i64")]), ("MaximumClique", [("graph", "SimpleGraph"), ("weight", "i64")]), ("ILP", [("variable", "bool"), ("coefficient", "i64")]), - ("ILP", [("variable", "bool"), ("coefficient", "f64")]), } register_ilp_pipeline! { ("MaximumLeafSpanningTree", [("graph", "SimpleGraph")]), ("ILP", [("variable", "i64"), ("coefficient", "i64")]), - ("ILP", [("variable", "i64"), ("coefficient", "f64")]), } register_ilp_pipeline! { ("MaximumLikelihoodRanking", []), ("ILP", [("variable", "bool"), ("coefficient", "i64")]), - ("ILP", [("variable", "bool"), ("coefficient", "f64")]), } register_ilp_pipeline! { ("MaximumMatching", [("graph", "SimpleGraph"), ("weight", "i64")]), ("ILP", [("variable", "bool"), ("coefficient", "i64")]), - ("ILP", [("variable", "bool"), ("coefficient", "f64")]), } register_ilp_pipeline! { ("MaximumSetPacking", [("weight", "One")]), ("MaximumSetPacking", [("weight", "i64")]), ("ILP", [("variable", "bool"), ("coefficient", "i64")]), - ("ILP", [("variable", "bool"), ("coefficient", "f64")]), } register_ilp_pipeline! { @@ -507,31 +436,26 @@ register_ilp_pipeline! { register_ilp_pipeline! { ("MaximumSetPacking", [("weight", "i64")]), ("ILP", [("variable", "bool"), ("coefficient", "i64")]), - ("ILP", [("variable", "bool"), ("coefficient", "f64")]), } register_ilp_pipeline! { ("MinMaxMulticenter", [("graph", "SimpleGraph"), ("weight", "i64")]), ("ILP", [("variable", "i64"), ("coefficient", "i64")]), - ("ILP", [("variable", "i64"), ("coefficient", "f64")]), } register_ilp_pipeline! { ("MinimumCapacitatedSpanningTree", [("graph", "SimpleGraph"), ("weight", "i64")]), ("ILP", [("variable", "i64"), ("coefficient", "i64")]), - ("ILP", [("variable", "i64"), ("coefficient", "f64")]), } register_ilp_pipeline! { ("MinimumCoveringByCliques", [("graph", "SimpleGraph")]), ("ILP", [("variable", "bool"), ("coefficient", "i64")]), - ("ILP", [("variable", "bool"), ("coefficient", "f64")]), } register_ilp_pipeline! { ("MinimumCutIntoBoundedSets", [("graph", "SimpleGraph"), ("weight", "i64")]), ("ILP", [("variable", "bool"), ("coefficient", "i64")]), - ("ILP", [("variable", "bool"), ("coefficient", "f64")]), } register_ilp_pipeline! { @@ -543,245 +467,205 @@ register_ilp_pipeline! { register_ilp_pipeline! { ("MinimumDominatingSet", [("graph", "SimpleGraph"), ("weight", "i64")]), ("ILP", [("variable", "bool"), ("coefficient", "i64")]), - ("ILP", [("variable", "bool"), ("coefficient", "f64")]), } register_ilp_pipeline! { ("MinimumEdgeCostFlow", []), ("ILP", [("variable", "i64"), ("coefficient", "i64")]), - ("ILP", [("variable", "i64"), ("coefficient", "f64")]), } register_ilp_pipeline! { ("MinimumExternalMacroDataCompression", []), ("ILP", [("variable", "bool"), ("coefficient", "i64")]), - ("ILP", [("variable", "bool"), ("coefficient", "f64")]), } register_ilp_pipeline! { ("MinimumFaultDetectionTestSet", []), ("ILP", [("variable", "bool"), ("coefficient", "i64")]), - ("ILP", [("variable", "bool"), ("coefficient", "f64")]), } register_ilp_pipeline! { ("MinimumFeedbackArcSet", [("weight", "i64")]), ("ILP", [("variable", "i64"), ("coefficient", "i64")]), - ("ILP", [("variable", "i64"), ("coefficient", "f64")]), } register_ilp_pipeline! { ("MinimumFeedbackVertexSet", [("weight", "i64")]), ("ILP", [("variable", "i64"), ("coefficient", "i64")]), - ("ILP", [("variable", "i64"), ("coefficient", "f64")]), } register_ilp_pipeline! { ("MinimumGraphBandwidth", [("graph", "SimpleGraph")]), ("ILP", [("variable", "i64"), ("coefficient", "i64")]), - ("ILP", [("variable", "i64"), ("coefficient", "f64")]), } register_ilp_pipeline! { ("MinimumHittingSet", []), ("ILP", [("variable", "bool"), ("coefficient", "i64")]), - ("ILP", [("variable", "bool"), ("coefficient", "f64")]), } register_ilp_pipeline! { ("MinimumInternalMacroDataCompression", []), ("ILP", [("variable", "bool"), ("coefficient", "i64")]), - ("ILP", [("variable", "bool"), ("coefficient", "f64")]), } register_ilp_pipeline! { ("MinimumMatrixCover", []), ("ILP", [("variable", "bool"), ("coefficient", "i64")]), - ("ILP", [("variable", "bool"), ("coefficient", "f64")]), } register_ilp_pipeline! { ("MinimumMaximalMatching", [("graph", "SimpleGraph")]), ("ILP", [("variable", "bool"), ("coefficient", "i64")]), - ("ILP", [("variable", "bool"), ("coefficient", "f64")]), } register_ilp_pipeline! { ("MinimumMetricDimension", [("graph", "SimpleGraph")]), ("ILP", [("variable", "bool"), ("coefficient", "i64")]), - ("ILP", [("variable", "bool"), ("coefficient", "f64")]), } register_ilp_pipeline! { ("MinimumMultiwayCut", [("graph", "SimpleGraph"), ("weight", "i64")]), ("ILP", [("variable", "bool"), ("coefficient", "i64")]), - ("ILP", [("variable", "bool"), ("coefficient", "f64")]), } register_ilp_pipeline! { ("MinimumSetCovering", [("weight", "i64")]), ("ILP", [("variable", "bool"), ("coefficient", "i64")]), - ("ILP", [("variable", "bool"), ("coefficient", "f64")]), } register_ilp_pipeline! { ("MinimumSumMulticenter", [("graph", "SimpleGraph"), ("weight", "i64")]), ("ILP", [("variable", "bool"), ("coefficient", "i64")]), - ("ILP", [("variable", "bool"), ("coefficient", "f64")]), } register_ilp_pipeline! { ("MinimumTardinessSequencing", [("weight", "One")]), ("ILP", [("variable", "bool"), ("coefficient", "i64")]), - ("ILP", [("variable", "bool"), ("coefficient", "f64")]), } register_ilp_pipeline! { ("MinimumTardinessSequencing", [("weight", "i64")]), ("ILP", [("variable", "bool"), ("coefficient", "i64")]), - ("ILP", [("variable", "bool"), ("coefficient", "f64")]), } register_ilp_pipeline! { ("MinimumVertexCover", [("graph", "SimpleGraph"), ("weight", "One")]), ("MinimumHittingSet", []), ("ILP", [("variable", "bool"), ("coefficient", "i64")]), - ("ILP", [("variable", "bool"), ("coefficient", "f64")]), } register_ilp_pipeline! { ("MinimumVertexCover", [("graph", "SimpleGraph"), ("weight", "i64")]), ("MinimumSetCovering", [("weight", "i64")]), ("ILP", [("variable", "bool"), ("coefficient", "i64")]), - ("ILP", [("variable", "bool"), ("coefficient", "f64")]), } register_ilp_pipeline! { ("MinimumWeightDecoding", []), ("ILP", [("variable", "i64"), ("coefficient", "i64")]), - ("ILP", [("variable", "i64"), ("coefficient", "f64")]), } register_ilp_pipeline! { ("MixedChinesePostman", [("weight", "i64")]), ("ILP", [("variable", "i64"), ("coefficient", "i64")]), - ("ILP", [("variable", "i64"), ("coefficient", "f64")]), } register_ilp_pipeline! { ("MonochromaticTriangle", [("graph", "SimpleGraph")]), ("ILP", [("variable", "bool"), ("coefficient", "i64")]), - ("ILP", [("variable", "bool"), ("coefficient", "f64")]), } register_ilp_pipeline! { ("MultipleCopyFileAllocation", []), ("ILP", [("variable", "bool"), ("coefficient", "i64")]), - ("ILP", [("variable", "bool"), ("coefficient", "f64")]), } register_ilp_pipeline! { ("MultipleChoiceBranching", [("weight", "i64")]), ("ILP", [("variable", "i64"), ("coefficient", "i64")]), - ("ILP", [("variable", "i64"), ("coefficient", "f64")]), } register_ilp_pipeline! { ("MultiprocessorScheduling", []), ("ILP", [("variable", "bool"), ("coefficient", "i64")]), - ("ILP", [("variable", "bool"), ("coefficient", "f64")]), } register_ilp_pipeline! { ("NAESatisfiability", []), ("ILP", [("variable", "bool"), ("coefficient", "i64")]), - ("ILP", [("variable", "bool"), ("coefficient", "f64")]), } register_ilp_pipeline! { ("Numerical3DimensionalMatching", []), ("NumericalMatchingWithTargetSums", []), ("ILP", [("variable", "bool"), ("coefficient", "i64")]), - ("ILP", [("variable", "bool"), ("coefficient", "f64")]), } register_ilp_pipeline! { ("NumericalMatchingWithTargetSums", []), ("ILP", [("variable", "bool"), ("coefficient", "i64")]), - ("ILP", [("variable", "bool"), ("coefficient", "f64")]), } register_ilp_pipeline! { ("OpenShopScheduling", []), ("ILP", [("variable", "i64"), ("coefficient", "i64")]), - ("ILP", [("variable", "i64"), ("coefficient", "f64")]), } register_ilp_pipeline! { ("OptimalLinearArrangement", [("graph", "SimpleGraph")]), ("ILP", [("variable", "i64"), ("coefficient", "i64")]), - ("ILP", [("variable", "i64"), ("coefficient", "f64")]), } register_ilp_pipeline! { ("OptimumCommunicationSpanningTree", []), ("ILP", [("variable", "bool"), ("coefficient", "i64")]), - ("ILP", [("variable", "bool"), ("coefficient", "f64")]), } register_ilp_pipeline! { ("PaintShop", []), ("ILP", [("variable", "bool"), ("coefficient", "i64")]), - ("ILP", [("variable", "bool"), ("coefficient", "f64")]), } register_ilp_pipeline! { ("PartiallyOrderedKnapsack", []), ("ILP", [("variable", "bool"), ("coefficient", "i64")]), - ("ILP", [("variable", "bool"), ("coefficient", "f64")]), } register_ilp_pipeline! { ("Partition", []), ("MultiprocessorScheduling", []), ("ILP", [("variable", "bool"), ("coefficient", "i64")]), - ("ILP", [("variable", "bool"), ("coefficient", "f64")]), } register_ilp_pipeline! { ("PartitionIntoCliques", [("graph", "SimpleGraph")]), ("ILP", [("variable", "bool"), ("coefficient", "i64")]), - ("ILP", [("variable", "bool"), ("coefficient", "f64")]), } register_ilp_pipeline! { ("PartitionIntoPathsOfLength2", [("graph", "SimpleGraph")]), ("ILP", [("variable", "bool"), ("coefficient", "i64")]), - ("ILP", [("variable", "bool"), ("coefficient", "f64")]), } register_ilp_pipeline! { ("PartitionIntoTriangles", [("graph", "SimpleGraph")]), ("ILP", [("variable", "bool"), ("coefficient", "i64")]), - ("ILP", [("variable", "bool"), ("coefficient", "f64")]), } register_ilp_pipeline! { ("PathConstrainedNetworkFlow", []), ("ILP", [("variable", "i64"), ("coefficient", "i64")]), - ("ILP", [("variable", "i64"), ("coefficient", "f64")]), } register_ilp_pipeline! { ("PrecedenceConstrainedScheduling", []), ("ILP", [("variable", "bool"), ("coefficient", "i64")]), - ("ILP", [("variable", "bool"), ("coefficient", "f64")]), } register_ilp_pipeline! { ("PreemptiveScheduling", []), ("ILP", [("variable", "i64"), ("coefficient", "i64")]), - ("ILP", [("variable", "i64"), ("coefficient", "f64")]), } register_ilp_pipeline! { @@ -792,122 +676,102 @@ register_ilp_pipeline! { register_ilp_pipeline! { ("QUBO", [("weight", "i64")]), ("ILP", [("variable", "bool"), ("coefficient", "i64")]), - ("ILP", [("variable", "bool"), ("coefficient", "f64")]), } register_ilp_pipeline! { ("QuadraticAssignment", []), ("ILP", [("variable", "bool"), ("coefficient", "i64")]), - ("ILP", [("variable", "bool"), ("coefficient", "f64")]), } register_ilp_pipeline! { ("RectilinearPictureCompression", []), ("ILP", [("variable", "bool"), ("coefficient", "i64")]), - ("ILP", [("variable", "bool"), ("coefficient", "f64")]), } register_ilp_pipeline! { ("RegisterSufficiency", []), ("ILP", [("variable", "i64"), ("coefficient", "i64")]), - ("ILP", [("variable", "i64"), ("coefficient", "f64")]), } register_ilp_pipeline! { ("ResourceConstrainedScheduling", []), ("ILP", [("variable", "bool"), ("coefficient", "i64")]), - ("ILP", [("variable", "bool"), ("coefficient", "f64")]), } register_ilp_pipeline! { ("RootedTreeStorageAssignment", []), ("ILP", [("variable", "i64"), ("coefficient", "i64")]), - ("ILP", [("variable", "i64"), ("coefficient", "f64")]), } register_ilp_pipeline! { ("RuralPostman", [("graph", "SimpleGraph"), ("weight", "i64")]), ("ILP", [("variable", "i64"), ("coefficient", "i64")]), - ("ILP", [("variable", "i64"), ("coefficient", "f64")]), } register_ilp_pipeline! { ("Satisfiability", []), ("NAESatisfiability", []), ("ILP", [("variable", "bool"), ("coefficient", "i64")]), - ("ILP", [("variable", "bool"), ("coefficient", "f64")]), } register_ilp_pipeline! { ("SchedulingToMinimizeWeightedCompletionTime", []), ("ILP", [("variable", "i64"), ("coefficient", "i64")]), - ("ILP", [("variable", "i64"), ("coefficient", "f64")]), } register_ilp_pipeline! { ("SchedulingWithIndividualDeadlines", []), ("ILP", [("variable", "bool"), ("coefficient", "i64")]), - ("ILP", [("variable", "bool"), ("coefficient", "f64")]), } register_ilp_pipeline! { ("SequencingToMinimizeMaximumCumulativeCost", []), ("ILP", [("variable", "i64"), ("coefficient", "i64")]), - ("ILP", [("variable", "i64"), ("coefficient", "f64")]), } register_ilp_pipeline! { ("SequencingToMinimizeTardyTaskWeight", []), ("ILP", [("variable", "bool"), ("coefficient", "i64")]), - ("ILP", [("variable", "bool"), ("coefficient", "f64")]), } register_ilp_pipeline! { ("SequencingToMinimizeWeightedTardiness", []), ("ILP", [("variable", "i64"), ("coefficient", "i64")]), - ("ILP", [("variable", "i64"), ("coefficient", "f64")]), } register_ilp_pipeline! { ("SequencingWithDeadlinesAndSetUpTimes", []), ("ILP", [("variable", "bool"), ("coefficient", "i64")]), - ("ILP", [("variable", "bool"), ("coefficient", "f64")]), } register_ilp_pipeline! { ("SequencingWithReleaseTimesAndDeadlines", []), ("ILP", [("variable", "bool"), ("coefficient", "i64")]), - ("ILP", [("variable", "bool"), ("coefficient", "f64")]), } register_ilp_pipeline! { ("SequencingWithinIntervals", []), ("ILP", [("variable", "bool"), ("coefficient", "i64")]), - ("ILP", [("variable", "bool"), ("coefficient", "f64")]), } register_ilp_pipeline! { ("SetSplitting", []), ("ILP", [("variable", "bool"), ("coefficient", "i64")]), - ("ILP", [("variable", "bool"), ("coefficient", "f64")]), } register_ilp_pipeline! { ("ShortestCommonSupersequence", []), ("ILP", [("variable", "bool"), ("coefficient", "i64")]), - ("ILP", [("variable", "bool"), ("coefficient", "f64")]), } register_ilp_pipeline! { ("ShortestWeightConstrainedPath", [("graph", "SimpleGraph"), ("weight", "i64")]), ("ILP", [("variable", "i64"), ("coefficient", "i64")]), - ("ILP", [("variable", "i64"), ("coefficient", "f64")]), } register_ilp_pipeline! { ("SparseMatrixCompression", []), ("ILP", [("variable", "bool"), ("coefficient", "i64")]), - ("ILP", [("variable", "bool"), ("coefficient", "f64")]), } register_ilp_pipeline! { @@ -920,66 +784,55 @@ register_ilp_pipeline! { ("SpinGlass", [("graph", "SimpleGraph"), ("weight", "i64")]), ("QUBO", [("weight", "i64")]), ("ILP", [("variable", "bool"), ("coefficient", "i64")]), - ("ILP", [("variable", "bool"), ("coefficient", "f64")]), } register_ilp_pipeline! { ("StackerCrane", []), ("ILP", [("variable", "bool"), ("coefficient", "i64")]), - ("ILP", [("variable", "bool"), ("coefficient", "f64")]), } register_ilp_pipeline! { ("StringToStringCorrection", []), ("ILP", [("variable", "bool"), ("coefficient", "i64")]), - ("ILP", [("variable", "bool"), ("coefficient", "f64")]), } register_ilp_pipeline! { ("StrongConnectivityAugmentation", [("weight", "i64")]), ("ILP", [("variable", "i64"), ("coefficient", "i64")]), - ("ILP", [("variable", "i64"), ("coefficient", "f64")]), } register_ilp_pipeline! { ("SubgraphIsomorphism", []), ("ILP", [("variable", "bool"), ("coefficient", "i64")]), - ("ILP", [("variable", "bool"), ("coefficient", "f64")]), } register_ilp_pipeline! { ("SumOfSquaresPartition", []), ("ILP", [("variable", "bool"), ("coefficient", "i64")]), - ("ILP", [("variable", "bool"), ("coefficient", "f64")]), } register_ilp_pipeline! { ("ThreeDimensionalMatching", []), ("ILP", [("variable", "bool"), ("coefficient", "i64")]), - ("ILP", [("variable", "bool"), ("coefficient", "f64")]), } register_ilp_pipeline! { ("ThreePartition", []), ("ResourceConstrainedScheduling", []), ("ILP", [("variable", "bool"), ("coefficient", "i64")]), - ("ILP", [("variable", "bool"), ("coefficient", "f64")]), } register_ilp_pipeline! { ("TravelingSalesman", [("graph", "SimpleGraph"), ("weight", "i64")]), ("ILP", [("variable", "bool"), ("coefficient", "i64")]), - ("ILP", [("variable", "bool"), ("coefficient", "f64")]), } register_ilp_pipeline! { ("UndirectedFlowLowerBounds", []), ("ILP", [("variable", "i64"), ("coefficient", "i64")]), - ("ILP", [("variable", "i64"), ("coefficient", "f64")]), } register_ilp_pipeline! { ("UndirectedTwoCommodityIntegralFlow", []), ("ILP", [("variable", "i64"), ("coefficient", "i64")]), - ("ILP", [("variable", "i64"), ("coefficient", "f64")]), } diff --git a/src/solvers/registry.rs b/src/solvers/registry.rs index c044e3d3..f1db3898 100644 --- a/src/solvers/registry.rs +++ b/src/solvers/registry.rs @@ -1,5 +1,7 @@ //! Deterministic solver capabilities for exact problem variants. +use super::ilp::adapter::HighsAdapter; +use crate::models::algebraic::ILP; use crate::registry::VariantEntry; use crate::rules::registry::{reduction_entries, AggregateReduceFn, ReduceFn, ReductionEntry}; use crate::rules::DynReductionResult; @@ -8,6 +10,22 @@ use std::any::Any; use std::collections::{BTreeMap, BTreeSet}; use std::sync::OnceLock; +/// Type erasure is resolved at the registry boundary, never inside the adapter. +fn solve_ilp_terminal( + source: &dyn Any, + adapter: &HighsAdapter, +) -> Result, super::ILPSolveError> { + macro_rules! dispatch { + ($($v:ty, $c:ty);* $(;)?) => { $( + if let Some(ilp) = source.downcast_ref::>() { + return adapter.solve(ilp).map_err(Into::into); + } + )* }; + } + dispatch! { bool, i64; i64, i64; bool, f64; i64, f64; } + Err(super::ILPSolveError::UnsupportedProblemType) +} + /// Canonical identity of one concrete problem variant. #[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Serialize)] pub struct ExactProblemKey { @@ -53,7 +71,10 @@ impl ExactProblemKey { self.variant.get("variable").map(String::as_str), Some("bool" | "i64") ) - && self.variant.get("coefficient").map(String::as_str) == Some("f64") + && matches!( + self.variant.get("coefficient").map(String::as_str), + Some("i64" | "f64") + ) } } @@ -118,14 +139,14 @@ impl CompiledIlpPipeline { fn solve_with( &self, source: &dyn Any, - solver: &super::ILPSolver, + adapter: &HighsAdapter, finish: impl FnOnce( Box, Option<&dyn DynReductionResult>, ) -> Result, ) -> Result { if self.reducers.is_empty() { - return finish(Box::new(solver.solve_dyn(source)?), None); + return finish(Box::new(solve_ilp_terminal(source, adapter)?), None); } let mut reductions: Vec> = Vec::new(); @@ -141,7 +162,7 @@ impl CompiledIlpPipeline { .last() .expect("non-empty fixed pipeline must produce a target") .target_problem_any(); - let solution = solver.solve_dyn(target)?; + let solution = solve_ilp_terminal(target, adapter)?; let mut source_solution: Box = Box::new(solution); for (index, step) in reductions.iter().enumerate().rev() { if let Some(reduce) = self.reducers[index].1 { @@ -168,9 +189,9 @@ impl CompiledIlpPipeline { pub(crate) fn solve( &self, source: &dyn Any, - solver: &super::ILPSolver, + adapter: &HighsAdapter, ) -> Result { - self.solve_with(source, solver, |solution, first_reduction| { + self.solve_with(source, adapter, |solution, first_reduction| { if let Some(reduction) = first_reduction { return reduction .source_solution_json(solution.as_ref()) @@ -188,9 +209,9 @@ impl CompiledIlpPipeline { pub(crate) fn solve_typed( &self, source: &dyn Any, - solver: &super::ILPSolver, + adapter: &HighsAdapter, ) -> Result { - self.solve_with(source, solver, |solution, _| { + self.solve_with(source, adapter, |solution, _| { solution .downcast::() .map(|solution| *solution) @@ -287,7 +308,7 @@ pub enum RegistryBuildError { MissingSolverCapability(String), #[error("ILP pipeline must contain at least one node")] EmptyPipeline, - #[error("ILP pipeline for {0} does not end at an f64-coefficient ILP")] + #[error("ILP pipeline for {0} does not end at a supported native ILP")] UnsupportedTarget(String), #[error("ILP pipeline for {0} continues after reaching a supported ILP node")] ContinuesAfterIlp(String), diff --git a/src/solvers/resolver.rs b/src/solvers/resolver.rs index 6444d125..ec263f67 100644 --- a/src/solvers/resolver.rs +++ b/src/solvers/resolver.rs @@ -72,7 +72,10 @@ fn solve_ilp( problem: &LoadedDynProblem, pipeline: &CompiledIlpPipeline, ) -> Result { - let outcome = match pipeline.solve(problem.as_any(), &super::ILPSolver::new()) { + let outcome = match pipeline.solve( + problem.as_any(), + &super::ilp::adapter::HighsAdapter::new(None), + ) { Ok(solution) => SolveOutcome::Optimal { evaluation: problem.evaluate_dyn(&solution)?, solution, diff --git a/src/types.rs b/src/types.rs index 03146feb..f3a450e5 100644 --- a/src/types.rs +++ b/src/types.rs @@ -38,7 +38,8 @@ pub fn i64_to_exact_f64(value: i64) -> Result { } } -/// Bound for objective value types (i64, f64, etc.) +/// Bound for objective value types (i64, f64, etc.). +/// Integers reject overflow; floats allow rounding and reject non-finite results. pub trait NumericSize: Clone + Default @@ -49,9 +50,9 @@ pub trait NumericSize: + std::ops::AddAssign + 'static { - /// Add two values when the exact result remains representable and finite. + /// Checked addition. fn checked_add_value(self, other: Self) -> Result; - /// Multiply two values when the exact result remains representable and finite. + /// Checked multiplication. fn checked_mul_value(self, other: Self) -> Result; } diff --git a/src/unit_tests/rules/acyclicpartition_ilp.rs b/src/unit_tests/rules/acyclicpartition_ilp.rs index 467d43ae..bf5bec2c 100644 --- a/src/unit_tests/rules/acyclicpartition_ilp.rs +++ b/src/unit_tests/rules/acyclicpartition_ilp.rs @@ -82,7 +82,10 @@ fn test_infeasible_instance() { ReduceTo::>::reduce_to(&source).expect("reduction should succeed"); let ilp = reduction.target_problem(); let solver = ILPSolver::new(); - assert!(solver.solve(ilp).is_err()); + assert_eq!( + solver.solve(ilp), + Err(crate::solvers::ILPSolveError::Infeasible) + ); } #[test] diff --git a/src/unit_tests/rules/balancedcompletebipartitesubgraph_ilp.rs b/src/unit_tests/rules/balancedcompletebipartitesubgraph_ilp.rs index f044cf0f..630fb7c6 100644 --- a/src/unit_tests/rules/balancedcompletebipartitesubgraph_ilp.rs +++ b/src/unit_tests/rules/balancedcompletebipartitesubgraph_ilp.rs @@ -45,7 +45,10 @@ fn test_infeasible_instance() { ReduceTo::>::reduce_to(&source).expect("reduction should succeed"); let ilp = reduction.target_problem(); let solver = crate::solvers::ILPSolver::new(); - assert!(solver.solve(ilp).is_err()); + assert_eq!( + solver.solve(ilp), + Err(crate::solvers::ILPSolveError::Infeasible) + ); } #[test] diff --git a/src/unit_tests/rules/bottlenecktravelingsalesman_ilp.rs b/src/unit_tests/rules/bottlenecktravelingsalesman_ilp.rs index 5d92bc4b..6b4bb126 100644 --- a/src/unit_tests/rules/bottlenecktravelingsalesman_ilp.rs +++ b/src/unit_tests/rules/bottlenecktravelingsalesman_ilp.rs @@ -96,8 +96,9 @@ fn test_no_hamiltonian_cycle_infeasible() { ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); let ilp_solver = ILPSolver::new(); let result = ilp_solver.solve(reduction.target_problem()); - assert!( - result.is_err(), + assert_eq!( + result, + Err(crate::solvers::ILPSolveError::Infeasible), "Path graph should have no Hamiltonian cycle" ); } diff --git a/src/unit_tests/rules/boundedcomponentspanningforest_ilp.rs b/src/unit_tests/rules/boundedcomponentspanningforest_ilp.rs index 039d7f2c..be431329 100644 --- a/src/unit_tests/rules/boundedcomponentspanningforest_ilp.rs +++ b/src/unit_tests/rules/boundedcomponentspanningforest_ilp.rs @@ -85,7 +85,10 @@ fn test_infeasible_instance() { ReduceTo::>::reduce_to(&source).expect("reduction should succeed"); let ilp = reduction.target_problem(); let solver = ILPSolver::new(); - assert!(solver.solve(ilp).is_err()); + assert_eq!( + solver.solve(ilp), + Err(crate::solvers::ILPSolveError::Infeasible) + ); } #[test] diff --git a/src/unit_tests/rules/clustering_ilp.rs b/src/unit_tests/rules/clustering_ilp.rs index 2142986b..ab374fa2 100644 --- a/src/unit_tests/rules/clustering_ilp.rs +++ b/src/unit_tests/rules/clustering_ilp.rs @@ -76,5 +76,8 @@ fn test_clustering_to_ilp_infeasible_instance_is_infeasible() { let reduction: ReductionClusteringToILP = ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); - assert!(ILPSolver::new().solve(reduction.target_problem()).is_err()); + assert_eq!( + ILPSolver::new().solve(reduction.target_problem()), + Err(crate::solvers::ILPSolveError::Infeasible) + ); } diff --git a/src/unit_tests/rules/coloring_ilp.rs b/src/unit_tests/rules/coloring_ilp.rs index 238d2cba..5625dea4 100644 --- a/src/unit_tests/rules/coloring_ilp.rs +++ b/src/unit_tests/rules/coloring_ilp.rs @@ -126,8 +126,9 @@ fn test_ilp_infeasible_triangle_2_colors() { // ILP should be infeasible let result = ilp_solver.solve(ilp); - assert!( - result.is_err(), + assert_eq!( + result, + Err(crate::solvers::ILPSolveError::Infeasible), "Triangle with 2 colors should be infeasible" ); } @@ -216,7 +217,11 @@ fn test_complete_graph_k4_with_3_colors_infeasible() { let ilp_solver = ILPSolver::new(); let result = ilp_solver.solve(ilp); - assert!(result.is_err(), "K4 with 3 colors should be infeasible"); + assert_eq!( + result, + Err(crate::solvers::ILPSolveError::Infeasible), + "K4 with 3 colors should be infeasible" + ); } #[test] diff --git a/src/unit_tests/rules/consistencyofdatabasefrequencytables_ilp.rs b/src/unit_tests/rules/consistencyofdatabasefrequencytables_ilp.rs index c9e07b86..a65b3a63 100644 --- a/src/unit_tests/rules/consistencyofdatabasefrequencytables_ilp.rs +++ b/src/unit_tests/rules/consistencyofdatabasefrequencytables_ilp.rs @@ -65,7 +65,10 @@ fn test_cdft_to_ilp_unsat_instance_is_infeasible() { let reduction: ReductionCDFTToILP = ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); let solver = ILPSolver::new(); - assert!(solver.solve(reduction.target_problem()).is_err()); + assert_eq!( + solver.solve(reduction.target_problem()), + Err(crate::solvers::ILPSolveError::Infeasible) + ); } #[test] diff --git a/src/unit_tests/rules/directedhamiltonianpath_ilp.rs b/src/unit_tests/rules/directedhamiltonianpath_ilp.rs index ad9c9f1b..a0fcccd2 100644 --- a/src/unit_tests/rules/directedhamiltonianpath_ilp.rs +++ b/src/unit_tests/rules/directedhamiltonianpath_ilp.rs @@ -89,8 +89,9 @@ fn test_directedhamiltonianpath_to_ilp_no_path() { ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); let ilp_solver = ILPSolver::new(); let result = ilp_solver.solve(reduction.target_problem()); - assert!( - result.is_err(), + assert_eq!( + result, + Err(crate::solvers::ILPSolveError::Infeasible), "Graph with no Hamiltonian path should be infeasible" ); } diff --git a/src/unit_tests/rules/directedtwocommodityintegralflow_ilp.rs b/src/unit_tests/rules/directedtwocommodityintegralflow_ilp.rs index d438da77..61eb1b7a 100644 --- a/src/unit_tests/rules/directedtwocommodityintegralflow_ilp.rs +++ b/src/unit_tests/rules/directedtwocommodityintegralflow_ilp.rs @@ -97,8 +97,9 @@ fn test_directedtwocommodityintegralflow_to_ilp_infeasible() { let problem = infeasible_instance(); let reduction: ReductionD2CIFToILP = ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); - assert!( - ILPSolver::new().solve(reduction.target_problem()).is_err(), + assert_eq!( + ILPSolver::new().solve(reduction.target_problem()), + Err(crate::solvers::ILPSolveError::Infeasible), "infeasible flow instance should produce infeasible ILP" ); } @@ -110,8 +111,9 @@ fn test_directedtwocommodityintegralflow_to_ilp_disallows_using_other_commodity_ let reduction: ReductionD2CIFToILP = ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); - assert!( - ILPSolver::new().solve(reduction.target_problem()).is_err(), + assert_eq!( + ILPSolver::new().solve(reduction.target_problem()), + Err(crate::solvers::ILPSolveError::Infeasible), "commodity 1 must conserve flow at commodity 2's source in the ILP reduction" ); } diff --git a/src/unit_tests/rules/ensemblecomputation_ilp.rs b/src/unit_tests/rules/ensemblecomputation_ilp.rs index 3be3c0aa..319cdca2 100644 --- a/src/unit_tests/rules/ensemblecomputation_ilp.rs +++ b/src/unit_tests/rules/ensemblecomputation_ilp.rs @@ -30,14 +30,20 @@ fn test_ensemblecomputation_to_ilp_closed_loop() { fn test_ensemblecomputation_to_ilp_infeasible_budget() { let source = EnsembleComputation::new(3, vec![vec![0, 1, 2]], 1); let reduction = ReduceTo::>::reduce_to(&source).unwrap(); - assert!(ILPSolver::new().solve(reduction.target_problem()).is_err()); + assert_eq!( + ILPSolver::new().solve(reduction.target_problem()), + Err(crate::solvers::ILPSolveError::Infeasible) + ); } #[test] fn test_ensemblecomputation_to_ilp_rejects_singleton_target() { let source = EnsembleComputation::new(3, vec![vec![0]], 2); let reduction = ReduceTo::>::reduce_to(&source).unwrap(); - assert!(ILPSolver::new().solve(reduction.target_problem()).is_err()); + assert_eq!( + ILPSolver::new().solve(reduction.target_problem()), + Err(crate::solvers::ILPSolveError::Infeasible) + ); } #[test] diff --git a/src/unit_tests/rules/eulerianpath_ilp.rs b/src/unit_tests/rules/eulerianpath_ilp.rs index 7d9f5c3c..3c95e24c 100644 --- a/src/unit_tests/rules/eulerianpath_ilp.rs +++ b/src/unit_tests/rules/eulerianpath_ilp.rs @@ -87,8 +87,9 @@ fn test_eulerianpath_to_ilp_infeasible_no_instance() { // The ILP must report infeasibility for a NO instance. let solution = ILPSolver::new().solve(reduction.target_problem()); - assert!( - solution.is_err(), + assert_eq!( + solution, + Err(crate::solvers::ILPSolveError::Infeasible), "ILP must be infeasible for a degree-unbalanced NO instance, got {:?}", solution ); diff --git a/src/unit_tests/rules/factoring_ilp.rs b/src/unit_tests/rules/factoring_ilp.rs index 472e9701..11bf3f1c 100644 --- a/src/unit_tests/rules/factoring_ilp.rs +++ b/src/unit_tests/rules/factoring_ilp.rs @@ -171,7 +171,11 @@ fn test_infeasible_target_too_large() { let ilp_solver = ILPSolver::new(); let result = ilp_solver.solve(ilp); - assert!(result.is_err(), "Should be infeasible"); + assert_eq!( + result, + Err(crate::solvers::ILPSolveError::Infeasible), + "Should be infeasible" + ); } #[test] @@ -277,7 +281,10 @@ fn test_oversized_biguint_target_makes_ilp_infeasible() { let target = BigUint::from(1u32) << 70; let problem = Factoring::with_factor_bits(target, 2, 2); let reduction = ReduceTo::>::reduce_to(&problem).unwrap(); - assert!(ILPSolver::new().solve(reduction.target_problem()).is_err()); + assert_eq!( + ILPSolver::new().solve(reduction.target_problem()), + Err(crate::solvers::ILPSolveError::Infeasible) + ); } #[test] diff --git a/src/unit_tests/rules/feasibleregisterassignment_ilp.rs b/src/unit_tests/rules/feasibleregisterassignment_ilp.rs index 6b18e1e2..5a58bdae 100644 --- a/src/unit_tests/rules/feasibleregisterassignment_ilp.rs +++ b/src/unit_tests/rules/feasibleregisterassignment_ilp.rs @@ -40,8 +40,9 @@ fn test_feasible_register_assignment_to_ilp_infeasible() { let source = FeasibleRegisterAssignment::new(3, vec![(0, 1), (0, 2), (1, 2)], 1, vec![0, 0, 0]); let reduction = ReduceTo::>::reduce_to(&source).expect("reduction should succeed"); - assert!( - ILPSolver::new().solve(reduction.target_problem()).is_err(), + assert_eq!( + ILPSolver::new().solve(reduction.target_problem()), + Err(crate::solvers::ILPSolveError::Infeasible), "register-conflict source instance should reduce to an infeasible ILP" ); } diff --git a/src/unit_tests/rules/flowshopscheduling_ilp.rs b/src/unit_tests/rules/flowshopscheduling_ilp.rs index 34da74e8..6bf8a37e 100644 --- a/src/unit_tests/rules/flowshopscheduling_ilp.rs +++ b/src/unit_tests/rules/flowshopscheduling_ilp.rs @@ -33,8 +33,9 @@ fn test_flowshopscheduling_to_ilp_infeasible() { // 2 machines, 3 jobs with large processing times, very tight deadline let problem = FlowShopScheduling::new(2, vec![vec![5, 5], vec![5, 5], vec![5, 5]], 6); let reduction = ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); - assert!( - ILPSolver::new().solve(reduction.target_problem()).is_err(), + assert_eq!( + ILPSolver::new().solve(reduction.target_problem()), + Err(crate::solvers::ILPSolveError::Infeasible), "infeasible FSS should produce infeasible ILP" ); } diff --git a/src/unit_tests/rules/hamiltonianpath_ilp.rs b/src/unit_tests/rules/hamiltonianpath_ilp.rs index 66a2a897..e9bd5b74 100644 --- a/src/unit_tests/rules/hamiltonianpath_ilp.rs +++ b/src/unit_tests/rules/hamiltonianpath_ilp.rs @@ -83,8 +83,9 @@ fn test_hamiltonianpath_to_ilp_no_path() { ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); let ilp_solver = ILPSolver::new(); let result = ilp_solver.solve(reduction.target_problem()); - assert!( - result.is_err(), + assert_eq!( + result, + Err(crate::solvers::ILPSolveError::Infeasible), "Disconnected graph should have no Hamiltonian path" ); } diff --git a/src/unit_tests/rules/ilp_i64_ilp_bool.rs b/src/unit_tests/rules/ilp_i64_ilp_bool.rs index 75be4dc9..027b6915 100644 --- a/src/unit_tests/rules/ilp_i64_ilp_bool.rs +++ b/src/unit_tests/rules/ilp_i64_ilp_bool.rs @@ -22,8 +22,16 @@ fn integer_ilp( fn solve_via_bool(source: &ILP) -> Option<(Vec, i64)> { let reduction = ReduceTo::>::reduce_to(source).expect("reduction should succeed"); - let witness = ILPSolver::new().solve(reduction.target_problem()).ok()?; + let witness = match ILPSolver::new().solve(reduction.target_problem()) { + Ok(solution) => solution, + Err(crate::solvers::ILPSolveError::Infeasible) => return None, + Err(error) => panic!("ILP execution failed: {error}"), + }; let source_solution = reduction.extract_solution(&witness).unwrap(); + assert!( + source.is_feasible(&source_solution).unwrap(), + "decoded integer ILP solution must be feasible" + ); let objective = source.evaluate_objective(&source_solution).unwrap(); Some((source_solution, objective)) } @@ -39,8 +47,7 @@ fn test_ilp_i64_to_ilp_bool_closed_loop() { vec![(0, -5), (1, -6)], ObjectiveSense::Minimize, ); - let (solution, objective) = solve_via_bool(&source).unwrap(); - assert!(source.is_feasible(&solution).unwrap()); + let (_, objective) = solve_via_bool(&source).unwrap(); assert_eq!(objective, -27); } @@ -52,8 +59,7 @@ fn test_ilp_i64_to_ilp_bool_maximize() { vec![(0, 3), (1, 5)], ObjectiveSense::Maximize, ); - let (solution, objective) = solve_via_bool(&source).unwrap(); - assert!(source.is_feasible(&solution).unwrap()); + let (_, objective) = solve_via_bool(&source).unwrap(); assert_eq!(objective, 24); } @@ -98,8 +104,7 @@ fn test_ilp_i64_to_ilp_bool_equality_constraint() { vec![(0, 1)], ObjectiveSense::Minimize, ); - let (solution, objective) = solve_via_bool(&source).unwrap(); - assert!(source.is_feasible(&solution).unwrap()); + let (_, objective) = solve_via_bool(&source).unwrap(); assert_eq!(objective, 1); } @@ -130,7 +135,10 @@ fn test_ilp_i64_to_ilp_bool_infeasible() { ObjectiveSense::Minimize, ); let reduction = ReduceTo::>::reduce_to(&source).unwrap(); - assert!(ILPSolver::new().solve(reduction.target_problem()).is_err()); + assert_eq!( + ILPSolver::new().solve(reduction.target_problem()), + Err(crate::solvers::ILPSolveError::Infeasible) + ); } #[test] diff --git a/src/unit_tests/rules/ilp_casts.rs b/src/unit_tests/rules/ilp_i64_ilp_f64.rs similarity index 74% rename from src/unit_tests/rules/ilp_casts.rs rename to src/unit_tests/rules/ilp_i64_ilp_f64.rs index 2784c1f0..3c9c6c8f 100644 --- a/src/unit_tests/rules/ilp_casts.rs +++ b/src/unit_tests/rules/ilp_i64_ilp_f64.rs @@ -1,6 +1,6 @@ use super::*; use crate::models::algebraic::{IntegerVariable, ObjectiveSense}; -use crate::rules::ReductionGraph; +use crate::rules::{ReductionGraph, ReductionResult}; use crate::solvers::ILPSolver; use crate::types::MAX_EXACT_F64_INTEGER; @@ -35,7 +35,7 @@ fn test_ilp_i64_coefficients_to_f64_rejects_inexact_value() { let source = ILP::::new( 1, vec![], - vec![(0, MAX_EXACT_F64_INTEGER + 1)], + vec![(0, MAX_EXACT_F64_INTEGER + 2)], ObjectiveSense::Minimize, ) .unwrap(); @@ -47,7 +47,7 @@ fn test_ilp_i64_coefficients_to_f64_rejects_inexact_value() { } #[test] -fn test_ilp_cast_rechecks_source_feasibility() { +fn test_ilp_integer_coefficients_preserve_large_exact_constraint() { let rhs = 1_000_000_000_000_i64; let source = ILP::::with_variables( vec![IntegerVariable::nonnegative()], @@ -57,13 +57,26 @@ fn test_ilp_cast_rechecks_source_feasibility() { ) .unwrap(); let reduction = ReduceTo::>::reduce_to(&source).unwrap(); - let target_solution = vec![rhs + 1]; + assert_eq!(reduction.target_problem().variables(), source.variables()); + assert_eq!( + reduction.target_problem().constraints()[0].terms(), + &[(0, 1.0)] + ); + assert_eq!( + reduction.target_problem().constraints()[0].rhs(), + rhs as f64 + ); + let target_solution = vec![rhs]; assert!(reduction .target_problem() .is_feasible(&target_solution) .unwrap()); - assert!(reduction.extract_solution(&target_solution).is_err()); + assert_eq!( + reduction.extract_solution(&target_solution).unwrap(), + target_solution + ); + assert!(reduction.extract_solution(&vec![]).is_err()); } #[test] diff --git a/src/unit_tests/rules/integralflowbundles_ilp.rs b/src/unit_tests/rules/integralflowbundles_ilp.rs index b9bea620..aad2e688 100644 --- a/src/unit_tests/rules/integralflowbundles_ilp.rs +++ b/src/unit_tests/rules/integralflowbundles_ilp.rs @@ -99,7 +99,10 @@ fn test_integral_flow_bundles_to_ilp_unsat_instance_is_infeasible() { let problem = no_instance(); let reduction: ReductionIFBToILP = ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); - assert!(ILPSolver::new().solve(reduction.target_problem()).is_err()); + assert_eq!( + ILPSolver::new().solve(reduction.target_problem()), + Err(crate::solvers::ILPSolveError::Infeasible) + ); } #[test] diff --git a/src/unit_tests/rules/ksatisfiability_directedtwocommodityintegralflow.rs b/src/unit_tests/rules/ksatisfiability_directedtwocommodityintegralflow.rs index 6ad979fc..908cf250 100644 --- a/src/unit_tests/rules/ksatisfiability_directedtwocommodityintegralflow.rs +++ b/src/unit_tests/rules/ksatisfiability_directedtwocommodityintegralflow.rs @@ -40,9 +40,17 @@ fn solve_target_via_ilp( problem: &crate::models::graph::DirectedTwoCommodityIntegralFlow, ) -> Option> { let reduction = ReduceTo::>::reduce_to(problem).expect("reduction should succeed"); - let ilp_solution = ILPSolver::new().solve(reduction.target_problem()).ok()?; + let ilp_solution = match ILPSolver::new().solve(reduction.target_problem()) { + Ok(solution) => solution, + Err(crate::solvers::ILPSolveError::Infeasible) => return None, + Err(error) => panic!("ILP execution failed: {error}"), + }; let extracted = reduction.extract_solution(&ilp_solution).unwrap(); - problem.evaluate(&extracted).unwrap().0.then_some(extracted) + assert!( + problem.evaluate(&extracted).unwrap().0, + "decoded flow must be feasible" + ); + Some(extracted) } #[test] diff --git a/src/unit_tests/rules/ksatisfiability_feasibleregisterassignment.rs b/src/unit_tests/rules/ksatisfiability_feasibleregisterassignment.rs index ef7624f3..77643b59 100644 --- a/src/unit_tests/rules/ksatisfiability_feasibleregisterassignment.rs +++ b/src/unit_tests/rules/ksatisfiability_feasibleregisterassignment.rs @@ -170,8 +170,9 @@ fn test_ksatisfiability_to_feasible_register_assignment_unsatisfiable_instance() let fra_to_ilp = ReduceTo::>::reduce_to(reduction.target_problem()) .expect("reduction should succeed"); - assert!( - ILPSolver::new().solve(fra_to_ilp.target_problem()).is_err(), + assert_eq!( + ILPSolver::new().solve(fra_to_ilp.target_problem()), + Err(crate::solvers::ILPSolveError::Infeasible), "an unsatisfiable source formula should yield an infeasible FRA instance" ); } diff --git a/src/unit_tests/rules/ksatisfiability_preemptivescheduling.rs b/src/unit_tests/rules/ksatisfiability_preemptivescheduling.rs index 37550929..eb94c789 100644 --- a/src/unit_tests/rules/ksatisfiability_preemptivescheduling.rs +++ b/src/unit_tests/rules/ksatisfiability_preemptivescheduling.rs @@ -32,7 +32,11 @@ fn solve_threshold_schedule_via_ilp( target.precedences().to_vec(), ); let pcs_to_ilp = ReduceTo::>::reduce_to(&pcs).expect("reduction should succeed"); - let ilp_solution = ILPSolver::new().solve(pcs_to_ilp.target_problem()).ok()?; + let ilp_solution = match ILPSolver::new().solve(pcs_to_ilp.target_problem()) { + Ok(solution) => solution, + Err(crate::solvers::ILPSolveError::Infeasible) => return None, + Err(error) => panic!("ILP execution failed: {error}"), + }; let slot_assignment = pcs_to_ilp.extract_solution(&ilp_solution).unwrap(); let mut config = vec![vec![false; target.d_max()]; target.num_tasks()]; diff --git a/src/unit_tests/rules/ksatisfiability_timetabledesign.rs b/src/unit_tests/rules/ksatisfiability_timetabledesign.rs index e9c62c15..49d71132 100644 --- a/src/unit_tests/rules/ksatisfiability_timetabledesign.rs +++ b/src/unit_tests/rules/ksatisfiability_timetabledesign.rs @@ -118,10 +118,9 @@ fn test_ksatisfiability_to_timetabledesign_unsatisfiable() { let target_reduction = ReduceTo::>::reduce_to(reduction.target_problem()) .expect("timetable reduction should succeed"); - assert!( - ILPSolver::new() - .solve(target_reduction.target_problem()) - .is_err(), + assert_eq!( + ILPSolver::new().solve(target_reduction.target_problem()), + Err(crate::solvers::ILPSolveError::Infeasible), "unsatisfiable 3SAT instance should produce an infeasible timetable" ); } diff --git a/src/unit_tests/rules/minimumcapacitatedspanningtree_ilp.rs b/src/unit_tests/rules/minimumcapacitatedspanningtree_ilp.rs index e3725ece..f4a06e03 100644 --- a/src/unit_tests/rules/minimumcapacitatedspanningtree_ilp.rs +++ b/src/unit_tests/rules/minimumcapacitatedspanningtree_ilp.rs @@ -167,5 +167,8 @@ fn test_zero_requirement_vertex_still_must_be_connected() { ); let reduction: ReductionMinimumCapacitatedSpanningTreeToILP = ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); - assert!(ILPSolver::new().solve(reduction.target_problem()).is_err()); + assert_eq!( + ILPSolver::new().solve(reduction.target_problem()), + Err(crate::solvers::ILPSolveError::Infeasible) + ); } diff --git a/src/unit_tests/rules/minimumedgecostflow_ilp.rs b/src/unit_tests/rules/minimumedgecostflow_ilp.rs index 683d7505..fa9a3c65 100644 --- a/src/unit_tests/rules/minimumedgecostflow_ilp.rs +++ b/src/unit_tests/rules/minimumedgecostflow_ilp.rs @@ -109,8 +109,9 @@ fn test_minimumedgecostflow_to_ilp_infeasible() { let problem = infeasible_instance(); let reduction: ReductionMECFToILP = ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); - assert!( - ILPSolver::new().solve(reduction.target_problem()).is_err(), + assert_eq!( + ILPSolver::new().solve(reduction.target_problem()), + Err(crate::solvers::ILPSolveError::Infeasible), "infeasible instance should produce infeasible ILP" ); } diff --git a/src/unit_tests/rules/minimumfaultdetectiontestset_ilp.rs b/src/unit_tests/rules/minimumfaultdetectiontestset_ilp.rs index f2400cdf..7726cafa 100644 --- a/src/unit_tests/rules/minimumfaultdetectiontestset_ilp.rs +++ b/src/unit_tests/rules/minimumfaultdetectiontestset_ilp.rs @@ -83,7 +83,10 @@ fn test_reduction_is_infeasible_when_an_internal_vertex_has_no_covering_pair() { assert_eq!(problem.evaluate(&vec![vec![false]]).unwrap(), Min(None)); assert_eq!(problem.evaluate(&vec![vec![true]]).unwrap(), Min(None)); - assert!(ILPSolver::new().solve(ilp).is_err()); + assert_eq!( + ILPSolver::new().solve(ilp), + Err(crate::solvers::ILPSolveError::Infeasible) + ); } #[test] diff --git a/src/unit_tests/rules/minimumweightdecoding_ilp.rs b/src/unit_tests/rules/minimumweightdecoding_ilp.rs index 09eb2833..fa462ed7 100644 --- a/src/unit_tests/rules/minimumweightdecoding_ilp.rs +++ b/src/unit_tests/rules/minimumweightdecoding_ilp.rs @@ -96,8 +96,9 @@ fn test_minimumweightdecoding_to_ilp_infeasible() { let problem = infeasible_instance(); let reduction: ReductionMinimumWeightDecodingToILP = ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); - assert!( - ILPSolver::new().solve(reduction.target_problem()).is_err(), + assert_eq!( + ILPSolver::new().solve(reduction.target_problem()), + Err(crate::solvers::ILPSolveError::Infeasible), "infeasible instance should produce infeasible ILP" ); } diff --git a/src/unit_tests/rules/monochromatictriangle_ilp.rs b/src/unit_tests/rules/monochromatictriangle_ilp.rs index 0dbd944f..b2c0ba37 100644 --- a/src/unit_tests/rules/monochromatictriangle_ilp.rs +++ b/src/unit_tests/rules/monochromatictriangle_ilp.rs @@ -69,8 +69,9 @@ fn test_monochromatic_triangle_to_ilp_infeasible_k6() { let problem = MonochromaticTriangle::new(SimpleGraph::new(6, edges)); let reduction = ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); - assert!( - ILPSolver::new().solve(reduction.target_problem()).is_err(), + assert_eq!( + ILPSolver::new().solve(reduction.target_problem()), + Err(crate::solvers::ILPSolveError::Infeasible), "K6 should be infeasible by R(3,3)=6" ); } diff --git a/src/unit_tests/rules/multiplechoicebranching_ilp.rs b/src/unit_tests/rules/multiplechoicebranching_ilp.rs index 6166a118..6c995f89 100644 --- a/src/unit_tests/rules/multiplechoicebranching_ilp.rs +++ b/src/unit_tests/rules/multiplechoicebranching_ilp.rs @@ -21,7 +21,10 @@ fn test_multiplechoicebranching_to_ilp_closed_loop() { let actual = reduction.extract_solution(&target).unwrap(); assert!(problem.evaluate(&actual).unwrap().0); } - None => assert!(ILPSolver::new().solve(reduction.target_problem()).is_err()), + None => assert_eq!( + ILPSolver::new().solve(reduction.target_problem()), + Err(crate::solvers::ILPSolveError::Infeasible) + ), } } } @@ -35,7 +38,10 @@ fn test_multiplechoicebranching_to_ilp_rejects_forced_cycle() { 2, ); let reduction = ReduceTo::>::reduce_to(&problem).unwrap(); - assert!(ILPSolver::new().solve(reduction.target_problem()).is_err()); + assert_eq!( + ILPSolver::new().solve(reduction.target_problem()), + Err(crate::solvers::ILPSolveError::Infeasible) + ); } #[test] diff --git a/src/unit_tests/rules/naesatisfiability_ilp.rs b/src/unit_tests/rules/naesatisfiability_ilp.rs index 287d5765..4e168df0 100644 --- a/src/unit_tests/rules/naesatisfiability_ilp.rs +++ b/src/unit_tests/rules/naesatisfiability_ilp.rs @@ -80,8 +80,9 @@ fn test_naesatisfiability_to_ilp_infeasible() { let ilp_solver = ILPSolver::new(); // The ILP should be infeasible: x1 ≥ 1 (at least one true) AND x1 ≤ 0 (at least one false) - assert!( - ilp_solver.solve(ilp).is_err(), + assert_eq!( + ilp_solver.solve(ilp), + Err(crate::solvers::ILPSolveError::Infeasible), "ILP should be infeasible for unsatisfiable NAE-SAT" ); } diff --git a/src/unit_tests/rules/numericalmatchingwithtargetsums_ilp.rs b/src/unit_tests/rules/numericalmatchingwithtargetsums_ilp.rs index 69712934..50dc188a 100644 --- a/src/unit_tests/rules/numericalmatchingwithtargetsums_ilp.rs +++ b/src/unit_tests/rules/numericalmatchingwithtargetsums_ilp.rs @@ -55,8 +55,9 @@ fn test_numericalmatchingwithtargetsums_to_ilp_unsatisfiable() { let problem = NumericalMatchingWithTargetSums::new(vec![1, 2], vec![3, 4], vec![10, 20]); let reduction = ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); let result = ILPSolver::new().solve(reduction.target_problem()); - assert!( - result.is_err(), + assert_eq!( + result, + Err(crate::solvers::ILPSolveError::Infeasible), "Unsatisfiable instance should have no ILP solution" ); } diff --git a/src/unit_tests/rules/partitionintocliques_ilp.rs b/src/unit_tests/rules/partitionintocliques_ilp.rs index 564c1af2..39a13c9c 100644 --- a/src/unit_tests/rules/partitionintocliques_ilp.rs +++ b/src/unit_tests/rules/partitionintocliques_ilp.rs @@ -29,5 +29,8 @@ fn test_partitionintocliques_to_ilp_preserves_infeasibility() { let problem = PartitionIntoCliques::new(SimpleGraph::new(3, vec![]), 2); let reduction = ReduceTo::>::reduce_to(&problem).unwrap(); - assert!(ILPSolver::new().solve(reduction.target_problem()).is_err()); + assert_eq!( + ILPSolver::new().solve(reduction.target_problem()), + Err(crate::solvers::ILPSolveError::Infeasible) + ); } diff --git a/src/unit_tests/rules/precedenceconstrainedscheduling_ilp.rs b/src/unit_tests/rules/precedenceconstrainedscheduling_ilp.rs index 3f54767c..a454000d 100644 --- a/src/unit_tests/rules/precedenceconstrainedscheduling_ilp.rs +++ b/src/unit_tests/rules/precedenceconstrainedscheduling_ilp.rs @@ -60,8 +60,9 @@ fn test_precedenceconstrainedscheduling_to_ilp_infeasible() { let problem = infeasible_instance(); let reduction: ReductionPCSToILP = ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); - assert!( - ILPSolver::new().solve(reduction.target_problem()).is_err(), + assert_eq!( + ILPSolver::new().solve(reduction.target_problem()), + Err(crate::solvers::ILPSolveError::Infeasible), "infeasible scheduling instance should produce infeasible ILP" ); } diff --git a/src/unit_tests/rules/registersufficiency_ilp.rs b/src/unit_tests/rules/registersufficiency_ilp.rs index beaef71d..aa1621b4 100644 --- a/src/unit_tests/rules/registersufficiency_ilp.rs +++ b/src/unit_tests/rules/registersufficiency_ilp.rs @@ -63,8 +63,9 @@ fn test_register_sufficiency_to_ilp_infeasible() { let source = infeasible_example(); let reduction = ReduceTo::>::reduce_to(&source).expect("reduction should succeed"); - assert!( - ILPSolver::new().solve(reduction.target_problem()).is_err(), + assert_eq!( + ILPSolver::new().solve(reduction.target_problem()), + Err(crate::solvers::ILPSolveError::Infeasible), "register-sufficiency instance with bound one should be infeasible" ); } diff --git a/src/unit_tests/rules/resourceconstrainedscheduling_ilp.rs b/src/unit_tests/rules/resourceconstrainedscheduling_ilp.rs index 9e811bbd..eb2438fb 100644 --- a/src/unit_tests/rules/resourceconstrainedscheduling_ilp.rs +++ b/src/unit_tests/rules/resourceconstrainedscheduling_ilp.rs @@ -50,8 +50,9 @@ fn test_resourceconstrainedscheduling_to_ilp_infeasible() { let problem = ResourceConstrainedScheduling::new(1, vec![5], vec![vec![6], vec![6], vec![6]], 1).unwrap(); let reduction = ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); - assert!( - ILPSolver::new().solve(reduction.target_problem()).is_err(), + assert_eq!( + ILPSolver::new().solve(reduction.target_problem()), + Err(crate::solvers::ILPSolveError::Infeasible), "infeasible RCS should produce infeasible ILP" ); } diff --git a/src/unit_tests/rules/rootedtreestorageassignment_ilp.rs b/src/unit_tests/rules/rootedtreestorageassignment_ilp.rs index 737aec83..a19c78a9 100644 --- a/src/unit_tests/rules/rootedtreestorageassignment_ilp.rs +++ b/src/unit_tests/rules/rootedtreestorageassignment_ilp.rs @@ -42,9 +42,10 @@ fn test_rootedtreestorageassignment_to_ilp_bf_vs_ilp() { assert!(ilp_value.0, "ILP solution should be feasible"); assert!(bf_value.0, "BF should also find feasible solution"); } - Err(_) => { + Err(crate::solvers::ILPSolveError::Infeasible) => { assert!(!bf_value.0, "both should agree on infeasibility"); } + Err(error) => panic!("ILP execution failed: {error}"), } } @@ -66,7 +67,11 @@ fn test_rootedtreestorageassignment_to_ilp_infeasible() { let ilp_solver = ILPSolver::new(); let ilp_result = ilp_solver.solve(reduction.target_problem()); assert!(bf_witness.is_none(), "source should be infeasible"); - assert!(ilp_result.is_err(), "reduced ILP should also be infeasible"); + assert_eq!( + ilp_result, + Err(crate::solvers::ILPSolveError::Infeasible), + "reduced ILP should also be infeasible" + ); } #[test] diff --git a/src/unit_tests/rules/schedulingwithindividualdeadlines_ilp.rs b/src/unit_tests/rules/schedulingwithindividualdeadlines_ilp.rs index 8838a384..b5f90424 100644 --- a/src/unit_tests/rules/schedulingwithindividualdeadlines_ilp.rs +++ b/src/unit_tests/rules/schedulingwithindividualdeadlines_ilp.rs @@ -86,8 +86,9 @@ fn test_schedulingwithindividualdeadlines_to_ilp_infeasible() { let problem = infeasible_instance(); let reduction: ReductionSWIDToILP = ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); - assert!( - ILPSolver::new().solve(reduction.target_problem()).is_err(), + assert_eq!( + ILPSolver::new().solve(reduction.target_problem()), + Err(crate::solvers::ILPSolveError::Infeasible), "infeasible instance should yield infeasible ILP" ); } diff --git a/src/unit_tests/rules/sequencingtominimizeweightedcompletiontime_ilp.rs b/src/unit_tests/rules/sequencingtominimizeweightedcompletiontime_ilp.rs index 62b7503d..0d00777e 100644 --- a/src/unit_tests/rules/sequencingtominimizeweightedcompletiontime_ilp.rs +++ b/src/unit_tests/rules/sequencingtominimizeweightedcompletiontime_ilp.rs @@ -104,8 +104,9 @@ fn test_cyclic_precedence_instance_is_infeasible() { ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); let ilp = reduction.target_problem(); - assert!( - ILPSolver::new().solve(ilp).is_err(), + assert_eq!( + ILPSolver::new().solve(ilp), + Err(crate::solvers::ILPSolveError::Infeasible), "cyclic precedences should make the ILP infeasible" ); } diff --git a/src/unit_tests/rules/sequencingtominimizeweightedtardiness_ilp.rs b/src/unit_tests/rules/sequencingtominimizeweightedtardiness_ilp.rs index 3768789a..6c85b75b 100644 --- a/src/unit_tests/rules/sequencingtominimizeweightedtardiness_ilp.rs +++ b/src/unit_tests/rules/sequencingtominimizeweightedtardiness_ilp.rs @@ -43,8 +43,9 @@ fn test_sequencingtominimizeweightedtardiness_to_ilp_infeasible() { let problem = SequencingToMinimizeWeightedTardiness::new(vec![10, 10], vec![1, 1], vec![1, 1], 0); let reduction = ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); - assert!( - ILPSolver::new().solve(reduction.target_problem()).is_err(), + assert_eq!( + ILPSolver::new().solve(reduction.target_problem()), + Err(crate::solvers::ILPSolveError::Infeasible), "infeasible STMWT should produce infeasible ILP" ); } diff --git a/src/unit_tests/rules/sequencingwithdeadlinesandsetuptimes_ilp.rs b/src/unit_tests/rules/sequencingwithdeadlinesandsetuptimes_ilp.rs index 30a4bccc..c839db5b 100644 --- a/src/unit_tests/rules/sequencingwithdeadlinesandsetuptimes_ilp.rs +++ b/src/unit_tests/rules/sequencingwithdeadlinesandsetuptimes_ilp.rs @@ -49,8 +49,9 @@ fn test_sequencingwithdeadlinesandsetuptimes_to_ilp_infeasible() { let problem = SequencingWithDeadlinesAndSetUpTimes::new(vec![2, 2], vec![1, 1], vec![0, 0], vec![0]); let reduction = ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); - assert!( - ILPSolver::new().solve(reduction.target_problem()).is_err(), + assert_eq!( + ILPSolver::new().solve(reduction.target_problem()), + Err(crate::solvers::ILPSolveError::Infeasible), "infeasible instance should produce infeasible ILP" ); } diff --git a/src/unit_tests/rules/sequencingwithinintervals_ilp.rs b/src/unit_tests/rules/sequencingwithinintervals_ilp.rs index 4f4a6c7e..aaf208a3 100644 --- a/src/unit_tests/rules/sequencingwithinintervals_ilp.rs +++ b/src/unit_tests/rules/sequencingwithinintervals_ilp.rs @@ -72,8 +72,9 @@ fn test_sequencingwithinintervals_to_ilp_infeasible() { let problem = infeasible_instance(); let reduction: ReductionSWIToILP = ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); - assert!( - ILPSolver::new().solve(reduction.target_problem()).is_err(), + assert_eq!( + ILPSolver::new().solve(reduction.target_problem()), + Err(crate::solvers::ILPSolveError::Infeasible), "infeasible instance (forced overlap) should yield infeasible ILP" ); } diff --git a/src/unit_tests/rules/sequencingwithreleasetimesanddeadlines_ilp.rs b/src/unit_tests/rules/sequencingwithreleasetimesanddeadlines_ilp.rs index 2598ef27..a4b81ef0 100644 --- a/src/unit_tests/rules/sequencingwithreleasetimesanddeadlines_ilp.rs +++ b/src/unit_tests/rules/sequencingwithreleasetimesanddeadlines_ilp.rs @@ -38,8 +38,9 @@ fn test_sequencingwithreleasetimesanddeadlines_to_ilp_infeasible() { // Two tasks that can't both fit: both need time 0-1, but overlap let problem = SequencingWithReleaseTimesAndDeadlines::new(vec![2, 2], vec![0, 0], vec![2, 2]); let reduction = ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); - assert!( - ILPSolver::new().solve(reduction.target_problem()).is_err(), + assert_eq!( + ILPSolver::new().solve(reduction.target_problem()), + Err(crate::solvers::ILPSolveError::Infeasible), "infeasible SWRTD should produce infeasible ILP" ); } @@ -51,8 +52,9 @@ fn test_sequencingwithreleasetimesanddeadlines_to_ilp_rejects_empty_start_window let problem = SequencingWithReleaseTimesAndDeadlines::new(vec![14], vec![0], vec![13]); let reduction = ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); - assert!( - ILPSolver::new().solve(reduction.target_problem()).is_err(), + assert_eq!( + ILPSolver::new().solve(reduction.target_problem()), + Err(crate::solvers::ILPSolveError::Infeasible), "a task longer than its release-deadline window must make the ILP infeasible" ); } diff --git a/src/unit_tests/rules/setsplitting_ilp.rs b/src/unit_tests/rules/setsplitting_ilp.rs index c4cb8d97..ef7505fa 100644 --- a/src/unit_tests/rules/setsplitting_ilp.rs +++ b/src/unit_tests/rules/setsplitting_ilp.rs @@ -70,8 +70,9 @@ fn test_setsplitting_to_ilp_infeasible() { let ilp = reduction.target_problem(); let ilp_solver = ILPSolver::new(); - assert!( - ilp_solver.solve(ilp).is_err(), + assert_eq!( + ilp_solver.solve(ilp), + Err(crate::solvers::ILPSolveError::Infeasible), "ILP should be infeasible for unsplittable instance" ); } diff --git a/src/unit_tests/rules/shortestweightconstrainedpath_ilp.rs b/src/unit_tests/rules/shortestweightconstrainedpath_ilp.rs index 8fe434aa..1b2f1bb3 100644 --- a/src/unit_tests/rules/shortestweightconstrainedpath_ilp.rs +++ b/src/unit_tests/rules/shortestweightconstrainedpath_ilp.rs @@ -61,10 +61,11 @@ fn test_shortestweightconstrainedpath_to_ilp_bf_vs_ilp() { // Both should agree on the optimal length assert_eq!(ilp_value, bf_value); } - Err(_) => { + Err(crate::solvers::ILPSolveError::Infeasible) => { // ILP found no feasible solution; brute force should agree assert_eq!(bf_value, Min(None)); } + Err(error) => panic!("ILP execution failed: {error}"), } } diff --git a/src/unit_tests/rules/spinglass_qubo.rs b/src/unit_tests/rules/spinglass_qubo.rs index 7894a553..ce265c1c 100644 --- a/src/unit_tests/rules/spinglass_qubo.rs +++ b/src/unit_tests/rules/spinglass_qubo.rs @@ -2,6 +2,7 @@ use super::*; use crate::rules::test_helpers::assert_optimization_round_trip_from_optimization_target; use crate::solvers::BruteForce; use crate::solvers::BruteForceProblem as _; +use crate::traits::Problem; include!("../jl_helpers.rs"); #[test] @@ -207,3 +208,30 @@ fn test_jl_parity_rule_qubo_to_spinglass() { assert_eq!(best_source, jl_parse_bool_configs_set(&case["best_source"])); } } + +#[test] +fn test_qubo_to_spinglass_preserves_small_nonzero_coefficients() { + // Exact powers of two distinguish algebraic coefficient preservation from + // backend tolerances. The two scales expose both former pruning branches: + // q < 1e-10, and q > 1e-10 but q/4 < 1e-10. + for magnitude in [2.0_f64.powi(-40), 2.0_f64.powi(-32)] { + for sign in [-1.0, 1.0] { + let q = sign * magnitude; + let source = QUBO::::from_matrix(vec![vec![q, q], vec![0.0, 0.0]]).unwrap(); + let reduction = ReduceTo::>::reduce_to(&source).unwrap(); + let target = reduction.target_problem(); + assert_eq!(target.fields(), &[3.0 * q / 4.0, q / 4.0]); + assert_eq!(target.interactions(), vec![((0, 1), q / 4.0)]); + let offset = 3.0 * q / 4.0; + for left in [-1, 1] { + for right in [-1, 1] { + let spins = vec![left, right]; + let bits = reduction.extract_solution(&spins).unwrap(); + let source_value = source.evaluate(&bits).unwrap().0.unwrap(); + let target_value = target.evaluate(&spins).unwrap().0.unwrap(); + assert_eq!(source_value, target_value + offset); + } + } + } + } +} diff --git a/src/unit_tests/rules/stringtostringcorrection_ilp.rs b/src/unit_tests/rules/stringtostringcorrection_ilp.rs index 83354d82..9132a713 100644 --- a/src/unit_tests/rules/stringtostringcorrection_ilp.rs +++ b/src/unit_tests/rules/stringtostringcorrection_ilp.rs @@ -65,8 +65,9 @@ fn test_stringtostringcorrection_to_ilp_infeasible() { let reduction: ReductionSTSCToILP = ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); let ilp_solver = ILPSolver::new(); - assert!( - ilp_solver.solve(reduction.target_problem()).is_err(), + assert_eq!( + ilp_solver.solve(reduction.target_problem()), + Err(crate::solvers::ILPSolveError::Infeasible), "reduced ILP should also be infeasible" ); } diff --git a/src/unit_tests/rules/strongconnectivityaugmentation_ilp.rs b/src/unit_tests/rules/strongconnectivityaugmentation_ilp.rs index 0fd893b3..7d1b060d 100644 --- a/src/unit_tests/rules/strongconnectivityaugmentation_ilp.rs +++ b/src/unit_tests/rules/strongconnectivityaugmentation_ilp.rs @@ -95,7 +95,10 @@ fn test_infeasible_budget() { ReduceTo::>::reduce_to(&source).expect("reduction should succeed"); let ilp = reduction.target_problem(); let solver = ILPSolver::new(); - assert!(solver.solve(ilp).is_err()); + assert_eq!( + solver.solve(ilp), + Err(crate::solvers::ILPSolveError::Infeasible) + ); } #[test] diff --git a/src/unit_tests/rules/subgraphisomorphism_ilp.rs b/src/unit_tests/rules/subgraphisomorphism_ilp.rs index cd7fe8f0..cfb73ae6 100644 --- a/src/unit_tests/rules/subgraphisomorphism_ilp.rs +++ b/src/unit_tests/rules/subgraphisomorphism_ilp.rs @@ -84,7 +84,11 @@ fn test_subgraphisomorphism_to_ilp_infeasible() { ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); let ilp_solver = ILPSolver::new(); let result = ilp_solver.solve(reduction.target_problem()); - assert!(result.is_err(), "K3 in path should be infeasible"); + assert_eq!( + result, + Err(crate::solvers::ILPSolveError::Infeasible), + "K3 in path should be infeasible" + ); } #[test] diff --git a/src/unit_tests/rules/threedimensionalmatching_ilp.rs b/src/unit_tests/rules/threedimensionalmatching_ilp.rs index aaec26d1..30eebc23 100644 --- a/src/unit_tests/rules/threedimensionalmatching_ilp.rs +++ b/src/unit_tests/rules/threedimensionalmatching_ilp.rs @@ -3,7 +3,7 @@ use crate::models::algebraic::{Comparison, ObjectiveSense, ILP}; use crate::models::misc::{ResourceConstrainedScheduling, ThreePartition}; use crate::models::set::ThreeDimensionalMatching; use crate::rules::{ReduceTo, ReductionGraph, ReductionResult}; -use crate::solvers::{BruteForce, ILPSolveError, ILPSolver}; +use crate::solvers::{BruteForce, ILPSolver}; use crate::traits::Problem; use crate::types::Or; @@ -112,8 +112,9 @@ fn test_threedimensionalmatching_to_ilp_infeasible_instance() { BruteForce::new().solve(&problem).unwrap().is_none(), "source instance should be infeasible" ); - assert!( - ILPSolver::new().solve(reduction.target_problem()).is_err(), + assert_eq!( + ILPSolver::new().solve(reduction.target_problem()), + Err(crate::solvers::ILPSolveError::Infeasible), "reduced ILP should be infeasible" ); } @@ -138,11 +139,6 @@ fn test_threedimensionalmatching_to_ilp_direct_path_beats_indirect_chain() { let direct_source = direct.extract_solution(&direct_solution).unwrap(); assert_eq!(problem.evaluate(&direct_source).unwrap(), Or(true)); - let indirect_solution = solver.solve(indirect.target_problem()); - assert!( - matches!(indirect_solution, Err(ILPSolveError::Extraction(_))), - "the numerically unstable indirect ILP should be rejected: {indirect_solution:?}" - ); assert!(direct.target_problem().num_vars() < indirect.target_problem().num_vars()); assert!( direct.target_problem().constraints().len() < indirect.target_problem().constraints().len() diff --git a/src/unit_tests/rules/threedimensionalmatching_threepartition.rs b/src/unit_tests/rules/threedimensionalmatching_threepartition.rs index f9cb2913..23df83cf 100644 --- a/src/unit_tests/rules/threedimensionalmatching_threepartition.rs +++ b/src/unit_tests/rules/threedimensionalmatching_threepartition.rs @@ -123,3 +123,60 @@ fn test_threedimensionalmatching_to_threepartition_uncovered_coordinate_maps_to_ "target instance should be infeasible" ); } + +#[test] +fn test_threedimensionalmatching_to_threepartition_extracts_noncanonical_partition() { + let (source, reduction) = reduce(1, &[(0, 0, 0)]); + // A mathematically valid witness found independently by HiGHS. Both regular + // triples initially contain UPrime elements; filler triples mix pair IDs. + // Keep the witness fixed so this regression does not depend on the backend. + let witness = vec![ + 4, 0, 0, 4, 2, 1, 3, 3, 6, 0, 6, 4, 5, 5, 2, 1, 3, 1, 6, 2, 5, + ]; + assert!(reduction.target_problem().evaluate(&witness).unwrap().0); + let extracted = reduction.extract_solution(&witness).unwrap(); + assert_eq!(extracted, vec![true]); + assert!(source.evaluate(&extracted).unwrap().0); + // Group labels have no mathematical significance. + let relabeled = witness.iter().map(|group| 6 - group).collect(); + assert_eq!(reduction.extract_solution(&relabeled).unwrap(), extracted); +} + +#[test] +fn test_threedimensionalmatching_to_threepartition_equal_size_permutations() { + let (source, reduction) = reduce(2, &[(0, 0, 0), (0, 1, 1), (1, 0, 0), (1, 1, 1)]); + let target = reduction.target_problem(); + for matching in [[1, 0, 0, 1], [0, 1, 1, 0]] { + let mut witness = reduction.build_target_witness(&matching); + let mut exchanges = 0; + // Cumulative equal-size exchanges preserve a valid partition while + // exercising regular-item identities, mixed fillers, and dummy groups. + for left in 0..target.num_elements() { + for right in left + 1..target.num_elements() { + if target.sizes()[left] == target.sizes()[right] && witness[left] != witness[right] + { + witness.swap(left, right); + assert!(target.evaluate(&witness).unwrap().0); + let extracted = reduction.extract_solution(&witness).unwrap(); + assert!(source.evaluate(&extracted).unwrap().0); + exchanges += 1; + } + } + } + assert!(exchanges > 0); + } +} + +#[test] +fn test_threedimensionalmatching_to_threepartition_rejects_invalid_partitions() { + let (_, reduction) = reduce(1, &[(0, 0, 0)]); + let valid = reduction.build_target_witness(&[1]); + assert!(reduction.extract_solution(&vec![]).is_err()); + let mut invalid = valid.clone(); + invalid[0] = reduction.target_problem().num_groups(); + assert!(reduction.extract_solution(&invalid).is_err()); + assert!(reduction.extract_solution(&vec![0; valid.len()]).is_err()); + let mut wrong_sum = valid; + wrong_sum.swap(0, 2); + assert!(reduction.extract_solution(&wrong_sum).is_err()); +} diff --git a/src/unit_tests/rules/timetabledesign_ilp.rs b/src/unit_tests/rules/timetabledesign_ilp.rs index 14fd854a..eac518e1 100644 --- a/src/unit_tests/rules/timetabledesign_ilp.rs +++ b/src/unit_tests/rules/timetabledesign_ilp.rs @@ -51,8 +51,9 @@ fn test_timetabledesign_to_ilp_infeasible() { // Craftsman 0 available only in period 0, but needs 2 periods of work with task 0 let problem = TimetableDesign::new(1, 1, 1, vec![vec![true]], vec![vec![true]], vec![vec![2]]); let reduction = ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); - assert!( - ILPSolver::new().solve(reduction.target_problem()).is_err(), + assert_eq!( + ILPSolver::new().solve(reduction.target_problem()), + Err(crate::solvers::ILPSolveError::Infeasible), "infeasible TD should produce infeasible ILP" ); } diff --git a/src/unit_tests/rules/travelingsalesman_ilp.rs b/src/unit_tests/rules/travelingsalesman_ilp.rs index d7c63071..394a33de 100644 --- a/src/unit_tests/rules/travelingsalesman_ilp.rs +++ b/src/unit_tests/rules/travelingsalesman_ilp.rs @@ -108,8 +108,9 @@ fn test_no_hamiltonian_cycle_infeasible() { let ilp_solver = ILPSolver::new(); let result = ilp_solver.solve(ilp); - assert!( - result.is_err(), + assert_eq!( + result, + Err(crate::solvers::ILPSolveError::Infeasible), "Path graph should have no Hamiltonian cycle (infeasible ILP)" ); } diff --git a/src/unit_tests/rules/undirectedflowlowerbounds_ilp.rs b/src/unit_tests/rules/undirectedflowlowerbounds_ilp.rs index feb60cb9..98ee7436 100644 --- a/src/unit_tests/rules/undirectedflowlowerbounds_ilp.rs +++ b/src/unit_tests/rules/undirectedflowlowerbounds_ilp.rs @@ -76,8 +76,9 @@ fn test_undirectedflowlowerbounds_to_ilp_infeasible() { let problem = infeasible_instance(); let reduction: ReductionUFLBToILP = ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); - assert!( - ILPSolver::new().solve(reduction.target_problem()).is_err(), + assert_eq!( + ILPSolver::new().solve(reduction.target_problem()), + Err(crate::solvers::ILPSolveError::Infeasible), "infeasible instance should produce infeasible ILP" ); } diff --git a/src/unit_tests/rules/undirectedtwocommodityintegralflow_ilp.rs b/src/unit_tests/rules/undirectedtwocommodityintegralflow_ilp.rs index 1d8d087e..9fa8be74 100644 --- a/src/unit_tests/rules/undirectedtwocommodityintegralflow_ilp.rs +++ b/src/unit_tests/rules/undirectedtwocommodityintegralflow_ilp.rs @@ -116,8 +116,9 @@ fn test_undirectedtwocommodityintegralflow_to_ilp_infeasible() { let problem = infeasible_instance(); let reduction: ReductionU2CIFToILP = ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); - assert!( - ILPSolver::new().solve(reduction.target_problem()).is_err(), + assert_eq!( + ILPSolver::new().solve(reduction.target_problem()), + Err(crate::solvers::ILPSolveError::Infeasible), "infeasible flow instance should yield infeasible ILP" ); } @@ -136,7 +137,10 @@ fn test_other_commodity_source_cannot_create_flow() { ); let reduction: ReductionU2CIFToILP = ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); - assert!(ILPSolver::new().solve(reduction.target_problem()).is_err()); + assert_eq!( + ILPSolver::new().solve(reduction.target_problem()), + Err(crate::solvers::ILPSolveError::Infeasible) + ); } #[test] diff --git a/src/unit_tests/solvers/ilp/adapter.rs b/src/unit_tests/solvers/ilp/adapter.rs new file mode 100644 index 00000000..e9cdd244 --- /dev/null +++ b/src/unit_tests/solvers/ilp/adapter.rs @@ -0,0 +1,213 @@ +use super::*; +use crate::models::algebraic::{IntegerVariable, LinearConstraint}; + +#[test] +fn test_backend_errors_are_classified_without_losing_the_cause() { + assert_eq!( + classify_backend_error(ResolutionError::Infeasible, None), + IlpBackendError::Infeasible, + ); + assert_eq!( + classify_backend_error(ResolutionError::Unbounded, None), + IlpBackendError::Unbounded, + ); + assert_eq!( + classify_backend_error(ResolutionError::Other("NoSolutionFound"), Some(0.1)), + IlpBackendError::Timeout, + ); + assert!(matches!( + classify_backend_error(ResolutionError::Other("SolveError"), None), + IlpBackendError::BackendFailure(message) if message.contains("SolveError") + )); +} + +#[test] +fn native_terminals_return_the_input_ilp_solution_format() { + let adapter = HighsAdapter::new(None); + let boolean_integer = ILP::::new( + 2, + vec![LinearConstraint::le(vec![(0, 1), (1, 1)], 1)], + vec![(0, 1), (1, 2)], + ObjectiveSense::Maximize, + ) + .unwrap(); + let boolean_float = ILP::::new( + 2, + vec![LinearConstraint::le(vec![(0, 1.0), (1, 1.0)], 1.0)], + vec![(0, 1.0), (1, 2.0)], + ObjectiveSense::Maximize, + ) + .unwrap(); + let integer_integer = ILP::::with_variables( + vec![IntegerVariable::new(Some(-2), Some(3)).unwrap()], + vec![], + vec![(0, 1)], + ObjectiveSense::Minimize, + ) + .unwrap(); + let integer_float = ILP::::with_variables( + vec![IntegerVariable::new(Some(-2), Some(3)).unwrap()], + vec![], + vec![(0, 1.0)], + ObjectiveSense::Minimize, + ) + .unwrap(); + let values: [Vec; 4] = [ + adapter.solve(&boolean_integer).unwrap(), + adapter.solve(&boolean_float).unwrap(), + adapter.solve(&integer_integer).unwrap(), + adapter.solve(&integer_float).unwrap(), + ]; + assert_eq!(values, [vec![0, 1], vec![0, 1], vec![-2], vec![-2]]); +} + +#[test] +fn decoding_checks_shape_integrality_range_and_original_constraints() { + let ilp = ILP::::new( + 2, + vec![LinearConstraint::eq(vec![(0, 1), (1, 1)], 1)], + vec![(0, 1)], + ObjectiveSense::Maximize, + ) + .unwrap(); + assert_eq!( + decode_and_validate(&ilp, [1.00000001, 0.0]).unwrap(), + vec![1, 0] + ); + for raw in [ + vec![], + vec![1.0], + vec![1.0, 0.0, 0.0], + vec![1.0, 1.0], + vec![2.0, -1.0], + vec![0.5, 0.5], + vec![f64::NAN, 0.0], + vec![f64::INFINITY, 0.0], + vec![f64::NEG_INFINITY, 0.0], + vec![i64::MAX as f64, 0.0], + vec![i64::MIN as f64, 0.0], + ] { + assert!(matches!( + decode_and_validate(&ilp, raw), + Err(IlpBackendError::InvalidSolution(_)) + )); + } +} + +#[test] +fn integer_validation_does_not_use_float_row_tolerances() { + let coefficient = 1_i64 << 52; + let ilp = ILP::::new( + 1, + vec![LinearConstraint::le( + vec![(0, coefficient)], + coefficient - 1, + )], + vec![], + ObjectiveSense::Minimize, + ) + .unwrap(); + assert!(matches!( + decode_and_validate(&ilp, [1.0]), + Err(IlpBackendError::InvalidSolution(_)) + )); + let float = ILP::::new( + 1, + vec![LinearConstraint::le( + vec![(0, coefficient as f64)], + (coefficient - 1) as f64, + )], + vec![], + ObjectiveSense::Minimize, + ) + .unwrap(); + assert_eq!(decode_and_validate(&float, [1.0]).unwrap(), vec![1]); +} + +#[test] +fn validation_propagates_constraint_and_objective_overflow() { + let objective = ILP::::new( + 2, + vec![], + vec![(0, i64::MAX), (1, 1)], + ObjectiveSense::Maximize, + ) + .unwrap(); + let constraint = ILP::::new( + 2, + vec![LinearConstraint::le(vec![(0, i64::MAX), (1, 1)], 0)], + vec![], + ObjectiveSense::Maximize, + ) + .unwrap(); + for ilp in [objective, constraint] { + assert!(matches!( + decode_and_validate(&ilp, [1.0, 1.0]), + Err(IlpBackendError::InvalidSolution(_)) + )); + } +} + +#[test] +fn coefficient_conversion_preserves_existing_transport_policy() { + assert_eq!(BackendCoefficient::to_backend_number(17_i64).unwrap(), 17.0); + assert_eq!(BackendCoefficient::to_backend_number(0.5_f64).unwrap(), 0.5); + let value = MAX_EXACT_F64_INTEGER + 1; + for ilp in [ + ILP::::new(1, vec![], vec![(0, value)], ObjectiveSense::Maximize).unwrap(), + ILP::::new( + 1, + vec![LinearConstraint::le(vec![(0, value)], 1)], + vec![], + ObjectiveSense::Maximize, + ) + .unwrap(), + ILP::::new( + 1, + vec![LinearConstraint::le(vec![(0, 1)], value)], + vec![], + ObjectiveSense::Maximize, + ) + .unwrap(), + ] { + assert!(matches!( + HighsAdapter::new(None).solve(&ilp), + Err(IlpBackendError::InexactTransport(_)) + )); + } +} + +#[test] +fn invalid_time_limits_are_errors_instead_of_backend_panics() { + for time in [-1.0, f64::NAN, f64::INFINITY] { + assert!(matches!( + HighsAdapter::new(Some(time)).solve(&ILP::::empty()), + Err(IlpBackendError::BackendFailure(_)) + )); + } +} + +#[test] +fn adapter_accepts_an_ilp_domain_without_any_registry_entry() { + #[derive(Clone, Debug)] + struct UnregisteredDomain; + impl VariableDomain for UnregisteredDomain { + const NAME: &'static str = "UnregisteredDomain"; + fn default_variable() -> IntegerVariable { + ::default_variable() + } + fn validate_variables( + variables: &[IntegerVariable], + ) -> Result<(), crate::registry::ConstructionError> { + ::validate_variables(variables) + } + } + let ilp = ILP::::with_variables( + vec![IntegerVariable::new(Some(0), Some(2)).unwrap()], + vec![], + vec![(0, 1)], + ObjectiveSense::Maximize, + ) + .unwrap(); + assert_eq!(HighsAdapter::new(None).solve(&ilp).unwrap(), vec![2]); +} diff --git a/src/unit_tests/solvers/ilp/solver.rs b/src/unit_tests/solvers/ilp/solver.rs index 5c193553..52fea02e 100644 --- a/src/unit_tests/solvers/ilp/solver.rs +++ b/src/unit_tests/solvers/ilp/solver.rs @@ -1,5 +1,5 @@ use super::*; -use crate::models::algebraic::{IntegerVariable, LinearConstraint}; +use crate::models::algebraic::{IntegerVariable, LinearConstraint, ObjectiveSense, ILP}; use crate::traits::Problem; fn binary_ilp( @@ -113,26 +113,6 @@ fn test_ilp_solver_rejects_inexact_integer_transport() { )); } -#[test] -fn test_backend_errors_are_classified_without_losing_the_cause() { - assert_eq!( - classify_backend_error(ResolutionError::Infeasible, None), - ILPSolveError::Infeasible, - ); - assert_eq!( - classify_backend_error(ResolutionError::Unbounded, None), - ILPSolveError::Unbounded, - ); - assert_eq!( - classify_backend_error(ResolutionError::Other("NoSolutionFound"), Some(0.1)), - ILPSolveError::Timeout, - ); - assert!(matches!( - classify_backend_error(ResolutionError::Other("SolveError"), None), - ILPSolveError::BackendFailure(message) if message.contains("SolveError") - )); -} - #[test] fn test_ilp_rejects_solution_that_is_infeasible_after_rounding() { let ilp = binary_ilp( @@ -258,37 +238,6 @@ fn test_registered_ilp_pipeline_success() { assert!(problem.evaluate(&solution).unwrap().is_valid()); } -#[test] -fn test_ilp_solve_dyn_bool() { - let ilp = ILP::::new(1, vec![], vec![(0, 1.0)], ObjectiveSense::Maximize).unwrap(); - assert!(ILPSolver::new() - .solve_dyn(&ilp as &dyn std::any::Any) - .is_ok()); -} - -#[test] -fn test_ilp_solve_dyn_i64() { - let ilp = ILP::::with_variables( - vec![ - IntegerVariable::new(Some(0), Some(3)).unwrap(), - IntegerVariable::new(Some(0), Some(3)).unwrap(), - ], - vec![], - vec![], - ObjectiveSense::Minimize, - ) - .unwrap(); - assert!(ILPSolver::new() - .solve_dyn(&ilp as &dyn std::any::Any) - .is_ok()); -} - -#[test] -fn test_ilp_solve_dyn_unknown_type_returns_unsupported_problem_type() { - let result = ILPSolver::new().solve_dyn(&42_i64 as &dyn std::any::Any); - assert_eq!(result, Err(ILPSolveError::UnsupportedProblemType)); -} - // Test acceptance policy in source-objective units, separate from variable rounding. // This allows small absolute numerical differences near zero; it is not a // guaranteed objective-error bound derived from HiGHS feasibility tolerances. @@ -325,3 +274,26 @@ fn test_float_qubo_objective_matches_reference_within_tolerance() { )); } } + +#[test] +fn test_ilp_solver_rejects_objective_overflow_after_backend_success() { + let ilp = ILP::::with_variables( + vec![IntegerVariable::new(Some(1025), Some(1025)).unwrap()], + vec![], + vec![(0, crate::types::MAX_EXACT_F64_INTEGER)], + ObjectiveSense::Maximize, + ) + .unwrap(); + assert!(matches!( + ILPSolver::new().solve(&ilp), + Err(ILPSolveError::InvalidSolution(_)) + )); +} + +#[test] +fn test_invalid_public_time_limit_returns_existing_backend_error() { + assert!(matches!( + ILPSolver::with_time_limit(-1.0).solve(&ILP::::empty()), + Err(ILPSolveError::BackendFailure(_)) + )); +} diff --git a/src/unit_tests/solvers/registry.rs b/src/unit_tests/solvers/registry.rs index 97c55d08..3a940cfc 100644 --- a/src/unit_tests/solvers/registry.rs +++ b/src/unit_tests/solvers/registry.rs @@ -32,10 +32,6 @@ fn generic_decision_ilp_respects_maximization_bounds() { name: "ILP", variant: BOOL_VARIANT, }, - StaticProblemStep { - name: "ILP", - variant: FLOAT_BOOL_VARIANT, - }, ], }; let registry = build_registry( @@ -54,7 +50,7 @@ fn generic_decision_ilp_respects_maximization_bounds() { ); for bound in [0, 1, 2] { let decision = Decision::new(inner.clone(), bound); - let result = pipeline.solve(&decision, &crate::solvers::ILPSolver::new()); + let result = pipeline.solve(&decision, &HighsAdapter::new(None)); if bound > 1 { assert!(matches!( result, @@ -76,7 +72,7 @@ fn generic_decision_ilp_reports_unresolved_but_preserves_extraction_errors() { use crate::models::decision::Decision; use crate::models::graph::MinimumVertexCover; use crate::rules::{ExtractionError, ReductionResult}; - use crate::solvers::{ILPSolveError, ILPSolver}; + use crate::solvers::ILPSolveError; use crate::topology::SimpleGraph; use crate::traits::Problem; @@ -114,11 +110,11 @@ fn generic_decision_ilp_reports_unresolved_but_preserves_extraction_errors() { }; let inner = Inner::new(SimpleGraph::new(2, vec![(0, 1)]), vec![1i64; 2]); assert!(matches!( - pipeline.solve(&Decision::new(inner.clone(), 0), &ILPSolver::new()), + pipeline.solve(&Decision::new(inner.clone(), 0), &HighsAdapter::new(None)), Err(ILPSolveError::UnresolvedDecision(_)) )); assert!(matches!( - pipeline.solve(&Decision::new(inner, 1), &ILPSolver::new()), + pipeline.solve(&Decision::new(inner, 1), &HighsAdapter::new(None)), Err(ILPSolveError::Extraction(ExtractionError::Reduction { message, .. })) if message == "broken witness decoder" )); @@ -406,11 +402,7 @@ fn solver_capability_registry_exposes_representative_capability_classes() { assert!(direct_ilp.customized.is_none()); assert_eq!( direct_ilp.ilp.unwrap().path_labels(), - [ - "MaximumClique", - "ILP", - "ILP" - ] + ["MaximumClique", "ILP"] ); let multihop_ilp = solver_capabilities(&key( @@ -435,10 +427,7 @@ fn solver_capability_registry_exposes_representative_capability_classes() { let ilp_itself = solver_capabilities(&key("ILP", &[("variable", "bool"), ("coefficient", "i64")])).unwrap(); - assert_eq!( - ilp_itself.ilp.unwrap().path_labels(), - ["ILP", "ILP"] - ); + assert_eq!(ilp_itself.ilp.unwrap().path_labels(), ["ILP"]); } #[test] @@ -588,3 +577,34 @@ fn solver_capability_registry_ambiguous_exact_edge_is_rejected() { RegistryBuildError::InvalidEdge { matches: 2, .. } )); } + +#[test] +fn native_terminal_dispatch_rejects_non_ilp_values() { + assert_eq!( + solve_ilp_terminal(&42_i64, &HighsAdapter::new(None)), + Err(crate::solvers::ILPSolveError::UnsupportedProblemType) + ); +} + +#[test] +fn registered_pipelines_stop_at_the_first_native_ilp() { + let registry = solver_capability_registry().unwrap(); + for pipeline in registry.ilp.values() { + assert!(pipeline.path.last().unwrap().is_supported_ilp()); + assert!(pipeline.path[..pipeline.path.len() - 1] + .iter() + .all(|step| !step.is_supported_ilp())); + } + for variable in ["bool", "i64"] { + for coefficient in ["i64", "f64"] { + let key = ExactProblemKey::new( + "ILP", + BTreeMap::from([ + ("variable".into(), variable.into()), + ("coefficient".into(), coefficient.into()), + ]), + ); + assert_eq!(registry.lookup(&key).ilp.unwrap().path(), &[key]); + } + } +} diff --git a/src/unit_tests/solvers/resolver.rs b/src/unit_tests/solvers/resolver.rs index 8cc05f90..ed9c120a 100644 --- a/src/unit_tests/solvers/resolver.rs +++ b/src/unit_tests/solvers/resolver.rs @@ -365,7 +365,7 @@ fn deterministic_solver_dispatch_customized_infeasibility_does_not_fall_back() { } #[test] -fn deterministic_solver_dispatch_integer_ilp_uses_registered_cast_pipeline() { +fn deterministic_solver_dispatch_integer_ilp_uses_native_terminal() { let problem = ILP::::new(0, vec![], vec![], ObjectiveSense::Minimize).unwrap(); let loaded = load_dyn( ILP::::NAME, @@ -381,7 +381,7 @@ fn deterministic_solver_dispatch_integer_ilp_uses_registered_cast_pipeline() { assert_eq!( result.solver, SolverExecution::Ilp { - reduction_path: vec!["ILP".to_string(), "ILP".to_string()] + reduction_path: vec!["ILP".to_string()] } ); assert!(matches!( @@ -498,7 +498,6 @@ fn deterministic_solver_dispatch_fixed_multihop_pipeline_is_repeatable() { "MaximumIndependentSet", "MaximumSetPacking", "ILP", - "ILP", ] ); } diff --git a/tests/suites/integration.rs b/tests/suites/integration.rs index db976e33..dd086604 100644 --- a/tests/suites/integration.rs +++ b/tests/suites/integration.rs @@ -532,3 +532,51 @@ mod weighted_problems { assert!(satisfying.is_empty()); } } + +/// Exercise the solver as a downstream crate: struct construction, generic +/// bounds, solution type, and exhaustive error matching must keep compiling. +#[test] +fn ilp_public_api_remains_source_compatible() { + use problemreductions::solvers::{ILPSolveError, ILPSolver}; + fn solve_as_before

(problem: &P) -> std::result::Result + where + P: Problem + 'static, + P::Solution: 'static, + { + ILPSolver::new().solve(problem) + } + fn classify_as_before(error: ILPSolveError) -> &'static str { + match error { + ILPSolveError::Infeasible => "infeasible", + ILPSolveError::UnresolvedDecision(_) => "unresolved decision", + ILPSolveError::Unbounded => "unbounded", + ILPSolveError::Timeout => "timeout", + ILPSolveError::BackendFailure(_) => "backend", + ILPSolveError::UnsupportedProblemType => "unsupported", + ILPSolveError::MissingPipeline(_) => "missing pipeline", + ILPSolveError::InvalidRegistry(_) => "registry", + ILPSolveError::PipelineTypeMismatch(_) => "type mismatch", + ILPSolveError::InvalidSolution(_) => "invalid solution", + ILPSolveError::InexactTransport(_) => "transport", + ILPSolveError::Extraction(_) => "extraction", + ILPSolveError::Reduction(_) => "reduction", + } + } + let solver = ILPSolver { time_limit: None }; + let ILPSolver { time_limit } = solver.clone(); + assert_eq!(time_limit, None); + let ilp = ILP::::new(1, vec![], vec![(0, 1)], ObjectiveSense::Maximize).unwrap(); + let solution: Vec = solve_as_before(&ilp).unwrap(); + assert_eq!(solution, vec![1]); + let infeasible = ILP::::new( + 0, + vec![LinearConstraint::ge(vec![], 1)], + vec![], + ObjectiveSense::Minimize, + ) + .unwrap(); + assert_eq!( + classify_as_before(solver.solve(&infeasible).unwrap_err()), + "infeasible" + ); +}