Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions doc/release_notes.rst
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ Upcoming Version
**Bug fixes**

* LP file export now honors bounds tightened below ``[0, 1]`` on a binary variable via the ``.lower``/``.upper`` setters after creation (e.g. ``upper = 0``). Previously such bounds were written only by ``io_api="direct"`` and dropped by ``io_api="lp"``. (https://github.com/PyPSA/linopy/issues/776)
* Freezing an empty constraint group (e.g. an empty ``isel`` slice) no longer raises ``ValueError: cannot reshape array of size 0``. ``Model(freeze_constraints=True)`` and ``Constraint.freeze()`` now round-trip zero-row constraints losslessly.

Version 0.8.0
-------------
Expand Down
5 changes: 4 additions & 1 deletion linopy/constraints.py
Original file line number Diff line number Diff line change
Expand Up @@ -1081,7 +1081,10 @@ def from_mutable(
# Build active_mask aligned with con_labels (rows in csr)
# Use same filter as to_matrix: label != -1 AND at least one var != -1
labels_flat = con.labels.values.ravel()
vars_flat = con.vars.values.reshape(len(labels_flat), -1)
# Explicit term count, not -1: NumPy can't infer a (0, -1) reshape for
# an empty group (size-0 vars). _term is the trailing dim of con.vars.
nterm = con.vars.sizes.get(TERM_DIM, 0)
vars_flat = con.vars.values.reshape(len(labels_flat), nterm)
Comment thread
Ketchp marked this conversation as resolved.
Outdated
active_mask = (labels_flat != -1) & (vars_flat != -1).any(axis=1)
rhs = con.rhs.values.ravel()[active_mask]
sign_vals = con.sign.values.ravel()
Expand Down
25 changes: 25 additions & 0 deletions test/test_constraint.py
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,31 @@ def test_add_constraints_uses_model_freeze_default() -> None:
)


def test_csr_constraint_handles_empty_rows() -> None:
Comment thread
Ketchp marked this conversation as resolved.
Outdated
"""
An empty constraint group must round-trip through freeze.

Regression test: `CSRConstraint.from_mutable` used to reshape
`con.vars` with an inferred `-1` dimension, which NumPy refuses for a
size-0 array (`cannot reshape array of size 0 into shape (0,newaxis)`).
"""
m = Model(freeze_constraints=True)
x = m.add_variables(
lower=0.0,
coords=[range(3), range(2)],
dims=["time", "product"],
name="x",
)
empty = x.isel(time=range(1, 1))
c = m.add_constraints(empty == 0, name="empty")
assert isinstance(c, linopy.constraints.CSRConstraint)
assert c.size == 0
# Solving a model with only an empty constraint group is also fine.
m.add_objective(x.sum())
m.solve("highs", io_api="direct", output_flag=False)
assert m.status == "ok"


def test_constraint_name(c: linopy.constraints.CSRConstraint) -> None:
assert c.name == "c"

Expand Down
Loading