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.

Files changed (73) hide show
  1. docs/examples/00-Minimal Example.md +5 -0
  2. docs/examples/01-Basic Example.md +5 -0
  3. docs/examples/02-Complex Example.md +10 -0
  4. docs/examples/03-Calculation Modes.md +5 -0
  5. docs/examples/index.md +5 -0
  6. docs/faq/contribute.md +49 -0
  7. docs/faq/index.md +3 -0
  8. docs/images/architecture_flixOpt-pre2.0.0.png +0 -0
  9. docs/images/architecture_flixOpt.png +0 -0
  10. docs/images/flixopt-icon.svg +1 -0
  11. docs/javascripts/mathjax.js +18 -0
  12. docs/release-notes/_template.txt +32 -0
  13. docs/release-notes/index.md +7 -0
  14. docs/release-notes/v2.0.0.md +93 -0
  15. docs/release-notes/v2.0.1.md +12 -0
  16. docs/user-guide/Mathematical Notation/Bus.md +33 -0
  17. docs/user-guide/Mathematical Notation/Effects, Penalty & Objective.md +132 -0
  18. docs/user-guide/Mathematical Notation/Flow.md +26 -0
  19. docs/user-guide/Mathematical Notation/LinearConverter.md +21 -0
  20. docs/user-guide/Mathematical Notation/Piecewise.md +49 -0
  21. docs/user-guide/Mathematical Notation/Storage.md +44 -0
  22. docs/user-guide/Mathematical Notation/index.md +22 -0
  23. docs/user-guide/Mathematical Notation/others.md +3 -0
  24. docs/user-guide/index.md +124 -0
  25. {flixOpt → flixopt}/__init__.py +5 -2
  26. {flixOpt → flixopt}/aggregation.py +113 -140
  27. flixopt/calculation.py +455 -0
  28. {flixOpt → flixopt}/commons.py +7 -4
  29. flixopt/components.py +630 -0
  30. {flixOpt → flixopt}/config.py +9 -8
  31. {flixOpt → flixopt}/config.yaml +3 -3
  32. flixopt/core.py +970 -0
  33. flixopt/effects.py +386 -0
  34. flixopt/elements.py +534 -0
  35. flixopt/features.py +1042 -0
  36. flixopt/flow_system.py +409 -0
  37. flixopt/interface.py +265 -0
  38. flixopt/io.py +308 -0
  39. flixopt/linear_converters.py +331 -0
  40. flixopt/plotting.py +1340 -0
  41. flixopt/results.py +898 -0
  42. flixopt/solvers.py +77 -0
  43. flixopt/structure.py +630 -0
  44. flixopt/utils.py +62 -0
  45. flixopt-2.0.1.dist-info/METADATA +145 -0
  46. flixopt-2.0.1.dist-info/RECORD +57 -0
  47. {flixopt-1.0.12.dist-info → flixopt-2.0.1.dist-info}/WHEEL +1 -1
  48. flixopt-2.0.1.dist-info/top_level.txt +6 -0
  49. pics/architecture_flixOpt-pre2.0.0.png +0 -0
  50. pics/architecture_flixOpt.png +0 -0
  51. pics/flixopt-icon.svg +1 -0
  52. pics/pics.pptx +0 -0
  53. scripts/gen_ref_pages.py +54 -0
  54. site/release-notes/_template.txt +32 -0
  55. flixOpt/calculation.py +0 -629
  56. flixOpt/components.py +0 -614
  57. flixOpt/core.py +0 -182
  58. flixOpt/effects.py +0 -410
  59. flixOpt/elements.py +0 -489
  60. flixOpt/features.py +0 -942
  61. flixOpt/flow_system.py +0 -351
  62. flixOpt/interface.py +0 -203
  63. flixOpt/linear_converters.py +0 -325
  64. flixOpt/math_modeling.py +0 -1145
  65. flixOpt/plotting.py +0 -712
  66. flixOpt/results.py +0 -563
  67. flixOpt/solvers.py +0 -21
  68. flixOpt/structure.py +0 -733
  69. flixOpt/utils.py +0 -134
  70. flixopt-1.0.12.dist-info/METADATA +0 -174
  71. flixopt-1.0.12.dist-info/RECORD +0 -29
  72. flixopt-1.0.12.dist-info/top_level.txt +0 -3
  73. {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
+ )