Repository navigation
feat: fold single-variable constraints into column bounds (opt-in, with dual recovery) - #1121
Draft
FabianHofmann wants to merge 3 commits into
Draft
FabianHofmann wants to merge 3 commits into
FabianHofmann wants to merge 3 commits into
Conversation
Add `Model.add_constraints(..., as_bound=True)`. Every active row of such a constraint must hold exactly one variable term `c * x`; when the model is lowered (Model.matrices, direct solver APIs, LP and MPS writers) the row tightens the bounds of `x` to `rhs / c` instead of becoming a matrix row. Several rows on a column and the variable's own bounds combine to the tightest; `Matrices.bound_rows` records which row sets each side. After the solve, the duals of these rows are recovered from the reduced costs `d = c + Q x - A^T y` (user units): the row setting the active bound gets `d / c`, all others 0. Verified against ordinary rows for HiGHS and Gurobi (direct, LP, MPS; min/max; LP/QP; with variable, constraint and objective scaling). Other solvers get NaN duals with a warning. Persistent updates rebuild (RebuildReason.BOUND_ROWS), dualize treats the rows as ordinary rows, penalty/soften and semi-continuous variables are rejected. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01F4nx8GJ8mSNvkTagja9dMj
Merging this PR will not alter performance1 note
Comparing Footnotes
|
- Skip dual recovery for mixed-integer models (NaN, no warning); HiGHS returned c/coeff on folded rows of a MILP. - Skip folded constraints in the generic dual loop. - Verify dual recovery for CPLEX, Xpress and COPT too; tests derive their solvers from BOUND_ROW_DUAL_SOLVERS. - Drop BoundRows.take, Constraints.as_bound and the unreachable indicator branch; reuse _row_block and one validation path. - Fold bounds in user units and scale them only in Matrices, so the LP writer no longer scales and unscales them. - Simpler _tightest; exclude folded rows from ncons; trim docs. - Tests for constant lhs, row scaling, masked rows, infeasibility and tie rules. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01F4nx8KhK8QzSzHhpYKvcSx
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01F4nx8GJ8mSNvkTagja9dMj
Collaborator
Author
|
Note: this is quite an experimental feature and I am not sure whether we want to have that. it is nice, but also feels "hacky". we should make some research for a) alternatives and b) whether other communities use such a feature |
This branch has not been deployed
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.
Closes # (if applicable).
Changes proposed in this Pull Request
Note
The following content was generated by AI.
Adds an opt-in
Model.add_constraints(..., as_bound=True). Every active row of such a constraint must hold exactly one variable termc * xwith a nonzero coefficient. If a row does not,add_constraintsraises, and so does lowering if the constraint was changed after it was added. When the model is lowered, the row is not emitted as a matrix row. Instead it tightens the bounds ofxtorhs / c: the direction flips forc < 0, and=sets both bounds. Lowering coversModel.matrices, and so every direct backend, the MPS writer andrestrict, plus the LP writer, which writes from the constraints directly. Several rows on one column and the variable's own bounds combine to the tightest bound.Matrices.bound_rowsrecords which row sets each side. Default behaviour is unchanged.Motivation: in PyPSA models most rows are bounds written as constraints (83 % of the rows on SciGrid-DE, 52 % on model-energy). PyPSA needs their duals, e.g. for
mu_upper.Dual recovery. The solver reports no dual for folded rows. In
Model.assign_result, linopy rebuilds the reduced costs from the duals of the remaining rows,d = c + Q x - A^T y. This is computed in solver units and converted to user units withvar_scaling / objective_scaling. The row that sets the active bound getsd_j / c, with the side taken fromsign(d)and the objective sense. Every other folded row gets 0. This is the dual the same row gets as an ordinary constraint, under linopy's convention thatc - A^T y = z, withz >= 0at a lower bound for min andz <= 0for max. The method is backend-agnostic and needs no reduced-cost API, but it only holds if a backend's dual signs follow that convention.Supported / verified: HiGHS and Gurobi (the
BOUND_ROW_DUAL_SOLVERSset), withio_apidirect,lpandmps, for min and max, LP and QP objectives, and with and without variable, constraint and objective scaling. In every case the folded duals match the duals of the same model with ordinary rows to 1e-7 (1e-5 for QP). The cases cover<=,>=,=, a negative coefficient, and two bound rows on one column where only the binding one gets the dual.Not verified: every other solver. Its folded rows get NaN duals and a
UserWarning. MILP is fine and reports no duals, as usual.Interactions:
track_updatesalways rebuilds (RebuildReason.BOUND_ROWS), because the snapshot buffers do not track folded rows.dualize(): treats the folded constraints as ordinary rows.ValueError:penalty=/soften(), and rows on semi-continuous variables. Indicator constraints cannot use the flag, since it exists only onadd_constraints.Matrices.take: remapsbound_rows.restrictwindows: the rows act as bounds, but their duals are not recovered.compute_infeasibilities): only reports matrix rows.Open design questions
as_bound. Alternatives arefold_boundsorbound=True.m.matricesonce inassign_result, which costs one extra matrix build per solve when folded constraints exist. Alternatives: cache the matrices from the build, or read native reduced costs (HiGHScol_dual, GurobiRC) through a newSolution.reduced_costfield.BOUND_ROW_DUAL_SOLVERSonce verified in CI (e.g. Xpress, CPLEX, MOSEK)?ConstraintLabelIndex.clabelsnow excludes folded constraints, so they are not matrix rows anywhere: the solver, the IIS mapping, persistent and restrict.Constraints.to_matrix()still includes them.Files and tests
linopy/matrices.py:BoundRows,_fold_bound_rows,bound_row_duals,folded_bounds,BOUND_ROW_DUAL_SOLVERS.Matrices.bound_rowsis a new optional field.linopy/model.py: theas_boundargument, and_assign_bound_row_dualscalled fromassign_result.linopy/constraints.py:Constraint.as_bound,Constraint.check_bound_rows,Constraints.as_bound, and thesoftenguard.linopy/common.py:clabelsexcludes folded constraints.linopy/io.py: the LP writer skips folded rows and writes the folded bounds, including on binaries.linopy/persistent/diff.py:RebuildReason.BOUND_ROWS.linopy/dualization.py: dualizes folded constraints as ordinary rows.doc/bound-rows.rst, the index, the user guide, and a release note under Version 1.0.0 / Performance.test/test_bound_rows.py: 49 tests.pytest test -qgives 5306 passed, 117 skipped.ruff check,ruff format --checkandmypy linopyare clean.No benchmark on PyPSA-sized models was run in this PR.
Checklist
AGENTS.md).doc.doc/release_notes.rstof the upcoming release is included.Generated by Claude Code