Skip to content

CSRConstraint: use label columns; frozen constraints break after later variable add/remove #926

Description

@FabianHofmann

Note

The following content was generated by AI (Claude Code), reviewed by @FabianHofmann.

Bug: frozen constraints break after any later variable change

CSRConstraint stores dense variable positions as column indices and a fixed column count (n_active_vars at freeze time). Any add_variables or remove_variables after freezing changes both, so model.matrices fails when stacking the blocks:

ValueError: inconsistent shapes (3, 6) and (1, 8)

This is independent of #870; it reproduces on master with a plain dense freeze=True constraint.

Reproduction
import linopy, pandas as pd

m = linopy.Model()
i = pd.RangeIndex(3, name="i")
a = m.add_variables(coords=[i], name="a")
b = m.add_variables(coords=[i], name="b")
m.add_constraints(2 * b >= 1, name="c1", freeze=True)

d = m.add_variables(coords=[pd.RangeIndex(2, name="i")], name="d")
m.add_constraints(d <= 5, name="c2", freeze=True)
m.matrices.A  # ValueError: inconsistent shapes (3, 6) and (1, 8)

# same after m.remove_variables("a") instead of adding: (3, 6) vs (1, 5)

Proposal: label columns for CSRConstraint

Store raw variable labels as column indices (width model._xCounter), and map labels to dense positions once at matrix assembly via VariableLabelIndex.label_to_pos. This is the convention CSRPayload (#870) already uses for expressions, so the two CSR representations would agree.

Consequences:

  1. to_matrix / to_matrix_with_rhs gather positions through label_to_pos and return n_active_vars-wide blocks; the bug above disappears by construction. has_labels, to_polars and the dataset reconstruction no longer need vlabels lookups.
  2. from_payload drops its label-to-position mapping and no longer needs the label index at construction time.
  3. The netcdf _linopy_format: csr writer stores labels; the reader gets a compatibility path for files written with positions (they carry vlabels in the same file).
  4. LinearExpression.to_constraint on a payload-backed expression can return an unassigned CSRConstraint (cindex=None) directly. Constraint._from_pending, the lazy Constraint.data branch and extract_pending introduced in feat(v1): CSR-backed sparse groupby-sum - skew-independent build memory (6-9x on the #745 hub case) #870 as a stopgap can then be deleted; add_constraints only assigns labels.
  5. linopy/persistent/snapshot.py reads to_matrix_with_rhs output and needs a check that buffer ownership assumptions still hold.

This is also the prerequisite for making the CSR-backed constraint the default without a dense-to-sparse conversion step.

Refs: #870

Activity

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions