flixopt 1.0.12__py3-none-any.whl → 2.0.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of flixopt might be problematic. Click here for more details.
- docs/examples/00-Minimal Example.md +5 -0
- docs/examples/01-Basic Example.md +5 -0
- docs/examples/02-Complex Example.md +10 -0
- docs/examples/03-Calculation Modes.md +5 -0
- docs/examples/index.md +5 -0
- docs/faq/contribute.md +49 -0
- docs/faq/index.md +3 -0
- docs/images/architecture_flixOpt-pre2.0.0.png +0 -0
- docs/images/architecture_flixOpt.png +0 -0
- docs/images/flixopt-icon.svg +1 -0
- docs/javascripts/mathjax.js +18 -0
- docs/release-notes/_template.txt +32 -0
- docs/release-notes/index.md +7 -0
- docs/release-notes/v2.0.0.md +93 -0
- docs/release-notes/v2.0.1.md +12 -0
- docs/user-guide/Mathematical Notation/Bus.md +33 -0
- docs/user-guide/Mathematical Notation/Effects, Penalty & Objective.md +132 -0
- docs/user-guide/Mathematical Notation/Flow.md +26 -0
- docs/user-guide/Mathematical Notation/LinearConverter.md +21 -0
- docs/user-guide/Mathematical Notation/Piecewise.md +49 -0
- docs/user-guide/Mathematical Notation/Storage.md +44 -0
- docs/user-guide/Mathematical Notation/index.md +22 -0
- docs/user-guide/Mathematical Notation/others.md +3 -0
- docs/user-guide/index.md +124 -0
- {flixOpt → flixopt}/__init__.py +5 -2
- {flixOpt → flixopt}/aggregation.py +113 -140
- flixopt/calculation.py +455 -0
- {flixOpt → flixopt}/commons.py +7 -4
- flixopt/components.py +630 -0
- {flixOpt → flixopt}/config.py +9 -8
- {flixOpt → flixopt}/config.yaml +3 -3
- flixopt/core.py +970 -0
- flixopt/effects.py +386 -0
- flixopt/elements.py +534 -0
- flixopt/features.py +1042 -0
- flixopt/flow_system.py +409 -0
- flixopt/interface.py +265 -0
- flixopt/io.py +308 -0
- flixopt/linear_converters.py +331 -0
- flixopt/plotting.py +1340 -0
- flixopt/results.py +898 -0
- flixopt/solvers.py +77 -0
- flixopt/structure.py +630 -0
- flixopt/utils.py +62 -0
- flixopt-2.0.1.dist-info/METADATA +145 -0
- flixopt-2.0.1.dist-info/RECORD +57 -0
- {flixopt-1.0.12.dist-info → flixopt-2.0.1.dist-info}/WHEEL +1 -1
- flixopt-2.0.1.dist-info/top_level.txt +6 -0
- pics/architecture_flixOpt-pre2.0.0.png +0 -0
- pics/architecture_flixOpt.png +0 -0
- pics/flixopt-icon.svg +1 -0
- pics/pics.pptx +0 -0
- scripts/gen_ref_pages.py +54 -0
- site/release-notes/_template.txt +32 -0
- flixOpt/calculation.py +0 -629
- flixOpt/components.py +0 -614
- flixOpt/core.py +0 -182
- flixOpt/effects.py +0 -410
- flixOpt/elements.py +0 -489
- flixOpt/features.py +0 -942
- flixOpt/flow_system.py +0 -351
- flixOpt/interface.py +0 -203
- flixOpt/linear_converters.py +0 -325
- flixOpt/math_modeling.py +0 -1145
- flixOpt/plotting.py +0 -712
- flixOpt/results.py +0 -563
- flixOpt/solvers.py +0 -21
- flixOpt/structure.py +0 -733
- flixOpt/utils.py +0 -134
- flixopt-1.0.12.dist-info/METADATA +0 -174
- flixopt-1.0.12.dist-info/RECORD +0 -29
- flixopt-1.0.12.dist-info/top_level.txt +0 -3
- {flixopt-1.0.12.dist-info → flixopt-2.0.1.dist-info/licenses}/LICENSE +0 -0
flixopt/flow_system.py
ADDED
|
@@ -0,0 +1,409 @@
|
|
|
1
|
+
"""
|
|
2
|
+
This module contains the FlowSystem class, which is used to collect instances of many other classes by the end User.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
import logging
|
|
7
|
+
import pathlib
|
|
8
|
+
import warnings
|
|
9
|
+
from io import StringIO
|
|
10
|
+
from typing import TYPE_CHECKING, Dict, List, Literal, Optional, Tuple, Union
|
|
11
|
+
|
|
12
|
+
import numpy as np
|
|
13
|
+
import pandas as pd
|
|
14
|
+
import xarray as xr
|
|
15
|
+
from rich.console import Console
|
|
16
|
+
from rich.pretty import Pretty
|
|
17
|
+
|
|
18
|
+
from . import io as fx_io
|
|
19
|
+
from .core import NumericData, NumericDataTS, TimeSeries, TimeSeriesCollection, TimeSeriesData
|
|
20
|
+
from .effects import Effect, EffectCollection, EffectTimeSeries, EffectValuesDict, EffectValuesUser
|
|
21
|
+
from .elements import Bus, Component, Flow
|
|
22
|
+
from .structure import CLASS_REGISTRY, Element, SystemModel, get_compact_representation, get_str_representation
|
|
23
|
+
|
|
24
|
+
if TYPE_CHECKING:
|
|
25
|
+
import pyvis
|
|
26
|
+
|
|
27
|
+
logger = logging.getLogger('flixopt')
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class FlowSystem:
|
|
31
|
+
"""
|
|
32
|
+
A FlowSystem organizes the high level Elements (Components & Effects).
|
|
33
|
+
"""
|
|
34
|
+
|
|
35
|
+
def __init__(
|
|
36
|
+
self,
|
|
37
|
+
timesteps: pd.DatetimeIndex,
|
|
38
|
+
hours_of_last_timestep: Optional[float] = None,
|
|
39
|
+
hours_of_previous_timesteps: Optional[Union[int, float, np.ndarray]] = None,
|
|
40
|
+
):
|
|
41
|
+
"""
|
|
42
|
+
Args:
|
|
43
|
+
timesteps: The timesteps of the model.
|
|
44
|
+
hours_of_last_timestep: The duration of the last time step. Uses the last time interval if not specified
|
|
45
|
+
hours_of_previous_timesteps: The duration of previous timesteps.
|
|
46
|
+
If None, the first time increment of time_series is used.
|
|
47
|
+
This is needed to calculate previous durations (for example consecutive_on_hours).
|
|
48
|
+
If you use an array, take care that its long enough to cover all previous values!
|
|
49
|
+
"""
|
|
50
|
+
self.time_series_collection = TimeSeriesCollection(
|
|
51
|
+
timesteps=timesteps,
|
|
52
|
+
hours_of_last_timestep=hours_of_last_timestep,
|
|
53
|
+
hours_of_previous_timesteps=hours_of_previous_timesteps,
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
# defaults:
|
|
57
|
+
self.components: Dict[str, Component] = {}
|
|
58
|
+
self.buses: Dict[str, Bus] = {}
|
|
59
|
+
self.effects: EffectCollection = EffectCollection()
|
|
60
|
+
self.model: Optional[SystemModel] = None
|
|
61
|
+
|
|
62
|
+
self._connected = False
|
|
63
|
+
|
|
64
|
+
@classmethod
|
|
65
|
+
def from_dataset(cls, ds: xr.Dataset):
|
|
66
|
+
timesteps_extra = pd.DatetimeIndex(ds.attrs['timesteps_extra'], name='time')
|
|
67
|
+
hours_of_last_timestep = TimeSeriesCollection.calculate_hours_per_timestep(timesteps_extra).isel(time=-1).item()
|
|
68
|
+
|
|
69
|
+
flow_system = FlowSystem(
|
|
70
|
+
timesteps=timesteps_extra[:-1],
|
|
71
|
+
hours_of_last_timestep=hours_of_last_timestep,
|
|
72
|
+
hours_of_previous_timesteps=ds.attrs['hours_of_previous_timesteps'],
|
|
73
|
+
)
|
|
74
|
+
|
|
75
|
+
structure = fx_io.insert_dataarray({key: ds.attrs[key] for key in ['components', 'buses', 'effects']}, ds)
|
|
76
|
+
flow_system.add_elements(
|
|
77
|
+
*[Bus.from_dict(bus) for bus in structure['buses'].values()]
|
|
78
|
+
+ [Effect.from_dict(effect) for effect in structure['effects'].values()]
|
|
79
|
+
+ [CLASS_REGISTRY[comp['__class__']].from_dict(comp) for comp in structure['components'].values()]
|
|
80
|
+
)
|
|
81
|
+
return flow_system
|
|
82
|
+
|
|
83
|
+
@classmethod
|
|
84
|
+
def from_dict(cls, data: Dict) -> 'FlowSystem':
|
|
85
|
+
"""
|
|
86
|
+
Load a FlowSystem from a dictionary.
|
|
87
|
+
|
|
88
|
+
Args:
|
|
89
|
+
data: Dictionary containing the FlowSystem data.
|
|
90
|
+
"""
|
|
91
|
+
timesteps_extra = pd.DatetimeIndex(data['timesteps_extra'], name='time')
|
|
92
|
+
hours_of_last_timestep = TimeSeriesCollection.calculate_hours_per_timestep(timesteps_extra).isel(time=-1).item()
|
|
93
|
+
|
|
94
|
+
flow_system = FlowSystem(
|
|
95
|
+
timesteps=timesteps_extra[:-1],
|
|
96
|
+
hours_of_last_timestep=hours_of_last_timestep,
|
|
97
|
+
hours_of_previous_timesteps=data['hours_of_previous_timesteps'],
|
|
98
|
+
)
|
|
99
|
+
|
|
100
|
+
flow_system.add_elements(*[Bus.from_dict(bus) for bus in data['buses'].values()])
|
|
101
|
+
|
|
102
|
+
flow_system.add_elements(*[Effect.from_dict(effect) for effect in data['effects'].values()])
|
|
103
|
+
|
|
104
|
+
flow_system.add_elements(
|
|
105
|
+
*[CLASS_REGISTRY[comp['__class__']].from_dict(comp) for comp in data['components'].values()]
|
|
106
|
+
)
|
|
107
|
+
|
|
108
|
+
flow_system.transform_data()
|
|
109
|
+
|
|
110
|
+
return flow_system
|
|
111
|
+
|
|
112
|
+
@classmethod
|
|
113
|
+
def from_netcdf(cls, path: Union[str, pathlib.Path]):
|
|
114
|
+
"""
|
|
115
|
+
Load a FlowSystem from a netcdf file
|
|
116
|
+
"""
|
|
117
|
+
return cls.from_dataset(fx_io.load_dataset_from_netcdf(path))
|
|
118
|
+
|
|
119
|
+
def add_elements(self, *elements: Element) -> None:
|
|
120
|
+
"""
|
|
121
|
+
Add Components(Storages, Boilers, Heatpumps, ...), Buses or Effects to the FlowSystem
|
|
122
|
+
|
|
123
|
+
Args:
|
|
124
|
+
*elements: childs of Element like Boiler, HeatPump, Bus,...
|
|
125
|
+
modeling Elements
|
|
126
|
+
"""
|
|
127
|
+
if self._connected:
|
|
128
|
+
warnings.warn(
|
|
129
|
+
'You are adding elements to an already connected FlowSystem. This is not recommended (But it works).',
|
|
130
|
+
stacklevel=2,
|
|
131
|
+
)
|
|
132
|
+
self._connected = False
|
|
133
|
+
for new_element in list(elements):
|
|
134
|
+
if isinstance(new_element, Component):
|
|
135
|
+
self._add_components(new_element)
|
|
136
|
+
elif isinstance(new_element, Effect):
|
|
137
|
+
self._add_effects(new_element)
|
|
138
|
+
elif isinstance(new_element, Bus):
|
|
139
|
+
self._add_buses(new_element)
|
|
140
|
+
else:
|
|
141
|
+
raise TypeError(
|
|
142
|
+
f'Tried to add incompatible object to FlowSystem: {type(new_element)=}: {new_element=} '
|
|
143
|
+
)
|
|
144
|
+
|
|
145
|
+
def to_json(self, path: Union[str, pathlib.Path]):
|
|
146
|
+
"""
|
|
147
|
+
Saves the flow system to a json file.
|
|
148
|
+
This not meant to be reloaded and recreate the object,
|
|
149
|
+
but rather used to document or compare the flow_system to others.
|
|
150
|
+
|
|
151
|
+
Args:
|
|
152
|
+
path: The path to the json file.
|
|
153
|
+
"""
|
|
154
|
+
with open(path, 'w', encoding='utf-8') as f:
|
|
155
|
+
json.dump(self.as_dict('stats'), f, indent=4, ensure_ascii=False)
|
|
156
|
+
|
|
157
|
+
def as_dict(self, data_mode: Literal['data', 'name', 'stats'] = 'data') -> Dict:
|
|
158
|
+
"""Convert the object to a dictionary representation."""
|
|
159
|
+
data = {
|
|
160
|
+
'components': {
|
|
161
|
+
comp.label: comp.to_dict()
|
|
162
|
+
for comp in sorted(self.components.values(), key=lambda component: component.label.upper())
|
|
163
|
+
},
|
|
164
|
+
'buses': {
|
|
165
|
+
bus.label: bus.to_dict() for bus in sorted(self.buses.values(), key=lambda bus: bus.label.upper())
|
|
166
|
+
},
|
|
167
|
+
'effects': {
|
|
168
|
+
effect.label: effect.to_dict()
|
|
169
|
+
for effect in sorted(self.effects, key=lambda effect: effect.label.upper())
|
|
170
|
+
},
|
|
171
|
+
'timesteps_extra': [date.isoformat() for date in self.time_series_collection.timesteps_extra],
|
|
172
|
+
'hours_of_previous_timesteps': self.time_series_collection.hours_of_previous_timesteps,
|
|
173
|
+
}
|
|
174
|
+
if data_mode == 'data':
|
|
175
|
+
return fx_io.replace_timeseries(data, 'data')
|
|
176
|
+
elif data_mode == 'stats':
|
|
177
|
+
return fx_io.remove_none_and_empty(fx_io.replace_timeseries(data, data_mode))
|
|
178
|
+
return fx_io.replace_timeseries(data, data_mode)
|
|
179
|
+
|
|
180
|
+
def as_dataset(self, constants_in_dataset: bool = False) -> xr.Dataset:
|
|
181
|
+
"""
|
|
182
|
+
Convert the FlowSystem to a xarray Dataset.
|
|
183
|
+
|
|
184
|
+
Args:
|
|
185
|
+
constants_in_dataset: If True, constants are included as Dataset variables.
|
|
186
|
+
"""
|
|
187
|
+
ds = self.time_series_collection.to_dataset(include_constants=constants_in_dataset)
|
|
188
|
+
ds.attrs = self.as_dict(data_mode='name')
|
|
189
|
+
return ds
|
|
190
|
+
|
|
191
|
+
def to_netcdf(self, path: Union[str, pathlib.Path], compression: int = 0, constants_in_dataset: bool = True):
|
|
192
|
+
"""
|
|
193
|
+
Saves the FlowSystem to a netCDF file.
|
|
194
|
+
Args:
|
|
195
|
+
path: The path to the netCDF file.
|
|
196
|
+
compression: The compression level to use when saving the file.
|
|
197
|
+
constants_in_dataset: If True, constants are included as Dataset variables.
|
|
198
|
+
"""
|
|
199
|
+
ds = self.as_dataset(constants_in_dataset=constants_in_dataset)
|
|
200
|
+
fx_io.save_dataset_to_netcdf(ds, path, compression=compression)
|
|
201
|
+
logger.info(f'Saved FlowSystem to {path}')
|
|
202
|
+
|
|
203
|
+
def plot_network(
|
|
204
|
+
self,
|
|
205
|
+
path: Union[bool, str, pathlib.Path] = 'flow_system.html',
|
|
206
|
+
controls: Union[
|
|
207
|
+
bool,
|
|
208
|
+
List[
|
|
209
|
+
Literal['nodes', 'edges', 'layout', 'interaction', 'manipulation', 'physics', 'selection', 'renderer']
|
|
210
|
+
],
|
|
211
|
+
] = True,
|
|
212
|
+
show: bool = False,
|
|
213
|
+
) -> Optional['pyvis.network.Network']:
|
|
214
|
+
"""
|
|
215
|
+
Visualizes the network structure of a FlowSystem using PyVis, saving it as an interactive HTML file.
|
|
216
|
+
|
|
217
|
+
Args:
|
|
218
|
+
path: Path to save the HTML visualization.
|
|
219
|
+
- `False`: Visualization is created but not saved.
|
|
220
|
+
- `str` or `Path`: Specifies file path (default: 'flow_system.html').
|
|
221
|
+
controls: UI controls to add to the visualization.
|
|
222
|
+
- `True`: Enables all available controls.
|
|
223
|
+
- `List`: Specify controls, e.g., ['nodes', 'layout'].
|
|
224
|
+
- Options: 'nodes', 'edges', 'layout', 'interaction', 'manipulation', 'physics', 'selection', 'renderer'.
|
|
225
|
+
show: Whether to open the visualization in the web browser.
|
|
226
|
+
|
|
227
|
+
Returns:
|
|
228
|
+
- Optional[pyvis.network.Network]: The `Network` instance representing the visualization, or `None` if `pyvis` is not installed.
|
|
229
|
+
|
|
230
|
+
Examples:
|
|
231
|
+
>>> flow_system.plot_network()
|
|
232
|
+
>>> flow_system.plot_network(show=False)
|
|
233
|
+
>>> flow_system.plot_network(path='output/custom_network.html', controls=['nodes', 'layout'])
|
|
234
|
+
|
|
235
|
+
Notes:
|
|
236
|
+
- This function requires `pyvis`. If not installed, the function prints a warning and returns `None`.
|
|
237
|
+
- Nodes are styled based on type (e.g., circles for buses, boxes for components) and annotated with node information.
|
|
238
|
+
"""
|
|
239
|
+
from . import plotting
|
|
240
|
+
|
|
241
|
+
node_infos, edge_infos = self.network_infos()
|
|
242
|
+
return plotting.plot_network(node_infos, edge_infos, path, controls, show)
|
|
243
|
+
|
|
244
|
+
def network_infos(self) -> Tuple[Dict[str, Dict[str, str]], Dict[str, Dict[str, str]]]:
|
|
245
|
+
if not self._connected:
|
|
246
|
+
self._connect_network()
|
|
247
|
+
nodes = {
|
|
248
|
+
node.label_full: {
|
|
249
|
+
'label': node.label,
|
|
250
|
+
'class': 'Bus' if isinstance(node, Bus) else 'Component',
|
|
251
|
+
'infos': node.__str__(),
|
|
252
|
+
}
|
|
253
|
+
for node in list(self.components.values()) + list(self.buses.values())
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
edges = {
|
|
257
|
+
flow.label_full: {
|
|
258
|
+
'label': flow.label,
|
|
259
|
+
'start': flow.bus if flow.is_input_in_component else flow.component,
|
|
260
|
+
'end': flow.component if flow.is_input_in_component else flow.bus,
|
|
261
|
+
'infos': flow.__str__(),
|
|
262
|
+
}
|
|
263
|
+
for flow in self.flows.values()
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
return nodes, edges
|
|
267
|
+
|
|
268
|
+
def transform_data(self):
|
|
269
|
+
if not self._connected:
|
|
270
|
+
self._connect_network()
|
|
271
|
+
for element in self.all_elements.values():
|
|
272
|
+
element.transform_data(self)
|
|
273
|
+
|
|
274
|
+
def create_time_series(
|
|
275
|
+
self,
|
|
276
|
+
name: str,
|
|
277
|
+
data: Optional[Union[NumericData, TimeSeriesData, TimeSeries]],
|
|
278
|
+
needs_extra_timestep: bool = False,
|
|
279
|
+
) -> Optional[TimeSeries]:
|
|
280
|
+
"""
|
|
281
|
+
Tries to create a TimeSeries from NumericData Data and adds it to the time_series_collection
|
|
282
|
+
If the data already is a TimeSeries, nothing happens and the TimeSeries gets reset and returned
|
|
283
|
+
If the data is a TimeSeriesData, it is converted to a TimeSeries, and the aggregation weights are applied.
|
|
284
|
+
If the data is None, nothing happens.
|
|
285
|
+
"""
|
|
286
|
+
|
|
287
|
+
if data is None:
|
|
288
|
+
return None
|
|
289
|
+
elif isinstance(data, TimeSeries):
|
|
290
|
+
data.restore_data()
|
|
291
|
+
if data in self.time_series_collection:
|
|
292
|
+
return data
|
|
293
|
+
return self.time_series_collection.create_time_series(
|
|
294
|
+
data=data.active_data, name=name, needs_extra_timestep=needs_extra_timestep
|
|
295
|
+
)
|
|
296
|
+
return self.time_series_collection.create_time_series(
|
|
297
|
+
data=data, name=name, needs_extra_timestep=needs_extra_timestep
|
|
298
|
+
)
|
|
299
|
+
|
|
300
|
+
def create_effect_time_series(
|
|
301
|
+
self,
|
|
302
|
+
label_prefix: Optional[str],
|
|
303
|
+
effect_values: EffectValuesUser,
|
|
304
|
+
label_suffix: Optional[str] = None,
|
|
305
|
+
) -> Optional[EffectTimeSeries]:
|
|
306
|
+
"""
|
|
307
|
+
Transform EffectValues to EffectTimeSeries.
|
|
308
|
+
Creates a TimeSeries for each key in the nested_values dictionary, using the value as the data.
|
|
309
|
+
|
|
310
|
+
The resulting label of the TimeSeries is the label of the parent_element,
|
|
311
|
+
followed by the label of the Effect in the nested_values and the label_suffix.
|
|
312
|
+
If the key in the EffectValues is None, the alias 'Standard_Effect' is used
|
|
313
|
+
"""
|
|
314
|
+
effect_values: Optional[EffectValuesDict] = self.effects.create_effect_values_dict(effect_values)
|
|
315
|
+
if effect_values is None:
|
|
316
|
+
return None
|
|
317
|
+
|
|
318
|
+
return {
|
|
319
|
+
effect: self.create_time_series('|'.join(filter(None, [label_prefix, effect, label_suffix])), value)
|
|
320
|
+
for effect, value in effect_values.items()
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
def create_model(self) -> SystemModel:
|
|
324
|
+
if not self._connected:
|
|
325
|
+
raise RuntimeError('FlowSystem is not connected. Call FlowSystem.connect() first.')
|
|
326
|
+
self.model = SystemModel(self)
|
|
327
|
+
return self.model
|
|
328
|
+
|
|
329
|
+
def _check_if_element_is_unique(self, element: Element) -> None:
|
|
330
|
+
"""
|
|
331
|
+
checks if element or label of element already exists in list
|
|
332
|
+
|
|
333
|
+
Args:
|
|
334
|
+
element: new element to check
|
|
335
|
+
"""
|
|
336
|
+
if element in self.all_elements.values():
|
|
337
|
+
raise ValueError(f'Element {element.label} already added to FlowSystem!')
|
|
338
|
+
# check if name is already used:
|
|
339
|
+
if element.label_full in self.all_elements:
|
|
340
|
+
raise ValueError(f'Label of Element {element.label} already used in another element!')
|
|
341
|
+
|
|
342
|
+
def _add_effects(self, *args: Effect) -> None:
|
|
343
|
+
self.effects.add_effects(*args)
|
|
344
|
+
|
|
345
|
+
def _add_components(self, *components: Component) -> None:
|
|
346
|
+
for new_component in list(components):
|
|
347
|
+
logger.info(f'Registered new Component: {new_component.label}')
|
|
348
|
+
self._check_if_element_is_unique(new_component) # check if already exists:
|
|
349
|
+
self.components[new_component.label] = new_component # Add to existing components
|
|
350
|
+
|
|
351
|
+
def _add_buses(self, *buses: Bus):
|
|
352
|
+
for new_bus in list(buses):
|
|
353
|
+
logger.info(f'Registered new Bus: {new_bus.label}')
|
|
354
|
+
self._check_if_element_is_unique(new_bus) # check if already exists:
|
|
355
|
+
self.buses[new_bus.label] = new_bus # Add to existing components
|
|
356
|
+
|
|
357
|
+
def _connect_network(self):
|
|
358
|
+
"""Connects the network of components and buses. Can be rerun without changes if no elements were added"""
|
|
359
|
+
for component in self.components.values():
|
|
360
|
+
for flow in component.inputs + component.outputs:
|
|
361
|
+
flow.component = component.label_full
|
|
362
|
+
flow.is_input_in_component = True if flow in component.inputs else False
|
|
363
|
+
|
|
364
|
+
# Add Bus if not already added (deprecated)
|
|
365
|
+
if flow._bus_object is not None and flow._bus_object not in self.buses.values():
|
|
366
|
+
self._add_buses(flow._bus_object)
|
|
367
|
+
warnings.warn(
|
|
368
|
+
f'The Bus {flow._bus_object.label} was added to the FlowSystem from {flow.label_full}.'
|
|
369
|
+
f'This is deprecated and will be removed in the future. '
|
|
370
|
+
f'Please pass the Bus.label to the Flow and the Bus to the FlowSystem instead.',
|
|
371
|
+
UserWarning,
|
|
372
|
+
stacklevel=1,
|
|
373
|
+
)
|
|
374
|
+
|
|
375
|
+
# Connect Buses
|
|
376
|
+
bus = self.buses.get(flow.bus)
|
|
377
|
+
if bus is None:
|
|
378
|
+
raise KeyError(
|
|
379
|
+
f'Bus {flow.bus} not found in the FlowSystem, but used by "{flow.label_full}". '
|
|
380
|
+
f'Please add it first.'
|
|
381
|
+
)
|
|
382
|
+
if flow.is_input_in_component and flow not in bus.outputs:
|
|
383
|
+
bus.outputs.append(flow)
|
|
384
|
+
elif not flow.is_input_in_component and flow not in bus.inputs:
|
|
385
|
+
bus.inputs.append(flow)
|
|
386
|
+
logger.debug(
|
|
387
|
+
f'Connected {len(self.buses)} Buses and {len(self.components)} '
|
|
388
|
+
f'via {len(self.flows)} Flows inside the FlowSystem.'
|
|
389
|
+
)
|
|
390
|
+
self._connected = True
|
|
391
|
+
|
|
392
|
+
def __repr__(self):
|
|
393
|
+
return f'<{self.__class__.__name__} with {len(self.components)} components and {len(self.effects)} effects>'
|
|
394
|
+
|
|
395
|
+
def __str__(self):
|
|
396
|
+
with StringIO() as output_buffer:
|
|
397
|
+
console = Console(file=output_buffer, width=1000) # Adjust width as needed
|
|
398
|
+
console.print(Pretty(self.as_dict('stats'), expand_all=True, indent_guides=True))
|
|
399
|
+
value = output_buffer.getvalue()
|
|
400
|
+
return value
|
|
401
|
+
|
|
402
|
+
@property
|
|
403
|
+
def flows(self) -> Dict[str, Flow]:
|
|
404
|
+
set_of_flows = {flow for comp in self.components.values() for flow in comp.inputs + comp.outputs}
|
|
405
|
+
return {flow.label_full: flow for flow in set_of_flows}
|
|
406
|
+
|
|
407
|
+
@property
|
|
408
|
+
def all_elements(self) -> Dict[str, Element]:
|
|
409
|
+
return {**self.components, **self.effects.effects, **self.flows, **self.buses}
|
flixopt/interface.py
ADDED
|
@@ -0,0 +1,265 @@
|
|
|
1
|
+
"""
|
|
2
|
+
This module contains classes to collect Parameters for the Investment and OnOff decisions.
|
|
3
|
+
These are tightly connected to features.py
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
import logging
|
|
7
|
+
from typing import TYPE_CHECKING, Dict, Iterator, List, Optional, Union
|
|
8
|
+
|
|
9
|
+
from .config import CONFIG
|
|
10
|
+
from .core import NumericData, NumericDataTS, Scalar
|
|
11
|
+
from .structure import Interface, register_class_for_io
|
|
12
|
+
|
|
13
|
+
if TYPE_CHECKING: # for type checking and preventing circular imports
|
|
14
|
+
from .effects import EffectValuesUser, EffectValuesUserScalar
|
|
15
|
+
from .flow_system import FlowSystem
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
logger = logging.getLogger('flixopt')
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
@register_class_for_io
|
|
22
|
+
class Piece(Interface):
|
|
23
|
+
def __init__(self, start: NumericData, end: NumericData):
|
|
24
|
+
"""
|
|
25
|
+
Define a Piece, which is part of a Piecewise object.
|
|
26
|
+
|
|
27
|
+
Args:
|
|
28
|
+
start: The x-values of the piece.
|
|
29
|
+
end: The end of the piece.
|
|
30
|
+
"""
|
|
31
|
+
self.start = start
|
|
32
|
+
self.end = end
|
|
33
|
+
|
|
34
|
+
def transform_data(self, flow_system: 'FlowSystem', name_prefix: str):
|
|
35
|
+
self.start = flow_system.create_time_series(f'{name_prefix}|start', self.start)
|
|
36
|
+
self.end = flow_system.create_time_series(f'{name_prefix}|end', self.end)
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
@register_class_for_io
|
|
40
|
+
class Piecewise(Interface):
|
|
41
|
+
def __init__(self, pieces: List[Piece]):
|
|
42
|
+
"""
|
|
43
|
+
Define a Piecewise, consisting of a list of Pieces.
|
|
44
|
+
|
|
45
|
+
Args:
|
|
46
|
+
pieces: The pieces of the piecewise.
|
|
47
|
+
"""
|
|
48
|
+
self.pieces = pieces
|
|
49
|
+
|
|
50
|
+
def __len__(self):
|
|
51
|
+
return len(self.pieces)
|
|
52
|
+
|
|
53
|
+
def __getitem__(self, index) -> Piece:
|
|
54
|
+
return self.pieces[index] # Enables indexing like piecewise[i]
|
|
55
|
+
|
|
56
|
+
def __iter__(self) -> Iterator[Piece]:
|
|
57
|
+
return iter(self.pieces) # Enables iteration like for piece in piecewise: ...
|
|
58
|
+
|
|
59
|
+
def transform_data(self, flow_system: 'FlowSystem', name_prefix: str):
|
|
60
|
+
for i, piece in enumerate(self.pieces):
|
|
61
|
+
piece.transform_data(flow_system, f'{name_prefix}|Piece{i}')
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
@register_class_for_io
|
|
65
|
+
class PiecewiseConversion(Interface):
|
|
66
|
+
def __init__(self, piecewises: Dict[str, Piecewise]):
|
|
67
|
+
"""
|
|
68
|
+
Define a piecewise conversion between multiple Flows.
|
|
69
|
+
--> "gaps" can be expressed by a piece not starting at the end of the prior piece: [(1,3), (4,5)]
|
|
70
|
+
--> "points" can expressed as piece with same begin and end: [(3,3), (4,4)]
|
|
71
|
+
|
|
72
|
+
Args:
|
|
73
|
+
piecewises: Dict of Piecewises defining the conversion factors. flow labels as keys, piecewise as values
|
|
74
|
+
"""
|
|
75
|
+
self.piecewises = piecewises
|
|
76
|
+
|
|
77
|
+
def items(self):
|
|
78
|
+
return self.piecewises.items()
|
|
79
|
+
|
|
80
|
+
def transform_data(self, flow_system: 'FlowSystem', name_prefix: str):
|
|
81
|
+
for name, piecewise in self.piecewises.items():
|
|
82
|
+
piecewise.transform_data(flow_system, f'{name_prefix}|{name}')
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
@register_class_for_io
|
|
86
|
+
class PiecewiseEffects(Interface):
|
|
87
|
+
def __init__(self, piecewise_origin: Piecewise, piecewise_shares: Dict[str, Piecewise]):
|
|
88
|
+
"""
|
|
89
|
+
Define piecewise effects related to a variable.
|
|
90
|
+
|
|
91
|
+
Args:
|
|
92
|
+
piecewise_origin: Piecewise of the related variable
|
|
93
|
+
piecewise_shares: Piecewise defining the shares to different Effects
|
|
94
|
+
"""
|
|
95
|
+
self.piecewise_origin = piecewise_origin
|
|
96
|
+
self.piecewise_shares = piecewise_shares
|
|
97
|
+
|
|
98
|
+
def transform_data(self, flow_system: 'FlowSystem', name_prefix: str):
|
|
99
|
+
raise NotImplementedError('PiecewiseEffects is not yet implemented for non scalar shares')
|
|
100
|
+
# self.piecewise_origin.transform_data(flow_system, f'{name_prefix}|PiecewiseEffects|origin')
|
|
101
|
+
# for name, piecewise in self.piecewise_shares.items():
|
|
102
|
+
# piecewise.transform_data(flow_system, f'{name_prefix}|PiecewiseEffects|{name}')
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
@register_class_for_io
|
|
106
|
+
class InvestParameters(Interface):
|
|
107
|
+
"""
|
|
108
|
+
collects arguments for invest-stuff
|
|
109
|
+
"""
|
|
110
|
+
|
|
111
|
+
def __init__(
|
|
112
|
+
self,
|
|
113
|
+
fixed_size: Optional[Union[int, float]] = None,
|
|
114
|
+
minimum_size: Union[int, float] = 0, # TODO: Use EPSILON?
|
|
115
|
+
maximum_size: Optional[Union[int, float]] = None,
|
|
116
|
+
optional: bool = True, # Investition ist weglassbar
|
|
117
|
+
fix_effects: Optional['EffectValuesUserScalar'] = None,
|
|
118
|
+
specific_effects: Optional['EffectValuesUserScalar'] = None, # costs per Flow-Unit/Storage-Size/...
|
|
119
|
+
piecewise_effects: Optional[PiecewiseEffects] = None,
|
|
120
|
+
divest_effects: Optional['EffectValuesUserScalar'] = None,
|
|
121
|
+
):
|
|
122
|
+
"""
|
|
123
|
+
Args:
|
|
124
|
+
fix_effects: Fixed investment costs if invested. (Attention: Annualize costs to chosen period!)
|
|
125
|
+
divest_effects: Fixed divestment costs (if not invested, e.g., demolition costs or contractual penalty).
|
|
126
|
+
fixed_size: Determines if the investment size is fixed.
|
|
127
|
+
optional: If True, investment is not forced.
|
|
128
|
+
specific_effects: Specific costs, e.g., in €/kW_nominal or €/m²_nominal.
|
|
129
|
+
Example: {costs: 3, CO2: 0.3} with costs and CO2 representing an Object of class Effect
|
|
130
|
+
(Attention: Annualize costs to chosen period!)
|
|
131
|
+
piecewise_effects: Linear piecewise relation [invest_pieces, cost_pieces].
|
|
132
|
+
Example 1:
|
|
133
|
+
[ [5, 25, 25, 100], # size in kW
|
|
134
|
+
{costs: [50,250,250,800], # €
|
|
135
|
+
PE: [5, 25, 25, 100] # kWh_PrimaryEnergy
|
|
136
|
+
}
|
|
137
|
+
]
|
|
138
|
+
Example 2 (if only standard-effect):
|
|
139
|
+
[ [5, 25, 25, 100], # kW # size in kW
|
|
140
|
+
[50,250,250,800] # value for standart effect, typically €
|
|
141
|
+
] # €
|
|
142
|
+
(Attention: Annualize costs to chosen period!)
|
|
143
|
+
(Args 'specific_effects' and 'fix_effects' can be used in parallel to Investsizepieces)
|
|
144
|
+
minimum_size: Min nominal value (only if: size_is_fixed = False).
|
|
145
|
+
maximum_size: Max nominal value (only if: size_is_fixed = False).
|
|
146
|
+
"""
|
|
147
|
+
self.fix_effects: EffectValuesUser = fix_effects or {}
|
|
148
|
+
self.divest_effects: EffectValuesUser = divest_effects or {}
|
|
149
|
+
self.fixed_size = fixed_size
|
|
150
|
+
self.optional = optional
|
|
151
|
+
self.specific_effects: EffectValuesUser = specific_effects or {}
|
|
152
|
+
self.piecewise_effects = piecewise_effects
|
|
153
|
+
self._minimum_size = minimum_size
|
|
154
|
+
self._maximum_size = maximum_size or CONFIG.modeling.BIG # default maximum
|
|
155
|
+
|
|
156
|
+
def transform_data(self, flow_system: 'FlowSystem'):
|
|
157
|
+
self.fix_effects = flow_system.effects.create_effect_values_dict(self.fix_effects)
|
|
158
|
+
self.divest_effects = flow_system.effects.create_effect_values_dict(self.divest_effects)
|
|
159
|
+
self.specific_effects = flow_system.effects.create_effect_values_dict(self.specific_effects)
|
|
160
|
+
|
|
161
|
+
@property
|
|
162
|
+
def minimum_size(self):
|
|
163
|
+
return self.fixed_size or self._minimum_size
|
|
164
|
+
|
|
165
|
+
@property
|
|
166
|
+
def maximum_size(self):
|
|
167
|
+
return self.fixed_size or self._maximum_size
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
@register_class_for_io
|
|
171
|
+
class OnOffParameters(Interface):
|
|
172
|
+
def __init__(
|
|
173
|
+
self,
|
|
174
|
+
effects_per_switch_on: Optional['EffectValuesUser'] = None,
|
|
175
|
+
effects_per_running_hour: Optional['EffectValuesUser'] = None,
|
|
176
|
+
on_hours_total_min: Optional[int] = None,
|
|
177
|
+
on_hours_total_max: Optional[int] = None,
|
|
178
|
+
consecutive_on_hours_min: Optional[NumericData] = None,
|
|
179
|
+
consecutive_on_hours_max: Optional[NumericData] = None,
|
|
180
|
+
consecutive_off_hours_min: Optional[NumericData] = None,
|
|
181
|
+
consecutive_off_hours_max: Optional[NumericData] = None,
|
|
182
|
+
switch_on_total_max: Optional[int] = None,
|
|
183
|
+
force_switch_on: bool = False,
|
|
184
|
+
):
|
|
185
|
+
"""
|
|
186
|
+
Bundles information about the on and off state of an Element.
|
|
187
|
+
If no parameters are given, the default is to create a binary variable for the on state
|
|
188
|
+
without further constraints or effects and a variable for the total on hours.
|
|
189
|
+
|
|
190
|
+
Args:
|
|
191
|
+
effects_per_switch_on: cost of one switch from off (var_on=0) to on (var_on=1),
|
|
192
|
+
unit i.g. in Euro
|
|
193
|
+
effects_per_running_hour: costs for operating, i.g. in € per hour
|
|
194
|
+
on_hours_total_min: min. overall sum of operating hours.
|
|
195
|
+
on_hours_total_max: max. overall sum of operating hours.
|
|
196
|
+
consecutive_on_hours_min: min sum of operating hours in one piece
|
|
197
|
+
(last on-time period of timeseries is not checked and can be shorter)
|
|
198
|
+
consecutive_on_hours_max: max sum of operating hours in one piece
|
|
199
|
+
consecutive_off_hours_min: min sum of non-operating hours in one piece
|
|
200
|
+
(last off-time period of timeseries is not checked and can be shorter)
|
|
201
|
+
consecutive_off_hours_max: max sum of non-operating hours in one piece
|
|
202
|
+
switch_on_total_max: max nr of switchOn operations
|
|
203
|
+
force_switch_on: force creation of switch on variable, even if there is no switch_on_total_max
|
|
204
|
+
"""
|
|
205
|
+
self.effects_per_switch_on: EffectValuesUser = effects_per_switch_on or {}
|
|
206
|
+
self.effects_per_running_hour: EffectValuesUser = effects_per_running_hour or {}
|
|
207
|
+
self.on_hours_total_min: Scalar = on_hours_total_min
|
|
208
|
+
self.on_hours_total_max: Scalar = on_hours_total_max
|
|
209
|
+
self.consecutive_on_hours_min: NumericDataTS = consecutive_on_hours_min
|
|
210
|
+
self.consecutive_on_hours_max: NumericDataTS = consecutive_on_hours_max
|
|
211
|
+
self.consecutive_off_hours_min: NumericDataTS = consecutive_off_hours_min
|
|
212
|
+
self.consecutive_off_hours_max: NumericDataTS = consecutive_off_hours_max
|
|
213
|
+
self.switch_on_total_max: Scalar = switch_on_total_max
|
|
214
|
+
self.force_switch_on: bool = force_switch_on
|
|
215
|
+
|
|
216
|
+
def transform_data(self, flow_system: 'FlowSystem', name_prefix: str):
|
|
217
|
+
self.effects_per_switch_on = flow_system.create_effect_time_series(
|
|
218
|
+
name_prefix, self.effects_per_switch_on, 'per_switch_on'
|
|
219
|
+
)
|
|
220
|
+
self.effects_per_running_hour = flow_system.create_effect_time_series(
|
|
221
|
+
name_prefix, self.effects_per_running_hour, 'per_running_hour'
|
|
222
|
+
)
|
|
223
|
+
self.consecutive_on_hours_min = flow_system.create_time_series(
|
|
224
|
+
f'{name_prefix}|consecutive_on_hours_min', self.consecutive_on_hours_min
|
|
225
|
+
)
|
|
226
|
+
self.consecutive_on_hours_max = flow_system.create_time_series(
|
|
227
|
+
f'{name_prefix}|consecutive_on_hours_max', self.consecutive_on_hours_max
|
|
228
|
+
)
|
|
229
|
+
self.consecutive_off_hours_min = flow_system.create_time_series(
|
|
230
|
+
f'{name_prefix}|consecutive_off_hours_min', self.consecutive_off_hours_min
|
|
231
|
+
)
|
|
232
|
+
self.consecutive_off_hours_max = flow_system.create_time_series(
|
|
233
|
+
f'{name_prefix}|consecutive_off_hours_max', self.consecutive_off_hours_max
|
|
234
|
+
)
|
|
235
|
+
|
|
236
|
+
@property
|
|
237
|
+
def use_off(self) -> bool:
|
|
238
|
+
"""Determines wether the OFF Variable is needed or not"""
|
|
239
|
+
return self.use_consecutive_off_hours
|
|
240
|
+
|
|
241
|
+
@property
|
|
242
|
+
def use_consecutive_on_hours(self) -> bool:
|
|
243
|
+
"""Determines wether a Variable for consecutive off hours is needed or not"""
|
|
244
|
+
return any(param is not None for param in [self.consecutive_on_hours_min, self.consecutive_on_hours_max])
|
|
245
|
+
|
|
246
|
+
@property
|
|
247
|
+
def use_consecutive_off_hours(self) -> bool:
|
|
248
|
+
"""Determines wether a Variable for consecutive off hours is needed or not"""
|
|
249
|
+
return any(param is not None for param in [self.consecutive_off_hours_min, self.consecutive_off_hours_max])
|
|
250
|
+
|
|
251
|
+
@property
|
|
252
|
+
def use_switch_on(self) -> bool:
|
|
253
|
+
"""Determines wether a Variable for SWITCH-ON is needed or not"""
|
|
254
|
+
return (
|
|
255
|
+
any(
|
|
256
|
+
param not in (None, {})
|
|
257
|
+
for param in [
|
|
258
|
+
self.effects_per_switch_on,
|
|
259
|
+
self.switch_on_total_max,
|
|
260
|
+
self.on_hours_total_min,
|
|
261
|
+
self.on_hours_total_max,
|
|
262
|
+
]
|
|
263
|
+
)
|
|
264
|
+
or self.force_switch_on
|
|
265
|
+
)
|