Skip to content

Fix objective offset dropped when writing the transformed problem (MPS/LP) - #213

Open
MathieuDutSik wants to merge 2 commits into
scipopt:masterfrom
MathieuDutSik:fix-write-transproblem-objoffset
Open

Fix objective offset dropped when writing the transformed problem (MPS/LP)#213
MathieuDutSik wants to merge 2 commits into
scipopt:masterfrom
MathieuDutSik:fix-write-transproblem-objoffset

Conversation

@MathieuDutSik

Copy link
Copy Markdown

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.

MathieuDutSik and others added 2 commits July 23, 2026 12:43
… 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
DominikKamp self-requested a review August 5, 2026 10:59
@DominikKamp

Copy link
Copy Markdown
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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants