Do not discard a MIP solution when its repair LP fails to solve (#3180) - #3181
Conversation
jajhall
left a comment
There was a problem hiding this comment.
Fair enough, this does no harm
Codecov Report❌ Patch coverage is
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. 🚀 New features to boost your workflow:
|
| // 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) { |
There was a problem hiding this comment.
Presolve may have already been disabled in the lines directly above. In this case there's no need to re-run the LP.
There was a problem hiding this comment.
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.
|
@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. |
|
@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>
|
Good catch, thanks — applied.
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 I have not added the further fallbacks you both suggested — retrying with presolve on when it was |
a88e154 to
2b3c167
Compare
Opt-Mucca
left a comment
There was a problem hiding this comment.
Happy with the changes! I'd consider removing all comments though as they're pretty verbose and it's always clear what's happening.
|
@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. |
|
Closes #3180 |
2b3c167 to
7a65646
Compare
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
7a65646 to
187c624
Compare
|
@Opt-Mucca how would you like me to get the mps files to you? I would rather not make them public at this stage. |
|
@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) |
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,HighsMipSolverDatarepairs it by fixing the integer columns andre-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
kHighsInfis 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 asOptimalat a zero gap. The repair LP runs withoutput_flag = false, so the error it raised isnever 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_valueand, when a row-value vector is supplied, trusts it instead of recomputing: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-7and then fail that checkat
1.5e-7, turning a reportedOptimalintoSolve error. With the retry alone I reproducedexactly that on a generated model; with both parts the same model correctly rejects the marginal
point and keeps a genuinely feasible one.
Effect
-3755449.094926->-3755469.767048(correct); 17 previously discarded solutions recovered-19105203.4816-7.60954221608e+14->-7.60954325469e+14(correct)Optimalwith no error, where the retry alone gave a false optimumcheck/instanceslatestMeasured on this branch alone —
latestplus this commit, with no other changes applied — so theresult 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 repaircomes from — it scales rows down, and the feasibility tolerance is not scale invariant, so a row
scaled by
sand satisfied tofeastolin the presolved space is satisfied only tofeastol / |s|in the original one.
Suppressing the down-scaling does fix the first model and leaves
check/instancesunchanged inobjective, node count and LP iteration count. But on the second production model above it causes a
wrong answer —
-19104998.2316against a correct-19105203.4816— even though that model neverreaches the untransformed-violation path. Any change to
scaleMIPperturbs models that have nodefect. 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-6to1e12, which is the kindof 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
presolveon and off;🤖 Generated with Claude Code