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
This report was generated by an AI agent (supervised by a human). It is a
self-documenting bug report: the repro, output and code references below were
produced and verified by the agent against master at 09c34dd.
Version Checks
Bug exists on the latest release (v0.9.1 — the offending line is identical there)
Bug exists on the current master branch (09c34dd)
Issue Description
Model.copy() rebuilds the objective as a LinearExpression unconditionally, so a QuadraticExpression objective is silently downgraded to a linear one. The copy's Model.type flips from QP to LP, objective.is_quadratic becomes False, and the
quadratic term's second variable column is simply reinterpreted as if the row were linear.
The copy then solves happily — as a different model — with no warning or error. The copy.copy / copy.deepcopy protocols route through the same function and are equally
affected.
Severity: silently wrong results. Nothing raises. Any workflow that copies a model
before solving (scenario loops, sensitivity sweeps, or building a reference/oracle model)
gets an answer to a problem it did not pose. In the repro below the copy returns -14.0 where the true optimum is 0.75. We first hit this in a differential test of
quadratic-objective support, where the "expected" value came from a copied model and was
off by more than 10x — indistinguishable, from inside the test, from a solver bug.
A related secondary symptom: on the copy, model.matrices.Q is None and model.matrices.c raises IndexError: boolean index did not match indexed array along axis 0 — the quadratic term's two-variable rows are still present in the flattened
coefficient array, but the accessor now takes the linear path.
The analytic optimum of the QP is x = -0.5, y = 1.5, objective 0.75, which the
original model reproduces; the copy reports x = -5.0, y = 6.0, objective -14.0.
Full script output (including matrices accessor symptom and version info)
linopy.__file__ : .../linopy-master/linopy/__init__.py
linopy.__version__ : 0.9.1.post1.dev26 (worktree of master @ 09c34dd)
python : 3.11.15
platform : Linux-6.8.0-1044-azure-x86_64-with-glibc2.35
=== original ===
type : QP
is_quadratic : True
nterm : 3
Objective:
----------
QuadraticExpression: +1 x x + 2 x + 1 y
Sense: min
Value: None
=== m.copy() ===
type : LP
is_quadratic : False
nterm : 3
Objective:
----------
LinearExpression: +1 x x + 2 x + 1 y
Sense: min
Value: None
expression class original : QuadraticExpression
expression class copy : LinearExpression
=== copy.copy / copy.deepcopy protocols ===
copy.copy type=LP is_quadratic=False
copy.deepcopy type=LP is_quadratic=False
=== matrices accessor on the copy ===
original matrices.Q is None : False
copy matrices.Q is None : True
copy matrices.c raised : IndexError boolean index did not match indexed array along axis 0; size of axis is 3 but size of corresponding boolean axis is 6
=== solve both with HiGHS ===
original : ('ok', 'optimal') objective = 0.75000000000001 x = -0.49999990000000993 y = 1.49999990000001
copy : ('ok', 'optimal') objective = -14.0 x = -5.0 y = 6.0
Analytic QP optimum: x = -0.5, y = 1.5, objective = 0.75
MISMATCH
Expected Behavior
m.copy() should preserve the objective's expression type, so that for a QP model:
The Dataset is copied faithfully, but wrapping it in LinearExpression discards the QuadraticExpression type, and Model.type / Objective.is_quadratic are derived from
that type rather than from the data — hence the silent downgrade.
Notably the same function already does the right thing a few lines above for named
expressions, in the _copy_expr helper:
So the fix looks like applying the same type(m.objective.expression)(...) dispatch at
line 1270. (Stated as the code supports it — we have not checked whether QuadraticExpression.__init__ needs anything beyond the same (data, model) signature,
though _copy_expr suggests it does not.) A regression test in test/test_io.py
asserting m.copy().type == "QP" and equal objective values after solving would cover it.
The line was introduced with Model.copy in #623 and has not been touched since, so this
has been present for every release that has Model.copy().
During differential testing of quadratic-objective support, where a reference model built
via model.copy() produced an expectation that disagreed with the solver by ~12x. The
workaround is to construct the second model from scratch rather than copying it.
Note
This report was generated by an AI agent (supervised by a human). It is a
self-documenting bug report: the repro, output and code references below were
produced and verified by the agent against
masterat09c34dd.Version Checks
v0.9.1— the offending line is identical there)masterbranch (09c34dd)Issue Description
Model.copy()rebuilds the objective as aLinearExpressionunconditionally, so aQuadraticExpressionobjective is silently downgraded to a linear one. The copy'sModel.typeflips fromQPtoLP,objective.is_quadraticbecomesFalse, and thequadratic term's second variable column is simply reinterpreted as if the row were linear.
The copy then solves happily — as a different model — with no warning or error. The
copy.copy/copy.deepcopyprotocols route through the same function and are equallyaffected.
Severity: silently wrong results. Nothing raises. Any workflow that copies a model
before solving (scenario loops, sensitivity sweeps, or building a reference/oracle model)
gets an answer to a problem it did not pose. In the repro below the copy returns
-14.0where the true optimum is0.75. We first hit this in a differential test ofquadratic-objective support, where the "expected" value came from a copied model and was
off by more than 10x — indistinguishable, from inside the test, from a solver bug.
A related secondary symptom: on the copy,
model.matrices.QisNoneandmodel.matrices.craisesIndexError: boolean index did not match indexed array along axis 0— the quadratic term's two-variable rows are still present in the flattenedcoefficient array, but the accessor now takes the linear path.
Reproducible Example
Key output:
The analytic optimum of the QP is
x = -0.5,y = 1.5, objective0.75, which theoriginal model reproduces; the copy reports
x = -5.0,y = 6.0, objective-14.0.Full script output (including matrices accessor symptom and version info)
Expected Behavior
m.copy()should preserve the objective's expression type, so that for a QP model:Root Cause
In
linopy/io.py,copy()hard-codes the objective's expression class:linopy/linopy/io.py
Line 1270 in 09c34dd
The
Datasetis copied faithfully, but wrapping it inLinearExpressiondiscards theQuadraticExpressiontype, andModel.type/Objective.is_quadraticare derived fromthat type rather than from the data — hence the silent downgrade.
Notably the same function already does the right thing a few lines above for named
expressions, in the
_copy_exprhelper:linopy/linopy/io.py
Lines 1243 to 1249 in 09c34dd
So the fix looks like applying the same
type(m.objective.expression)(...)dispatch atline 1270. (Stated as the code supports it — we have not checked whether
QuadraticExpression.__init__needs anything beyond the same(data, model)signature,though
_copy_exprsuggests it does not.) A regression test intest/test_io.pyasserting
m.copy().type == "QP"and equal objective values after solving would cover it.The line was introduced with
Model.copyin #623 and has not been touched since, so thishas been present for every release that has
Model.copy().Installed Versions
Details
How this was found
During differential testing of quadratic-objective support, where a reference model built
via
model.copy()produced an expectation that disagreed with the solver by ~12x. Theworkaround is to construct the second model from scratch rather than copying it.