Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
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 @@ -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 <https://github.com/PyPSA/linopy/issues/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 <https://github.com/PyPSA/linopy/issues/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 <https://github.com/PyPSA/linopy/issues/934>`__)
Expand Down
13 changes: 11 additions & 2 deletions linopy/csr.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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):
Expand Down Expand Up @@ -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 = {}
Expand Down
18 changes: 18 additions & 0 deletions test/test_io.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Loading