Fix objective offset dropped when writing the transformed problem (MPS/LP) - #213
Open
MathieuDutSik wants to merge 2 commits into
Open
Fix objective offset dropped when writing the transformed problem (MPS/LP)#213MathieuDutSik wants to merge 2 commits into
MathieuDutSik wants to merge 2 commits into
Conversation
… problems When writing the transformed problem, the MPS and LP writers used only SCIPprobGetObjoffset(transprob), which excludes the objective constant of the original problem. SCIP stores that constant separately in origprob->objoffset and adds it on top when converting transformed <-> original objective values (prob.c: extobj = objsense * objscale * (intobj + transoffset) + origoffset). As a result, `read <model with objective constant> ... write transproblem` produced a file whose objective constant was missing, so re-reading/solving it yielded an optimum off by exactly the dropped constant. The in-memory solve was always correct (presolve keeps the constant); only the serialization dropped it, so it went unnoticed for fresh models (constant 0) but corrupts any workflow that round-trips presolved problems. Fix: add SCIPgetOrigObjoffset(scip) to the written objective constant when `transformed` is TRUE (added outside objscale, matching prob.c). The LP writer's guard is widened so a nonzero original constant is still emitted when the transformed offset is zero. Original-problem writes are unchanged (the offset is already included and the term is not added). Found via the harness_scip_highs presolve-chaining experiment; e.g. HiGHS-presolved egout (objective constant -327.83418) round-tripped through SCIP presolve gave 240.2665 instead of 568.1007 = 240.2665 + 327.83418. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…m writer Builds "min x s.t. x >= 3" with an original objective constant of 100 (optimum 103), transforms it, writes the transformed problem, reads it back into a fresh SCIP and solves. Asserts the recovered optimum is 103. On the buggy writers the objective constant is dropped and the optimum comes back as 3, so the test fails; it passes once reader_mps.c/reader_lp.c add SCIPgetOrigObjoffset. Both the MPS and LP writer paths are covered. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
DominikKamp
self-requested a review
August 5, 2026 10:59
Contributor
|
Even if it is no longer strictly the transformed problem being written, it is still lost information if the original offset is not included and therefore unexpected in the presence of a transformed offset. The unit test is not necessary since the available reader tests rely on this as well, so adding original offsets to check instances should be enough. But this should apply to all supported formats which represent an objective offset, especially CIP format. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Description
Summary
The MPS and LP writers omit the original problem's objective constant when writing the transformed problem. As a result, read → presolve → write transproblem produces a file whose objective constant is missing, so re-reading and solving it returns an optimum off by exactly that constant.
Root cause
SCIP stores the objective constant of the original problem in origprob->objoffset, separately from the transformed problem's offset (transprob->objoffset, which only accumulates presolve fixings). The external objective is combined as
extobj = objsense * objscale * (intobj + transprob->objoffset) + origprob->objoffset
(prob.c, SCIPgetTransObjoffset/conversion). Both writers, however, used only SCIPprobGetObjoffset(transprob) for the written objective constant, dropping the + origprob->objoffset term.
Because a freshly read MPS/LP normally has objective constant 0, the in-memory solve is always correct and the bug is invisible in ordinary use. It only surfaces when the input already carries a nonzero objective constant — e.g. when chaining/round-tripping presolved models — which is how it was found.
Fix
src/scip/reader_mps.c and src/scip/reader_lp.c: when writing the transformed problem (transformed == TRUE), add SCIPgetOrigObjoffset(scip) to the written objective constant, outside the objscale factor to match prob.c. The LP writer's emit guard is widened so a nonzero original constant is still written when the transformed offset is zero. Original-problem writes are unchanged (the offset is already included there, and the extra term is not added). Exact solving mode is unaffected (the MPS writer already rejects it).
Testing
A test that fails prior to the fix and passes on the fix is introduced.