Skip to content

Commit 92f5000

Browse files
committed
Let me give you a summary of what we've implemented:
Changes made: 1. flixopt/config.py: Added CONFIG.Legacy.solution_access option (default: False) 2. flixopt/flow_system.py: Added LegacySolutionWrapper class that translates legacy access patterns: - solution['costs'] → solution['effect|total'].sel(effect='costs') - solution['Src(heat)|flow_rate'] → solution['flow|rate'].sel(flow='Src(heat)') - solution['Src(heat)|invested'] → solution['flow|invested'].sel(flow='Src(heat)') - solution['Battery|size'] → solution['storage|size'].sel(storage='Battery') 3. tests/test_math/conftest.py: Enabled legacy mode for backward-compatible tests 4. flixopt/comparison.py: Fixed the coord extraction functions to use *args for DataArr
1 parent 4dfc86c commit 92f5000

3 files changed

Lines changed: 139 additions & 1 deletion

File tree

flixopt/config.py

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -577,6 +577,27 @@ class Plotting:
577577
default_qualitative_colorscale: str = _DEFAULTS['plotting']['default_qualitative_colorscale']
578578
default_line_shape: str = _DEFAULTS['plotting']['default_line_shape']
579579

580+
class Legacy:
581+
"""Legacy compatibility settings.
582+
583+
Attributes:
584+
solution_access: Enable backwards-compatible solution access patterns.
585+
When True, accessing `fs.solution['effect_name']` will automatically
586+
translate to `fs.solution['effect|total'].sel(effect='effect_name')`.
587+
Default: False (disabled).
588+
589+
Examples:
590+
```python
591+
# Enable legacy solution access
592+
CONFIG.Legacy.solution_access = True
593+
594+
# Now old-style access works
595+
fs.solution['costs'] # Returns effect total for 'costs'
596+
```
597+
"""
598+
599+
solution_access: bool = False
600+
580601
class Carriers:
581602
"""Default carrier definitions for common energy types.
582603

flixopt/flow_system.py

Lines changed: 115 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,113 @@
6060
logger = logging.getLogger('flixopt')
6161

6262

63+
class LegacySolutionWrapper:
64+
"""Wrapper for xr.Dataset that provides legacy solution access patterns.
65+
66+
When CONFIG.Legacy.solution_access is True, this wrapper intercepts
67+
__getitem__ calls to translate legacy access patterns like:
68+
fs.solution['costs'] -> fs.solution['effect|total'].sel(effect='costs')
69+
fs.solution['Src(heat)|flow_rate'] -> fs.solution['flow|rate'].sel(flow='Src(heat)')
70+
71+
All other operations are proxied directly to the underlying Dataset.
72+
"""
73+
74+
__slots__ = ('_dataset',)
75+
76+
# Mapping from old variable suffixes to new type|variable format
77+
# Format: old_suffix -> (dimension, new_variable_suffix)
78+
_LEGACY_VAR_MAP = {
79+
# Flow variables
80+
'flow_rate': ('flow', 'rate'),
81+
'size': ('flow', 'size'), # For flows: Comp(flow)|size
82+
'status': ('flow', 'status'),
83+
'invested': ('flow', 'invested'),
84+
}
85+
86+
# Storage-specific mappings (no parentheses in label, e.g., 'Battery|size')
87+
_LEGACY_STORAGE_VAR_MAP = {
88+
'size': ('storage', 'size'),
89+
'invested': ('storage', 'invested'),
90+
'charge_state': ('storage', 'charge'), # Old: charge_state -> New: charge
91+
}
92+
93+
def __init__(self, dataset: xr.Dataset) -> None:
94+
object.__setattr__(self, '_dataset', dataset)
95+
96+
def __getitem__(self, key):
97+
ds = object.__getattribute__(self, '_dataset')
98+
try:
99+
return ds[key]
100+
except KeyError as e:
101+
if not isinstance(key, str):
102+
raise e
103+
104+
# Try legacy effect access: solution['costs'] -> solution['effect|total'].sel(effect='costs')
105+
if 'effect' in ds.coords and key in ds.coords['effect'].values:
106+
warnings.warn(
107+
f"Legacy solution access: solution['{key}'] is deprecated. "
108+
f"Use solution['effect|total'].sel(effect='{key}') instead.",
109+
DeprecationWarning,
110+
stacklevel=2,
111+
)
112+
return ds['effect|total'].sel(effect=key)
113+
114+
# Try legacy flow/storage access: solution['Src(heat)|flow_rate'] -> solution['flow|rate'].sel(flow='Src(heat)')
115+
if '|' in key:
116+
parts = key.rsplit('|', 1)
117+
if len(parts) == 2:
118+
element_label, var_suffix = parts
119+
120+
# Try flow variables first (labels have parentheses like 'Src(heat)')
121+
if var_suffix in self._LEGACY_VAR_MAP:
122+
dim, var_name = self._LEGACY_VAR_MAP[var_suffix]
123+
new_key = f'{dim}|{var_name}'
124+
if new_key in ds and dim in ds.coords and element_label in ds.coords[dim].values:
125+
warnings.warn(
126+
f"Legacy solution access: solution['{key}'] is deprecated. "
127+
f"Use solution['{new_key}'].sel({dim}='{element_label}') instead.",
128+
DeprecationWarning,
129+
stacklevel=2,
130+
)
131+
return ds[new_key].sel({dim: element_label})
132+
133+
# Try storage variables (labels without parentheses like 'Battery')
134+
if var_suffix in self._LEGACY_STORAGE_VAR_MAP:
135+
dim, var_name = self._LEGACY_STORAGE_VAR_MAP[var_suffix]
136+
new_key = f'{dim}|{var_name}'
137+
if new_key in ds and dim in ds.coords and element_label in ds.coords[dim].values:
138+
warnings.warn(
139+
f"Legacy solution access: solution['{key}'] is deprecated. "
140+
f"Use solution['{new_key}'].sel({dim}='{element_label}') instead.",
141+
DeprecationWarning,
142+
stacklevel=2,
143+
)
144+
return ds[new_key].sel({dim: element_label})
145+
146+
raise e
147+
148+
def __getattr__(self, name):
149+
return getattr(object.__getattribute__(self, '_dataset'), name)
150+
151+
def __setattr__(self, name, value):
152+
if name == '_dataset':
153+
object.__setattr__(self, name, value)
154+
else:
155+
setattr(object.__getattribute__(self, '_dataset'), name, value)
156+
157+
def __repr__(self):
158+
return repr(object.__getattribute__(self, '_dataset'))
159+
160+
def __iter__(self):
161+
return iter(object.__getattribute__(self, '_dataset'))
162+
163+
def __len__(self):
164+
return len(object.__getattribute__(self, '_dataset'))
165+
166+
def __contains__(self, key):
167+
return key in object.__getattribute__(self, '_dataset')
168+
169+
63170
class FlowSystem(Interface, CompositeContainerMixin[Element]):
64171
"""
65172
A FlowSystem organizes the high level Elements (Components, Buses, Effects & Flows).
@@ -1064,7 +1171,7 @@ def _log_infeasibilities(self) -> None:
10641171
logger.error('Successfully extracted infeasibilities: \n%s', infeasibilities)
10651172

10661173
@property
1067-
def solution(self) -> xr.Dataset | None:
1174+
def solution(self) -> xr.Dataset | LegacySolutionWrapper | None:
10681175
"""
10691176
Access the optimization solution as an xarray Dataset.
10701177
@@ -1073,6 +1180,9 @@ def solution(self) -> xr.Dataset | None:
10731180
extra timestep (most variables except storage charge states) will contain
10741181
NaN values at the final timestep.
10751182
1183+
When ``CONFIG.Legacy.solution_access`` is True, returns a wrapper that
1184+
supports legacy access patterns like ``solution['effect_name']``.
1185+
10761186
Returns:
10771187
xr.Dataset: The solution dataset with all optimization variable results,
10781188
or None if the model hasn't been solved yet.
@@ -1081,6 +1191,10 @@ def solution(self) -> xr.Dataset | None:
10811191
>>> flow_system.optimize(solver)
10821192
>>> flow_system.solution.isel(time=slice(None, -1)) # Exclude trailing NaN (and final charge states)
10831193
"""
1194+
if self._solution is None:
1195+
return None
1196+
if CONFIG.Legacy.solution_access:
1197+
return LegacySolutionWrapper(self._solution)
10841198
return self._solution
10851199

10861200
@solution.setter

tests/test_math/conftest.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,9 @@
2525

2626
import flixopt as fx
2727

28+
# Enable legacy solution access for backward compatibility in tests
29+
fx.CONFIG.Legacy.solution_access = True
30+
2831
_SOLVER = fx.solvers.HighsSolver(mip_gap=0, time_limit_seconds=60, log_to_console=False)
2932

3033

0 commit comments

Comments
 (0)