Skip to content

Commit 371a9c6

Browse files
FBumannclaude
andcommitted
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) <noreply@anthropic.com>
1 parent b6d66d8 commit 371a9c6

2 files changed

Lines changed: 9 additions & 5 deletions

File tree

flixopt/structure.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -335,6 +335,11 @@ def solution(self):
335335
if 'time' in solution.coords:
336336
if not solution.indexes['time'].equals(self.flow_system.timesteps_extra):
337337
solution = solution.reindex(time=self.flow_system.timesteps_extra)
338+
# Normalise dim order to 'cluster' before 'time'. linopy <0.8 puts the
339+
# extra-timestep charge_state as (time, cluster) while every other
340+
# variable is (cluster, time); linopy >=0.8 is already consistent.
341+
if 'cluster' in solution.dims and 'time' in solution.dims:
342+
solution = solution.transpose('cluster', 'time', ...)
338343
return solution
339344

340345
@property

tests/test_math/test_clustering.py

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -336,12 +336,11 @@ def test_storage_cyclic_charge_discharge_pattern(self, optimize):
336336
discharge_fr = fs.solution['Battery(discharge)|flow_rate'].values[:, :4]
337337
assert_allclose(discharge_fr, [[0, 50, 0, 50], [0, 50, 0, 50]], atol=1e-5)
338338

339-
# Charge state: dims=(time, cluster), 5 entries (incl. final)
340-
# Cyclic: SOC wraps, starting with pre-charge from previous cycle
339+
# Charge state: 5 entries per cluster (incl. final), cyclic SOC wraps
341340
charge_state = fs.solution['Battery|charge_state']
342-
assert charge_state.dims == ('time', 'cluster')
343-
cs_c0 = charge_state.values[:5, 0]
344-
cs_c1 = charge_state.values[:5, 1]
341+
assert charge_state.dims == ('cluster', 'time')
342+
cs_c0 = charge_state.isel(cluster=0).values[:5]
343+
cs_c1 = charge_state.isel(cluster=1).values[:5]
345344
assert_allclose(cs_c0, [50, 50, 0, 50, 0], atol=1e-5)
346345
assert_allclose(cs_c1, [100, 100, 50, 100, 50], atol=1e-5)
347346

0 commit comments

Comments
 (0)