From 687e81969076d2ed5af327edcd92ac968b1eff38 Mon Sep 17 00:00:00 2001 From: FBumann <117816358+FBumann@users.noreply.github.com> Date: Sun, 14 Jun 2026 18:29:31 +0200 Subject: [PATCH] fix: normalize clustered solution dim order to (cluster, time) linopy <0.8 stores the extra-timestep charge_state variable as (time, cluster), because its 'time' coordinate (length n+1) conflicts with the model's 'time' (length n), and 0.7 reorders the conflicting dim to the front. Every other variable is (cluster, time). linopy >=0.8 makes coords the source of truth and is already consistent. flixopt cannot control this from the bounds/coords it passes (0.7 reorders internally), so normalize in the solution property: transpose 'cluster' before 'time'. This is a no-op on linopy >=0.8 and for non-clustered systems, and only touches the cluster/time axis ordering (other dims and scalars are preserved via the ellipsis). Also update the clustering test to access charge_state by label and assert the now-deterministic (cluster, time) order. Unblocks the linopy 0.8 bump in #701. Co-Authored-By: Claude Opus 4.8 (1M context) --- flixopt/structure.py | 2 ++ tests/test_math/test_clustering.py | 8 +++----- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/flixopt/structure.py b/flixopt/structure.py index 7fd89e3f8..983681951 100644 --- a/flixopt/structure.py +++ b/flixopt/structure.py @@ -335,6 +335,8 @@ def solution(self): if 'time' in solution.coords: if not solution.indexes['time'].equals(self.flow_system.timesteps_extra): solution = solution.reindex(time=self.flow_system.timesteps_extra) + if 'cluster' in solution.dims and 'time' in solution.dims: + solution = solution.transpose('cluster', 'time', ...) return solution @property diff --git a/tests/test_math/test_clustering.py b/tests/test_math/test_clustering.py index 8c7917502..aaa37923c 100644 --- a/tests/test_math/test_clustering.py +++ b/tests/test_math/test_clustering.py @@ -336,12 +336,10 @@ def test_storage_cyclic_charge_discharge_pattern(self, optimize): discharge_fr = fs.solution['Battery(discharge)|flow_rate'].values[:, :4] assert_allclose(discharge_fr, [[0, 50, 0, 50], [0, 50, 0, 50]], atol=1e-5) - # Charge state: dims=(time, cluster), 5 entries (incl. final) - # Cyclic: SOC wraps, starting with pre-charge from previous cycle charge_state = fs.solution['Battery|charge_state'] - assert charge_state.dims == ('time', 'cluster') - cs_c0 = charge_state.values[:5, 0] - cs_c1 = charge_state.values[:5, 1] + assert charge_state.dims == ('cluster', 'time') + cs_c0 = charge_state.isel(cluster=0).values[:5] + cs_c1 = charge_state.isel(cluster=1).values[:5] assert_allclose(cs_c0, [50, 50, 0, 50, 0], atol=1e-5) assert_allclose(cs_c1, [100, 100, 50, 100, 50], atol=1e-5)