diff --git a/doc/release_notes.rst b/doc/release_notes.rst index 4c8bb82f..d866d86d 100644 --- a/doc/release_notes.rst +++ b/doc/release_notes.rst @@ -94,6 +94,7 @@ Upcoming Version * ``sum()`` over a dimension no longer raises when another dimension of the expression has size 0; it returns an expression without terms over the kept coordinates, as summing over the empty dimension itself already did. (https://github.com/PyPSA/linopy/issues/906) * A multi-key ``groupby`` now returns its groups sorted by key tuple, like the single-key path. The key combinations were numbered by iterating a ``set``, so the group order was arbitrary and changed between processes with ``PYTHONHASHSEED``. * The ``linopy.options`` context manager now restores the values that were active on entry instead of resetting all options to their defaults. +* Freezing a constraint (``freeze=True``) with a ``MultiIndex`` dimension no longer raises ``TypeError: Must pass list-like as `names```. Building the CSR grid renamed the index to its dimension, which renames the levels of a ``MultiIndex``. * ``Model.copy`` (and the ``copy.copy``/``copy.deepcopy`` protocols) no longer downgrades a quadratic objective to a linear one. The objective was rebuilt as a ``LinearExpression`` regardless of its type, so the copy silently solved a different problem. ``linopy.testing.assert_model_equal`` now compares the objective by expression type as well, which it could not do before. (`#903 `__) * ``Model.to_netcdf``/``linopy.read_netcdf`` downgraded a quadratic objective the same way; the expression type is now stored alongside the objective and restored on read. Files written by earlier versions are read as before. (`#903 `__) * ``linopy.read_netcdf`` now restores variables, expressions and constraints in their original insertion order instead of alphabetically, so ``matrices.A`` of a round-tripped model is no longer a row/column permutation of the original. The order is stored in the file; files written by earlier versions still load in sorted order. ``linopy.testing.assert_model_equal`` now also compares container order. (`#934 `__) diff --git a/linopy/csr.py b/linopy/csr.py index 7c27463a..3a5f3f16 100644 --- a/linopy/csr.py +++ b/linopy/csr.py @@ -92,7 +92,7 @@ def from_coords(cls, coords: Iterable[pd.Index]) -> Grid: def from_dataset(cls, ds: Dataset | DataArray, dims: Iterable[str]) -> Grid: """Build from the indexes and auxiliary coordinates ``ds`` carries on ``dims``.""" dims = tuple(dims) - indexes = {d: ds.get_index(d).rename(d) for d in dims} + indexes = {d: _named(ds.get_index(d), d) for d in dims} return cls(indexes, _aux_coords(ds, set(dims))) @classmethod @@ -373,7 +373,7 @@ def from_grouper( ) for d in coord_dims: if d != member_dim: - indexes[d] = ds.get_index(d).rename(d) + indexes[d] = _named(ds.get_index(d), d) aux |= _aux_coords(ds, set(coord_dims) - {member_dim}) grid = Grid({d: indexes[d] for d in grid_dims}, aux) if isinstance(source, Dataset): @@ -837,6 +837,15 @@ def _readonly(values: np.ndarray) -> np.ndarray: return values +def _named(index: pd.Index, dim: str) -> pd.Index: + """``index`` named after ``dim``, keeping the level names of a MultiIndex.""" + if isinstance(index, pd.MultiIndex): + index = index.copy() + index.name = dim + return index + return index.rename(dim) + + def _aux_coords(ds: Dataset | DataArray, dims: set[str]) -> AuxCoords: """Scalar and one-dimensional auxiliary coordinates of ``ds`` lying on ``dims``.""" aux: AuxCoords = {} diff --git a/test/test_io.py b/test/test_io.py index dabee6f5..b3566434 100644 --- a/test/test_io.py +++ b/test/test_io.py @@ -399,6 +399,24 @@ def test_model_to_netcdf_with_multiindex( assert_model_equal(m, p) +def test_model_to_netcdf_with_multiindex_frozen_constraint( + model_with_multiindex: Model, tmp_path: Path +) -> None: + from linopy.constraints import CSRConstraint + + m = model_with_multiindex + x, y = m.variables["x-var"], m.variables["y-var"] + con = m.add_constraints(x + y, LESS_EQUAL, 10, name="frozen", freeze=True) + assert isinstance(con, CSRConstraint) + + fn = tmp_path / "test.nc" + m.to_netcdf(fn) + p = read_netcdf(fn) + + assert isinstance(p.constraints["frozen"], CSRConstraint) + assert_model_equal(m, p) + + # Regression for https://github.com/PyPSA/linopy/issues/525. def test_model_to_netcdf_with_multiindex_scipy_engine( model_with_multiindex: Model, tmp_path: Path