Skip to content

fix: support adding/removing variables after adding frozen constraint#806

Open
koen-vg wants to merge 2 commits into
PyPSA:masterfrom
koen-vg:fix/frozen-csr-active-vars
Open

fix: support adding/removing variables after adding frozen constraint#806
koen-vg wants to merge 2 commits into
PyPSA:masterfrom
koen-vg:fix/frozen-csr-active-vars

Conversation

@koen-vg

@koen-vg koen-vg commented Jul 1, 2026

Copy link
Copy Markdown

I like the new CSRConstraint and the memory benefits it comes with. The current implementation, however, breaks when you add or remove variables after adding a frozen constraint. This is because existing CSRConstraints record the number of active variables (matrix columns) in a shape parameter, which becomes stale when a new variable is added. When a variable (column) is removed, the CSR indices change and have to be rebuilt.

This PR makes it so CSR representations are updated when variables are added and removed. Especially in the case of adding variables, this is very low-impact, since the actual CSR data doesn't change; only the "shape" does.

I guess there is an argument to be made for a stricter kind of behavior: that no variables may be added or removed after any CSRConstraint has been added to the model. I believe that may be overly strict since the solution in this PR seems to work fine, but happy to discuss the alternative if anyone else has any insights. In any case, though, if the intention is that no variables may be added or removed when CSRConstaints are present, then this should be enforced properly at variable addition/removal. (Currently, you just get an obscure matrix alignment error when passing the model to a solver.)

Code and tests were written with the help of Claude Code.

Changes proposed in this Pull Request

  • Added a function to reshape CSR matrix returned by to_matrix in case any variables were added and matrix shape is stale.
  • Added a function to remap CSR columns for when a variable is removed.
  • Added tests for different orders of adding variables and CSRConstraints.

Checklist

  • AI-generated content is marked (see AGENTS.md).
  • Code changes are sufficiently documented; i.e. new functions contain docstrings and further explanations may be given in doc.
  • Unit tests for new features were added (if applicable).
  • A note for the release notes doc/release_notes.rst of the upcoming release is included.
  • I consent to the release of this PR's code under the MIT license.

@codspeed-hq

codspeed-hq Bot commented Jul 1, 2026

Copy link
Copy Markdown

Merging this PR will not alter performance

✅ 138 untouched benchmarks
⏩ 138 skipped benchmarks1


Comparing koen-vg:fix/frozen-csr-active-vars (b2b622c) with master (fe798b1)

Open in CodSpeed

Footnotes

  1. 138 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.

koen-vg added 2 commits July 1, 2026 11:27
Frozen (CSR-backed) constraints cache their matrix at freeze time, sized against
the active-variable set as it was then. Adding variables afterwards (e.g. an
auxiliary variable introduced by a constraint added after this one was frozen,
common under Model(freeze_constraints=True) when building incrementally) grows
the active-variable count, but CSRConstraint.to_matrix and to_matrix_with_rhs
returned the cached CSR verbatim, ignoring the VariableLabelIndex passed in. The
earlier-frozen blocks stay narrower than later ones, so stacking them fails with
"ValueError: incompatible dimensions for axis 1".

This breaks the invariant documented in PyPSA#630 -- to_matrix on both the mutable
and frozen constraint "always returns a csr matrix of shape (n_active_cons,
n_active_vars)" -- which the VariableLabelIndex exists precisely to uphold.

Widen the cached CSR to the current n_active_vars in a shared _csr_for_index
helper used by both to_matrix (matrix-accessor path) and to_matrix_with_rhs
(direct-solve path). Variable positions are assigned in encounter order and are
stable under additions, so widening only appends empty trailing columns.

Adds differential tests (freeze vs mutable must agree) covering the baseline,
variables added after freeze, and constraints linking later-added variables.
Frozen (CSR-backed) constraints store absolute dense variable positions from
freeze time. Removing a variable renumbers the positions of every later
variable, but Model.remove_variables left surviving frozen constraints with
those stale positions -- both the wrong width and the wrong columns -- producing
a corrupt constraint matrix (e.g. shape (2, 5) instead of (2, 2)) or an
IndexError at solve time. The mutable Constraint path handles removal correctly,
so this was a frozen-vs-mutable parity gap.

remove_variables now captures the pre-removal variable ordering and, once the
variable (and any constraints referencing it) are gone, remaps each surviving
frozen constraint's CSR columns through variable labels via the new
CSRConstraint._remap_columns. The old ordering is still available at the
mutation site, so no per-constraint label storage is needed; a frozen constraint
that referenced the removed variable has already been dropped, so every
remaining column maps to a valid new position.

Extends the differential tests with removal scenarios: first/middle-variable
removal, straddling references, sequential removals, remove-then-add, and the
drop-referencing-constraint parity case.
@koen-vg koen-vg force-pushed the fix/frozen-csr-active-vars branch from b690853 to b2b622c Compare July 1, 2026 18:28
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.

1 participant