fix: Model.copy() silently drops the quadratic part of the objective - #905
fix: Model.copy() silently drops the quadratic part of the objective#905FBumann wants to merge 4 commits into
Conversation
Strict-xfail regression test for #903: Model.copy() and the copy.copy / copy.deepcopy protocols rebuild the objective as a LinearExpression, silently turning a QP into a different LP. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Model.copy() wrapped the objective data in LinearExpression regardless of its actual type, so a QuadraticExpression objective was silently downgraded: Model.type flipped from QP to LP, the quadratic term's second variable column was reinterpreted as a linear row, and the copy solved a different problem without warning. Dispatch on the source expression's type, as _copy_expr already does for named expressions. assert_model_equal compared objectives with assert_linequal, which rejects a QuadraticExpression outright, so it could not be used on a QP model at all. It now dispatches via assert_exprequal, which also makes a difference in expression type fail the comparison. Closes #903 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
to_netcdf/read_netcdf rebuilt the objective as a LinearExpression, so a saved QP was read back as a different LP — the same silent downgrade as Model.copy in the previous commit. Store the expression type next to the objective and restore it on read, falling back to the presence of the factor dimension for files written by earlier versions. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Build cost — v1 vs legacyv1 build peak & time relative to legacy, on this commit — not a comparison against master (that is CodSpeed).
Full table (time + peak, mean)📊 Interactive plots + CSV: download the semantics-report-v1-vs-legacy artifact from this run. Report-only · not a gate · refreshed on every push · obsolete once legacy is dropped. |
Merging this PR will degrade performance by 3.61%
|
| Mode | Benchmark | BASE |
HEAD |
Efficiency | |
|---|---|---|---|---|---|
| ❌ | Memory | test_to_lp[milp-n=50] |
2 MB | 2.6 MB | -23.25% |
| ❌ | Memory | test_to_lp[merge_balance-severity=0] |
2.6 MB | 3.2 MB | -17.9% |
| ❌ | Memory | test_to_lp[nodal_balance-severity=50] |
2.8 MB | 3.3 MB | -15.02% |
| ❌ | Memory | test_to_lp[rolling-severity=0] |
2.7 MB | 3.1 MB | -13.86% |
| ⚡ | Memory | test_to_lp[masked-n=100] |
2.7 MB | 2.1 MB | +29.23% |
| ⚡ | Memory | test_to_lp[cumsum-severity=100] |
208.7 MB | 177.5 MB | +17.59% |
| ⚡ | Memory | test_to_lp[storage-n=10] |
2.9 MB | 2.6 MB | +10.31% |
Tip
Investigate this regression by commenting @codspeedbot fix this regression on this PR, or directly use the CodSpeed MCP with your agent.
Comparing fix/copy-quadratic-objective (586dd5b) with master (09c34dd)
Footnotes
-
175 benchmarks were skipped, so the baseline results were used instead. If they were deleted from the codebase, click here and archive them to remove them from the performance reports. ↩
The attribute carrying an expression's class through to_netcdf has been spelled out at each of its four use sites since it was introduced for named expressions. Give it a constant, next to NETCDF_VERSION_ATTR. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Closes #903.
Note
The following content was generated by AI.
Model.copy()wrapped the objective'sDatasetinLinearExpressionunconditionally, soa
QuadraticExpressionobjective was silently downgraded —Model.typeflipped fromQPto
LP, the quadratic term's second variable column was reinterpreted as a linear row, andthe copy solved a different problem without any warning (
-14.0instead of0.75in theissue's repro).
copy.copy/copy.deepcopyroute through the same function.Three commits, the last one droppable on its own:
Model.copy/copy.copy/copy.deepcopy.type(m.objective.expression), as_copy_expralready doesfor named expressions. Also switches
assert_model_equalfromassert_linequaltoassert_exprequalfor the objective:assert_linequalrejects aQuadraticExpressionoutright, so
assert_model_equalcould not be used on a QP model at all, and it nowcatches a mismatch in expression type.
separate.
to_netcdf/read_netcdfdowngraded the objective exactly the same way, so asaved QP came back an LP. The expression type is now stored next to the objective and
restored on read, falling back to the presence of the factor dimension for files written
by earlier versions.
Issue repro, before and after