From 19252c55776633f6778ab0871860cab0323afa44 Mon Sep 17 00:00:00 2001 From: FBumann <117816358+FBumann@users.noreply.github.com> Date: Sat, 13 Jun 2026 18:26:19 +0200 Subject: [PATCH 1/2] feat(io): stamp linopy version into netCDF export Record the writing linopy version under the ``_linopy_version`` dataset attribute in ``to_netcdf``. This is provenance today and a future key for format-compatibility branching on read. Files written by older versions (no attribute) keep reading unchanged. Co-Authored-By: Claude Opus 4.8 (1M context) --- doc/release_notes.rst | 2 ++ linopy/io.py | 4 ++++ test/test_io.py | 26 ++++++++++++++++++++++++++ 3 files changed, 32 insertions(+) diff --git a/doc/release_notes.rst b/doc/release_notes.rst index 9b1ecbd8..81a4b562 100644 --- a/doc/release_notes.rst +++ b/doc/release_notes.rst @@ -4,6 +4,8 @@ Release Notes Upcoming Version ---------------- +* ``Model.to_netcdf`` now records the writing linopy version in the ``_linopy_version`` dataset attribute. Files written by older versions (without the attribute) continue to read unchanged. + Version 0.8.0 ------------- diff --git a/linopy/io.py b/linopy/io.py index 1c4714c4..220a230a 100644 --- a/linopy/io.py +++ b/linopy/io.py @@ -12,6 +12,7 @@ import time import warnings from collections.abc import Callable, Iterable +from importlib.metadata import version from io import BufferedWriter from pathlib import Path from tempfile import TemporaryDirectory @@ -37,6 +38,8 @@ logger = logging.getLogger(__name__) +NETCDF_VERSION_ATTR = "_linopy_version" + ufunc_kwargs = dict(vectorize=True) concat_kwargs = dict(dim=CONCAT_DIM, coords="minimal") @@ -971,6 +974,7 @@ def with_prefix(ds: xr.Dataset, prefix: str) -> xr.Dataset: scalars = {k: getattr(m, k) for k in m.scalar_attrs} ds = xr.merge(vars + cons + obj + params, combine_attrs="drop_conflicts") ds = ds.assign_attrs(scalars) + ds.attrs[NETCDF_VERSION_ATTR] = version("linopy") if m._relaxed_registry: ds.attrs["_relaxed_registry"] = json.dumps(m._relaxed_registry) if m._piecewise_formulations: diff --git a/test/test_io.py b/test/test_io.py index fba65aab..b108926f 100644 --- a/test/test_io.py +++ b/test/test_io.py @@ -221,6 +221,32 @@ def test_read_netcdf_with_multiindex_legacy_list_attr( assert_model_equal(m, read_netcdf(fn_legacy)) +def test_model_to_netcdf_stamps_version(model: Model, tmp_path: Path) -> None: + from importlib.metadata import version + + from linopy.io import NETCDF_VERSION_ATTR + + fn = tmp_path / "test.nc" + model.to_netcdf(fn) + + ds = xr.load_dataset(fn) + assert ds.attrs[NETCDF_VERSION_ATTR] == version("linopy") + + +def test_read_netcdf_without_version_stamp(model: Model, tmp_path: Path) -> None: + from linopy.io import NETCDF_VERSION_ATTR + + fn = tmp_path / "test.nc" + model.to_netcdf(fn) + + ds = xr.load_dataset(fn).load() + del ds.attrs[NETCDF_VERSION_ATTR] + fn_legacy = tmp_path / "legacy.nc" + ds.to_netcdf(fn_legacy) + + assert_model_equal(model, read_netcdf(fn_legacy)) + + @pytest.mark.skipif("gurobi" not in available_solvers, reason="Gurobipy not installed") def test_to_file_lp(model: Model, tmp_path: Path) -> None: import gurobipy From 428227cc66880fd5d4cef08181b6904f2ec2c9ab Mon Sep 17 00:00:00 2001 From: FBumann <117816358+FBumann@users.noreply.github.com> Date: Sat, 13 Jun 2026 18:32:01 +0200 Subject: [PATCH 2/2] test(io): drop tautological version-stamp assertion The equal-to-__version__ test mirrored the assignment one-to-one. Keep only the backward-compat test that an unstamped (pre-versioning) file still reads. Co-Authored-By: Claude Opus 4.8 (1M context) --- test/test_io.py | 12 ------------ 1 file changed, 12 deletions(-) diff --git a/test/test_io.py b/test/test_io.py index b108926f..dd614c5e 100644 --- a/test/test_io.py +++ b/test/test_io.py @@ -221,18 +221,6 @@ def test_read_netcdf_with_multiindex_legacy_list_attr( assert_model_equal(m, read_netcdf(fn_legacy)) -def test_model_to_netcdf_stamps_version(model: Model, tmp_path: Path) -> None: - from importlib.metadata import version - - from linopy.io import NETCDF_VERSION_ATTR - - fn = tmp_path / "test.nc" - model.to_netcdf(fn) - - ds = xr.load_dataset(fn) - assert ds.attrs[NETCDF_VERSION_ATTR] == version("linopy") - - def test_read_netcdf_without_version_stamp(model: Model, tmp_path: Path) -> None: from linopy.io import NETCDF_VERSION_ATTR