diff --git a/doc/release_notes.rst b/doc/release_notes.rst index 7ac128671..cd70a2b7a 100644 --- a/doc/release_notes.rst +++ b/doc/release_notes.rst @@ -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 ------------- diff --git a/linopy/constraints.py b/linopy/constraints.py index 96e2a8433..fce1e5a6e 100644 --- a/linopy/constraints.py +++ b/linopy/constraints.py @@ -1081,7 +1081,7 @@ 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) + vars_flat = con.vars.values.reshape(len(labels_flat), con.nterm) active_mask = (labels_flat != -1) & (vars_flat != -1).any(axis=1) rhs = con.rhs.values.ravel()[active_mask] sign_vals = con.sign.values.ravel() diff --git a/test/test_constraint.py b/test/test_constraint.py index a684b9664..b8075edde 100644 --- a/test/test_constraint.py +++ b/test/test_constraint.py @@ -104,6 +104,27 @@ def test_empty_constraints_repr() -> None: Model().constraints.__repr__() +@pytest.mark.parametrize("freeze_constraints", [True, False]) +def test_constraint_handles_empty_rows(freeze_constraints: bool) -> None: + """An empty constraint group must be accepted and solve cleanly.""" + + m = Model(freeze_constraints=freeze_constraints) + 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.ConstraintBase) + 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_cannot_create_constraint_without_variable() -> None: model = linopy.Model() with pytest.raises(ValueError):