Skip to content
2 changes: 2 additions & 0 deletions doc/release_notes.rst
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ Release Notes
Upcoming Version
----------------

* ``read_netcdf`` no longer drops parameters, parameter attributes or ``MultiIndex`` dimensions whose name contains ``-``. (`#1025 <https://github.com/PyPSA/linopy/pull/1025>`__)

Version 0.10.0
--------------

Expand Down
28 changes: 20 additions & 8 deletions linopy/io.py
Original file line number Diff line number Diff line change
Expand Up @@ -1218,9 +1218,6 @@ def read_netcdf(path: Path | str, **kwargs: Any) -> Model:
ds = xr.load_dataset(path, **kwargs)
m = Model(sparse=bool(ds.attrs.get("sparse", False)))

def has_prefix(k: str, prefix: str) -> bool:
return k.rsplit("-", 1)[0] == prefix

def remove_prefix(k: str, prefix: str) -> str:
return k[len(prefix) + 1 :]

Expand All @@ -1231,7 +1228,7 @@ def parse_multiindex_attr(value: str | Iterable[str]) -> list[str]:
return [str(n) for n in value]

def get_prefix(ds: xr.Dataset, prefix: str) -> xr.Dataset:
ds = ds[[k for k in ds if has_prefix(str(k), prefix)]]
ds = ds[[k for k in ds if owners[str(k)] == prefix]]
multiindexes = []
for dim in ds.dims:
attr = ds.attrs.get(f"{dim}_multiindex")
Expand All @@ -1245,7 +1242,7 @@ def get_prefix(ds: xr.Dataset, prefix: str) -> xr.Dataset:
ds.attrs = {
remove_prefix(k, prefix): v
for k, v in ds.attrs.items()
if has_prefix(k, prefix)
if owners[k] == prefix
}

for dim in ds.dims:
Expand All @@ -1269,15 +1266,30 @@ def container_names(kind: str) -> list[str]:
)
return ordered

containers = {
kind: container_names(kind)
for kind in ("variables", "expressions", "constraints")
}
prefixes = {"objective", "parameters"}.union(*containers.values())

def owner(k: str) -> str | None:
while "-" in k:
k = k.rsplit("-", 1)[0]
if k in prefixes:
return k
return None

owners = {k: owner(k) for k in [*map(str, ds), *ds.attrs]}

variables = {}
for k in container_names("variables"):
for k in containers["variables"]:
name = remove_prefix(k, "variables")
variables[name] = Variable(get_prefix(ds, k), m, name)

m._variables = Variables(variables, m)

expressions: dict[str, LinearExpression | QuadraticExpression] = {}
for k in container_names("expressions"):
for k in containers["expressions"]:
name = remove_prefix(k, "expressions")
expr_ds = get_prefix(ds, k)
expr_type = expr_ds.attrs.pop(EXPR_TYPE_ATTR, None)
Expand All @@ -1295,7 +1307,7 @@ def container_names(kind: str) -> list[str]:
m._expressions = Expressions(expressions, m)

constraints: dict[str, ConstraintBase] = {}
for k in container_names("constraints"):
for k in containers["constraints"]:
name = remove_prefix(k, "constraints")
con_ds = get_prefix(ds, k)
if con_ds.attrs.get("_linopy_format") == "csr":
Expand Down
38 changes: 38 additions & 0 deletions test/test_io.py
Original file line number Diff line number Diff line change
Expand Up @@ -360,6 +360,44 @@ def test_model_to_netcdf_with_dash_names(
assert_model_equal(m, p)


def test_model_to_netcdf_with_dash_named_parameters(
model: Model, tmp_path: Path
) -> None:
m = model
m.parameters["my-param"] = xr.DataArray(
[5, 6], dims=["my-dim"], coords={"my-dim": ["a", "b"]}
)
m.parameters.attrs["my-attr"] = "kept"
fn = tmp_path / "test.nc"
m.to_netcdf(fn)
p = read_netcdf(fn)

assert_model_equal(m, p)
assert p.parameters.attrs == m.parameters.attrs


def test_model_to_netcdf_with_dash_named_multiindex_dim(tmp_path: Path) -> None:
from linopy.semantics import is_v1

if is_v1():
pytest.skip("v1 rejects MultiIndex; this model only builds under legacy")
index = pd.MultiIndex.from_tuples(
[(1, "a"), (1, "b"), (2, "a")], names=["first", "second"]
)
coords = xr.Coordinates.from_pandas_multiindex(index, "multi-dim")
m = Model()
x = m.add_variables(coords=coords, name="x")
m.add_objective(x.sum())
m.parameters["param"] = xr.DataArray([1, 2, 3], coords=coords)
fn = tmp_path / "test.nc"
m.to_netcdf(fn)
p = read_netcdf(fn)

assert_model_equal(m, p)
for index in (p.variables["x"].indexes, p.parameters.indexes):
assert isinstance(index["multi-dim"], pd.MultiIndex)


def test_model_to_netcdf_with_status_and_condition(
model_with_dash_names: Model, tmp_path: Path
) -> None:
Expand Down
Loading