6060logger = 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+
63170class 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
0 commit comments