Skip to content

Do not discard a MIP solution when its repair LP fails to solve (#3180) - #3181

Merged
jajhall merged 2 commits into
ERGO-Code:latestfrom
EamonHetherton:issue-repair-lp
Aug 1, 2026
Merged

Do not discard a MIP solution when its repair LP fails to solve (#3180)#3181
jajhall merged 2 commits into
ERGO-Code:latestfrom
EamonHetherton:issue-repair-lp

Conversation

@EamonHetherton

Copy link
Copy Markdown

Fixes #3180.

The defect

When a MIP solution is feasible for the presolved model but violates the original model by more
than mip_feasibility_tolerance, HighsMipSolverData repairs it by fixing the integer columns and
re-solving the LP in the original space. The repaired solution is accepted only if that LP returns a
feasible primal solution, so a failure of the solver is treated exactly like a solution that is
genuinely bad: it is discarded, and kHighsInf is returned so it is not used for bounding.

The repair LP is solved in the original space with the dual simplex, which on a badly scaled model
can fail the ratio test — "excessive dual values", or its primal counterpart — and return
kSolveError. When the discarded solution is the optimum, the search reports a worse incumbent as
Optimal at a zero gap. The repair LP runs with output_flag = false, so the error it raised is
never shown either.

The change

Two parts, both in HighsMipSolverData::transformNewIntegerFeasibleSolution.

Retry a failed repair LP without presolve.

    tmpSolver.optimizeLp();
    if (tmpSolver.getModelStatus() == HighsModelStatus::kSolveError) {
      tmpSolver.setOptionValue("presolve", kHighsOffString);
      tmpSolver.optimizeLp();
    }

The failing LPs solve to optimality that way; primal simplex and IPM also solve them, so if you
would rather fall back differently, any of the three recovers the solution.

Recompute the row activities from the repaired column values.

      solution = tmpSolver.getSolution();
      calculateRowValuesQuad(*mipsolver.orig_model_, solution.col_value,
                             solution.row_value);

This part is not optional, and it is the subtler of the two. solutionFeasible() is passed
&solution.row_value and, when a row-value vector is supplied, trusts it instead of recomputing:

if (pass_row_value) {
  ...                                    // used as given
} else {
  calculateRowValuesQuad(*lp, col_value, row_value);
}

After a repair those activities come from the repair LP and need only satisfy that LP to its own
tolerance, whereas the check later applied to the returned solution recomputes them in quad
precision. Without recomputing, a repaired solution can pass here at 1e-7 and then fail that check
at 1.5e-7, turning a reported Optimal into Solve error. With the retry alone I reproduced
exactly that on a generated model; with both parts the same model correctly rejects the marginal
point and keeps a genuinely feasible one.

Effect

result
12710 x 23091 production MIP -3755449.094926 -> -3755469.767048 (correct); 17 previously discarded solutions recovered
14412 x 26520 production MIP unchanged and correct at -19105203.4816
generated MIP, repair LP fails -7.60954221608e+14 -> -7.60954325469e+14 (correct)
generated MIP, marginal repair now Optimal with no error, where the retry alone gave a false optimum
the 26 MIP models in check/instances identical objectives to stock latest
full unit test suite 1259300 assertions in 334 test cases, pass

Measured on this branch alone — latest plus this commit, with no other changes applied — so the
result does not depend on anything else I have reported.

The retry is reached only when a repair LP has already failed, and the recomputation only when a
repair has already succeeded, so neither can perturb a model where no solution is ever repaired.

What I would advise against

Addressing this in HPresolve::scaleMIP, which is where the violation that triggers the repair
comes from — it scales rows down, and the feasibility tolerance is not scale invariant, so a row
scaled by s and satisfied to feastol in the presolved space is satisfied only to feastol / |s|
in the original one.

Suppressing the down-scaling does fix the first model and leaves check/instances unchanged in
objective, node count and LP iteration count. But on the second production model above it causes a
wrong answer — -19104998.2316 against a correct -19105203.4816 — even though that model never
reaches the untransformed-violation path. Any change to scaleMIP perturbs models that have no
defect. The repair path is the right place: it already exists, it already recovers 17 of the 34
repairs on the first model, and it is inert otherwise.

On a regression test

I have a generated MIP that reproduces the discard — 134 rows x 356 columns, 72 integers — but it
needs the pinned option set and its objective coefficients span 1e-6 to 1e12, which is the kind
of instance that was objected to on #3172. I have not added it as a test for that reason, and am
happy to supply it if you would like it, or to look for a numerically tamer one.

Testing

🤖 Generated with Claude Code

@jajhall jajhall left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fair enough, this does no harm

@codecov

codecov Bot commented Jul 28, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 0% with 5 lines in your changes missing coverage. Please review.
✅ Project coverage is 72.92%. Comparing base (4e6489d) to head (187c624).
⚠️ Report is 135 commits behind head on latest.

Files with missing lines Patch % Lines
highs/mip/HighsMipSolverData.cpp 0.00% 5 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##           latest    #3181      +/-   ##
==========================================
- Coverage   73.18%   72.92%   -0.27%     
==========================================
  Files         432      436       +4     
  Lines      105460   106038     +578     
  Branches    16982    17071      +89     
==========================================
+ Hits        77182    77329     +147     
- Misses      28002    28433     +431     
  Partials      276      276              

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

Comment thread highs/mip/HighsMipSolverData.cpp Outdated
// or its primal counterpart. That is a failure of the solver, not a
// statement about the solution, so retry once without presolve rather than
// discarding a solution that may be the incumbent - or the optimum.
if (tmpSolver.getModelStatus() == HighsModelStatus::kSolveError) {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Presolve may have already been disabled in the lines directly above. In this case there's no need to re-run the LP.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

True, in which case the LP could be solved with presolve "on" as another attempt to get an optimal solution.

If it matters enough to a user, IPM could also be tried.

@Opt-Mucca

Copy link
Copy Markdown
Collaborator

@EamonHetherton you keep mentioning "production models" and "available upon request". Any chance you're willing to share them? If so we'd store them, use them for testing, and make them public with appropriate credit in a couple of years.

Comment thread highs/mip/HighsMipSolverData.cpp

@jajhall jajhall left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

See comment

@EamonHetherton

Copy link
Copy Markdown
Author

@Opt-Mucca yes, I can share more, see #3180 (comment) for some detail. Would you like an MPS file where I find a bug? they are usually of the order of 2-3MB.

When a solution is feasible for the presolved model but violates the original
model by more than mip_feasibility_tolerance, HighsMipSolverData repairs it by
fixing the integer columns and re-solving the LP in the original space. The
repaired solution is accepted only if that LP returns a feasible primal
solution, so a failure of the solver is treated exactly like a solution that is
genuinely bad: the solution is discarded and kHighsInf is returned, so it is not
used for bounding. When the discarded solution is the optimum, the search
reports a worse incumbent as optimal at a zero gap.

The repair LP is solved in the original space with the dual simplex, which on a
badly scaled model can fail the ratio test with "excessive dual values" (or its
primal counterpart) and return kSolveError. On a 12710 x 23091 MIP with an
objective coefficient range of 2.4e13, 17 of 34 repair LPs failed this way and
the optimum was among the solutions discarded, leaving an objective 20.67 worse
reported as optimal.

Retry such a solve once without presolve; the same LP then solves to optimality,
as it also does with the primal simplex or with IPM.

Also recompute the row activities from the repaired column values. The
feasibility test is given solution.row_value and trusts it, but the values
returned by the LP solver need only satisfy the LP to its own tolerance, so they
can differ from the quad-precision recomputation used by the check that is later
applied to the returned solution. Without this a repaired solution can pass here
and then fail that check, turning a reported optimum into a solve error.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@EamonHetherton

Copy link
Copy Markdown
Author

Good catch, thanks — applied.

use_presolve is !mip_root_presolve_only, so with that option set presolve is already off for
the first attempt and the retry would solve an identical LP and fail in the same way. The retry is
now guarded:

    if (use_presolve &&
        tmpSolver.getModelStatus() == HighsModelStatus::kSolveError) {
      tmpSolver.setOptionValue("presolve", kHighsOffString);
      tmpSolver.optimizeLp();
    }

Re-verified after the change: the two production models and the generated reproducer still return
the correct optima, including with mip_root_presolve_only = true, and the full unit test suite
passes (1259300 assertions in 334 test cases).

I have not added the further fallbacks you both suggested — retrying with presolve on when it was
off first, or trying IPM — since that widens the change beyond what the reported defect needs. Both
would fit in the same place if you want them; on the LPs I have, presolve off, primal simplex and
IPM all solve what the dual simplex fails on, so any of the three works as a fallback.

@Opt-Mucca Opt-Mucca left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Happy with the changes! I'd consider removing all comments though as they're pretty verbose and it's always clear what's happening.

@Opt-Mucca

Copy link
Copy Markdown
Collaborator

@EamonHetherton I'd like one or two of the instances that you think are representative of the application. It doesn't have to be the ones that exposed some bugs.

@jajhall

jajhall commented Jul 29, 2026

Copy link
Copy Markdown
Member

Closes #3180

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@EamonHetherton

Copy link
Copy Markdown
Author

@Opt-Mucca how would you like me to get the mps files to you? I would rather not make them public at this stage.

@Opt-Mucca

Copy link
Copy Markdown
Collaborator

@EamonHetherton please email me using the information here https://www.zib.de/members/turner (if the files are more than a couple of megabytes compressed then please send a good drive link or something similar)

@jajhall
jajhall merged commit ae53450 into ERGO-Code:latest Aug 1, 2026
358 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants