You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
Suboptimal solution reported as optimal on a small MIP with presolve enabled #3561
What version of HiGHS and what language are you using?
Branch latest (17a99d4). Release 1.15.1 (04024d7) is not affected.
Using the standalone C++ command-line executable.
Which solver are you using (e.g. LP, MIP, QP)
MIP
What operating system (Linux, Windows, ...) and version?
Debian GNU/Linux 12, x86-64
What did you do?
Ran the attached model rand_numer_006_mr8.mps.txt (17 rows, 91 columns, 7 binary)
What did you expect to see
Optimal, objective ≈ 144.68
What did you see instead?
Optimal, objective 78863.54 (primal and dual bound both 78863.5419419, gap 0%)
Disabling presolve returns an optimal solution with objective 144.680849, and release 1.15.1 returns 144.680846 with default settings:
highs rand_numer_006_mr8.mps --threads 1 --time_limit 120 --presolve off
The attached witness.txt lists a verified feasible solution with objective 144.68085, disproving the reported optimality claim. Variables not listed are zero.
Anything else we should know about your project / environment
The model is a random MIP with every finite row side relaxed by 1e-6 * max(1, abs(side)). The unrelaxed model solves to 144.68 on the same build. HiGHS warns "Problem has some excessively small bounds on constraints" because zero row sides become ±1e-6. The pattern is similar to #3487, which is fixed on this build.
A second model, rand_numer_060_mr8.mps.txt (7 rows, 18 columns), shows the same behaviour: Optimal 80652.21 by default and 171.336426 with presolve off; witness_060.txt lists a verified feasible solution with objective 171.33643.
I have been looking at this presolve code, and I found what causes this. Here is a proposed fix.
The wrong fixing comes from dual-fixing probing (rule 20; presolve_rule_off = 1048576 avoids it). updateLhsRedundant and updateRhsRedundant in HighsDomain.cpp count a row side as redundant when its activity bound is within feastol of the side. On rand_numer_060_mr8, probing x1 = 0 leaves r0 1e-6 short of redundant, but it is marked redundant anyway, so x3 and x8 are dual-fixed down. That violates r0, the probe is declared infeasible, and x1 is fixed to 1.
Without the tolerance, both models solve correctly (144.680846 and 171.336411):
--- a/highs/mip/HighsDomain.cpp+++ b/highs/mip/HighsDomain.cpp@@ -733,8 +733,7 @@
mipsolver->model_->row_upper_[row] == kHighsInf)
return;
- if (domain->getMaxActivity(row) <=- mipsolver->model_->row_upper_[row] + mipsolver->mipdata_->feastol) {+ if (domain->getMaxActivity(row) <= mipsolver->model_->row_upper_[row]) {
redundantRowInds_.push_back(idx);
redundantRowFlags_[idx] = 1;
if (!isEnabled()) ++numGlobalRedundantRows_;
@@ -747,8 +746,7 @@
mipsolver->model_->row_lower_[row] == -kHighsInf)
return;
- if (domain->getMinActivity(row) >=- mipsolver->model_->row_lower_[row] - mipsolver->mipdata_->feastol) {+ if (domain->getMinActivity(row) >= mipsolver->model_->row_lower_[row]) {
redundantRowInds_.push_back(idx);
redundantRowFlags_[idx] = 1;
if (!isEnabled()) ++numGlobalRedundantRows_;
@DoumarCharles thanks for tracking down the issue! I've proposed a fix in #3575 that works for the problematic instance. In general I'd prefer to keep tolerances here as they're used constantly in this context throughout the code. It was some avoidable unlucky floating precision stuff that was causing this instance to produce incorrect results.
Activity
jajhall commentedon Nov 6, 2026
Reproduced with latest (9ba15c4)
DoumarCharles commentedon Nov 6, 2026
I have been looking at this presolve code, and I found what causes this. Here is a proposed fix.
The wrong fixing comes from dual-fixing probing (rule 20;
presolve_rule_off = 1048576avoids it).updateLhsRedundantandupdateRhsRedundantinHighsDomain.cppcount a row side as redundant when its activity bound is withinfeastolof the side. Onrand_numer_060_mr8, probingx1 = 0leavesr01e-6 short of redundant, but it is marked redundant anyway, sox3andx8are dual-fixed down. That violatesr0, the probe is declared infeasible, andx1is fixed to 1.Without the tolerance, both models solve correctly (144.680846 and 171.336411):
I used an AI assistant to help trace this.
Opt-Mucca commentedon Nov 6, 2026
@DoumarCharles thanks for tracking down the issue! I've proposed a fix in #3575 that works for the problematic instance. In general I'd prefer to keep tolerances here as they're used constantly in this context throughout the code. It was some avoidable unlucky floating precision stuff that was causing this instance to produce incorrect results.
@hodangkhoinguyen thanks for raising the issue!