Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 13 additions & 10 deletions .claude/CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -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/<source>_<target>.rs` (e.g., `maximumindependentset_qubo.rs`)
- Model files: `src/models/<category>/<name>.rs` — category is by input structure: `graph/` (graph input), `formula/` (boolean formula/circuit), `set/` (universe + subsets), `algebraic/` (matrix/linear system/lattice), `misc/` (other)
Expand Down
6 changes: 3 additions & 3 deletions docs/paper/reductions.typ
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down Expand Up @@ -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")
Expand Down
59 changes: 47 additions & 12 deletions docs/src/design.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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<bool, f64>` or `ILP<i64, f64>`, which is solved by HiGHS via `good_lp`. |
| **ILPSolver** | Executes a problem's registered ILP pipeline. Each pipeline terminates at a native `ILP<V, C>` 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
Expand All @@ -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<P>() -> Result<P::Solution, ILPSolveError>` 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<V, C>` and returns its existing
`Vec<i64>` 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:
Expand Down
7 changes: 2 additions & 5 deletions src/rules/closestsubstring_ilp.rs
Original file line number Diff line number Diff line change
Expand Up @@ -166,11 +166,8 @@ impl ReduceTo<ILP<i64>> 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.
Expand Down
110 changes: 0 additions & 110 deletions src/rules/ilp_casts.rs

This file was deleted.

86 changes: 86 additions & 0 deletions src/rules/ilp_i64_ilp_f64.rs
Original file line number Diff line number Diff line change
@@ -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<V> = VariantReductionResult<ILP<V>, ILP<V, f64>>;

fn reduce_coefficients<V: VariableDomain>(
source: &ILP<V>,
) -> Result<ReductionILPToFloat<V>, ReductionError> {
let convert = |coefficient: i64| {
i64_to_exact_f64(coefficient)
.map_err(ReductionError::inexact_float_conversion::<ILP<V>, ILP<V, f64>>)
};
let constraints = source
.constraints()
.iter()
.map(|constraint| {
let terms = constraint
.terms()
.iter()
.map(|&(variable, coefficient)| Ok((variable, convert(coefficient)?)))
.collect::<Result<Vec<_>, 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::<Result<Vec<_>, ReductionError>>()?;
let objective = source
.objective()
.iter()
.map(|&(variable, coefficient)| Ok((variable, convert(coefficient)?)))
.collect::<Result<Vec<_>, ReductionError>>()?;
let target = ILP::with_variables(
source.variables().to_vec(),
constraints,
objective,
source.sense(),
)
.map_err(ReductionError::construction::<ILP<V>, ILP<V, f64>>)?;
Ok(VariantReductionResult::new(target))
}

#[reduction(
transform = exact {
num_vars = "num_vars",
num_constraints = "num_constraints",
num_nonzeros = "num_nonzeros",
},
)]
impl ReduceTo<ILP<bool, f64>> for ILP<bool> {
type Result = ReductionILPToFloat<bool>;

fn reduce_to(&self) -> Result<Self::Result, ReductionError> {
reduce_coefficients(self)
}
}

#[reduction(
transform = exact {
num_vars = "num_vars",
num_constraints = "num_constraints",
num_nonzeros = "num_nonzeros",
},
)]
impl ReduceTo<ILP<i64, f64>> for ILP<i64> {
type Result = ReductionILPToFloat<i64>;

fn reduce_to(&self) -> Result<Self::Result, ReductionError> {
reduce_coefficients(self)
}
}

#[cfg(test)]
#[path = "../unit_tests/rules/ilp_i64_ilp_f64.rs"]
mod tests;
Loading
Loading