Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions flixopt/io.py
Original file line number Diff line number Diff line change
Expand Up @@ -1839,6 +1839,13 @@ def _restore_clustering(
return

clustering_structure = json.loads(reference_structure['clustering'])
# Backward-compat: files written before flixopt 7.0 stored clustering.original_data
# and clustering._metrics as ':::original_data|...' / ':::metrics|...' references whose
# target arrays are no longer serialized (they only fed the removed plot.compare()).
# These keys are also not accepted by the current Clustering.__init__. Drop them so
# such files remain loadable.
clustering_structure.pop('_original_data_refs', None)
clustering_structure.pop('_metrics_refs', None)
clustering = fs_cls._resolve_reference_structure(clustering_structure, {})
flow_system.clustering = clustering

Expand Down
51 changes: 51 additions & 0 deletions tests/test_clustering/test_clustering_io.py
Original file line number Diff line number Diff line change
Expand Up @@ -710,3 +710,54 @@ def test_expand_after_load_and_optimize(self, system_with_periods_and_scenarios,
# Solution should be expanded
assert fs_expanded.solution is not None
assert 'source(out)|flow_rate' in fs_expanded.solution


class TestLegacyClusteringBackwardCompat:
"""Loading files written before flixopt 7.0, which stored clustering.original_data
and clustering._metrics as ':::original_data|...' / ':::metrics|...' references whose
target arrays are no longer serialized. See hotfix 7.2.2."""

def _inject_legacy_refs(self, ds: xr.Dataset) -> xr.Dataset:
"""Mutate a clustered dataset's clustering attrs to look like a pre-7.0 file."""
import json

clustering = json.loads(ds.attrs['clustering'])
clustering['_original_data_refs'] = [
':::original_data|demand(in)|fixed_relative_profile',
]
clustering['_metrics_refs'] = None
ds.attrs['clustering'] = json.dumps(clustering, ensure_ascii=False)
return ds

def test_legacy_refs_reproduce_failure_without_shim(self, simple_system_8_days):
"""Sanity check: the legacy references do point at arrays absent from the dataset."""
fs_clustered = simple_system_8_days.transform.cluster(n_clusters=2, cluster_duration='1D')
ds = self._inject_legacy_refs(fs_clustered.to_dataset(include_solution=False))
assert 'original_data|demand(in)|fixed_relative_profile' not in ds.variables

def test_load_legacy_clustered_dataset(self, simple_system_8_days):
"""A pre-7.0 clustered dataset (with dangling original_data/metrics refs) loads cleanly."""
from flixopt.clustering import Clustering

fs_clustered = simple_system_8_days.transform.cluster(n_clusters=2, cluster_duration='1D')
ds = self._inject_legacy_refs(fs_clustered.to_dataset(include_solution=False))

fs_restored = fx.FlowSystem.from_dataset(ds)

assert isinstance(fs_restored.clustering, Clustering)
assert fs_restored.clustering.n_clusters == 2

def test_load_legacy_clustered_netcdf(self, simple_system_8_days, tmp_path):
"""Same as above but through a real NetCDF file roundtrip."""
from flixopt.clustering import Clustering
from flixopt.io import save_dataset_to_netcdf

fs_clustered = simple_system_8_days.transform.cluster(n_clusters=2, cluster_duration='1D')
ds = self._inject_legacy_refs(fs_clustered.to_dataset(include_solution=False))

nc_path = tmp_path / 'legacy_clustered.nc'
save_dataset_to_netcdf(ds, nc_path)
fs_restored = fx.FlowSystem.from_netcdf(nc_path)

assert isinstance(fs_restored.clustering, Clustering)
assert fs_restored.clustering.n_clusters == 2
Loading