Skip to content

Commit 8dfa6d4

Browse files
Ketchpclaude
andauthored
fix(constraints): freeze empty constraint groups without reshape error (#783)
* fix(constraints): freeze empty constraint groups without reshape error CSRConstraint.from_mutable reshaped con.vars with an inferred -1 dimension. For an empty constraint group (zero rows and zero terms) the vars array has size 0, and NumPy refuses to infer a (0, -1) reshape, raising "cannot reshape array of size 0 into shape (0,newaxis)". This broke the documented lossless freeze round-trip for legitimately empty groups (e.g. shifted-time difference constraints at n_time == 1). Pass the explicit _term count instead of -1; NumPy accepts a (0, 0) reshape and the rest of the method already handles zero-row input. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * chore: code review --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent c2d9f5c commit 8dfa6d4

3 files changed

Lines changed: 23 additions & 1 deletion

File tree

doc/release_notes.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ Upcoming Version
1313
**Bug fixes**
1414

1515
* 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)
16+
* 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.
1617

1718
Version 0.8.0
1819
-------------

linopy/constraints.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1081,7 +1081,7 @@ def from_mutable(
10811081
# Build active_mask aligned with con_labels (rows in csr)
10821082
# Use same filter as to_matrix: label != -1 AND at least one var != -1
10831083
labels_flat = con.labels.values.ravel()
1084-
vars_flat = con.vars.values.reshape(len(labels_flat), -1)
1084+
vars_flat = con.vars.values.reshape(len(labels_flat), con.nterm)
10851085
active_mask = (labels_flat != -1) & (vars_flat != -1).any(axis=1)
10861086
rhs = con.rhs.values.ravel()[active_mask]
10871087
sign_vals = con.sign.values.ravel()

test/test_constraint.py

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -104,6 +104,27 @@ def test_empty_constraints_repr() -> None:
104104
Model().constraints.__repr__()
105105

106106

107+
@pytest.mark.parametrize("freeze_constraints", [True, False])
108+
def test_constraint_handles_empty_rows(freeze_constraints: bool) -> None:
109+
"""An empty constraint group must be accepted and solve cleanly."""
110+
111+
m = Model(freeze_constraints=freeze_constraints)
112+
x = m.add_variables(
113+
lower=0.0,
114+
coords=[range(3), range(2)],
115+
dims=["time", "product"],
116+
name="x",
117+
)
118+
empty = x.isel(time=range(1, 1))
119+
c = m.add_constraints(empty == 0, name="empty")
120+
assert isinstance(c, linopy.constraints.ConstraintBase)
121+
assert c.size == 0
122+
# Solving a model with only an empty constraint group is also fine.
123+
m.add_objective(x.sum())
124+
m.solve("highs", io_api="direct", output_flag=False)
125+
assert m.status == "ok"
126+
127+
107128
def test_cannot_create_constraint_without_variable() -> None:
108129
model = linopy.Model()
109130
with pytest.raises(ValueError):

0 commit comments

Comments
 (0)