From 7932c5afbc58e6706026997580c3d21cb2dc2b40 Mon Sep 17 00:00:00 2001 From: Florian Maurer Date: Wed, 15 Apr 2026 12:04:35 +0200 Subject: [PATCH 1/3] fix: use xarray.Dataset copy instead of constructor since the latest xarray version, passing a Dataset as `data_vars` to the Dataset constructor is not supported. Use `ds.copy()` to create a copy of a Dataset. --- doc/release_notes.rst | 1 + linopy/expressions.py | 10 ++++++---- linopy/model.py | 5 ++++- 3 files changed, 11 insertions(+), 5 deletions(-) diff --git a/doc/release_notes.rst b/doc/release_notes.rst index 54c98f43..4bcf0bf5 100644 --- a/doc/release_notes.rst +++ b/doc/release_notes.rst @@ -27,6 +27,7 @@ Upcoming Version * Add ``format_labels()`` on ``Constraints``/``Variables`` and ``format_infeasibilities()`` on ``Model`` that return strings instead of printing to stdout, allowing usage with logging, storage, or custom output handling. Deprecate ``print_labels()`` and ``print_infeasibilities()``. * Add ``fix()``, ``unfix()``, and ``fixed`` to ``Variable`` and ``Variables`` for fixing variables to values via equality constraints. Supports automatic rounding for integer/binary variables. * Add ``relax()``, ``unrelax()``, and ``relaxed`` to ``Variable`` and ``Variables`` for LP relaxation of integer/binary variables. Supports partial relaxation via filtered views (e.g. ``m.variables.integers.relax()``). Semi-continuous variables raise ``NotImplementedError``. +* Add compatibility to latest xarray version. Version 0.6.6 diff --git a/linopy/expressions.py b/linopy/expressions.py index ca491c3e..43f4d300 100644 --- a/linopy/expressions.py +++ b/linopy/expressions.py @@ -284,7 +284,7 @@ def sum(self, use_fallback: bool = False, **kwargs: Any) -> LinearExpression: index.names = [str(col) for col in orig_group.columns] index.name = GROUP_DIM new_coords = Coordinates.from_pandas_multiindex(index, GROUP_DIM) - ds = xr.Dataset(ds.assign_coords(new_coords)) + ds = ds.assign_coords(new_coords).copy() ds = ds.rename({GROUP_DIM: final_group_name}) return LinearExpression(ds, self.model) @@ -391,8 +391,10 @@ def __init__(self, data: Dataset | Any | None, model: Model) -> None: coeffs_vars_dict = {str(k): v for k, v in coeffs_vars.items()} data = assign_multiindex_safe(data, **coeffs_vars_dict) - # transpose with new Dataset to really ensure correct order - data = Dataset(data.transpose(..., TERM_DIM)) + # Ensure correct dimension order and materialize a new Dataset. + # Wrapping an existing Dataset in Dataset(...) is no longer supported + # by newer xarray versions. + data = data.transpose(..., TERM_DIM).copy() # ensure helper dimensions are not set as coordinates if drop_dims := set(HELPER_DIMS).intersection(data.coords): @@ -2098,7 +2100,7 @@ def __init__(self, data: Dataset | None, model: Model) -> None: raise ValueError(f"Size of dimension {FACTOR_DIM} must be 2.") # transpose data to have _term as last dimension and _factor as second last - data = xr.Dataset(data.transpose(..., FACTOR_DIM, TERM_DIM)) + data = data.transpose(..., FACTOR_DIM, TERM_DIM).copy() self._data = data @property diff --git a/linopy/model.py b/linopy/model.py index 2a635680..c197790f 100644 --- a/linopy/model.py +++ b/linopy/model.py @@ -359,7 +359,10 @@ def parameters(self, value: Dataset | Mapping) -> None: """ Set the parameters of the model. """ - self._parameters = Dataset(value) + if isinstance(value, Dataset): + self._parameters = value.copy() + else: + self._parameters = Dataset(value) @property def solution(self) -> Dataset: From a0d4928a737ca17b62f7f4c0571d5da9085ffcb9 Mon Sep 17 00:00:00 2001 From: Florian Maurer Date: Wed, 15 Apr 2026 13:39:40 +0200 Subject: [PATCH 2/3] fix: work around a bug in highs presolve this results in failing CI and wrong results otherwise --- test/test_optimization.py | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/test/test_optimization.py b/test/test_optimization.py index cdac8e61..fc709fb1 100644 --- a/test/test_optimization.py +++ b/test/test_optimization.py @@ -897,8 +897,17 @@ def test_modified_model( if solver not in feasible_mip_solvers: pytest.skip(f"{solver} does not support MIP") + # HiGHS 1.14 presolve has a bug where it incorrectly fixes binary variables + # when a continuous variable has a lower bound below (but near) the constraint + # RHS. Disabling presolve gives the correct optimal solution. + # See https://github.com/PyPSA/linopy/issues/648 + extra_kwargs: dict = {"presolve": "off"} if solver == "highs" else {} + status, condition = modified_model.solve( - solver, io_api=io_api, explicit_coordinate_names=explicit_coordinate_names + solver, + io_api=io_api, + explicit_coordinate_names=explicit_coordinate_names, + **extra_kwargs, ) assert condition == "optimal" assert (modified_model.solution.x == 0).all() From 67d507619040a42630f0c1364d2e23f7a9f7a6ad Mon Sep 17 00:00:00 2001 From: Florian Maurer Date: Wed, 15 Apr 2026 13:48:17 +0200 Subject: [PATCH 3/3] fix: suppress type error as union of two types does not resolve to GenericExpression --- linopy/expressions.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/linopy/expressions.py b/linopy/expressions.py index 43f4d300..8522e9ca 100644 --- a/linopy/expressions.py +++ b/linopy/expressions.py @@ -2386,7 +2386,10 @@ def merge( has_quad_expression = any(type(e) is QuadraticExpression for e in exprs) has_linear_expression = any(type(e) is LinearExpression for e in exprs) if cls is None: - cls = QuadraticExpression if has_quad_expression else LinearExpression + cls = cast( + type[GenericExpression], + QuadraticExpression if has_quad_expression else LinearExpression, + ) if cls is QuadraticExpression and dim == TERM_DIM and has_linear_expression: raise ValueError(