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/elements.py ADDED
@@ -0,0 +1,534 @@
1
+ """
2
+ This module contains the basic elements of the flixopt framework.
3
+ """
4
+
5
+ import logging
6
+ import warnings
7
+ from typing import TYPE_CHECKING, Dict, List, Literal, Optional, Tuple, Union
8
+
9
+ import linopy
10
+ import numpy as np
11
+
12
+ from .config import CONFIG
13
+ from .core import NumericData, NumericDataTS, PlausibilityError, Scalar, TimeSeriesCollection
14
+ from .effects import EffectValuesUser
15
+ from .features import InvestmentModel, OnOffModel, PreventSimultaneousUsageModel
16
+ from .interface import InvestParameters, OnOffParameters
17
+ from .structure import Element, ElementModel, SystemModel, register_class_for_io
18
+
19
+ if TYPE_CHECKING:
20
+ from .flow_system import FlowSystem
21
+
22
+ logger = logging.getLogger('flixopt')
23
+
24
+
25
+ @register_class_for_io
26
+ class Component(Element):
27
+ """
28
+ A Component contains incoming and outgoing [`Flows`][flixopt.elements.Flow]. It defines how these Flows interact with each other.
29
+ The On or Off state of the Component is defined by all its Flows. Its on, if any of its FLows is On.
30
+ It's mathematically advisable to define the On/Off state in a FLow rather than a Component if possible,
31
+ as this introduces less binary variables to the Model
32
+ Constraints to the On/Off state are defined by the [`on_off_parameters`][flixopt.interface.OnOffParameters].
33
+ """
34
+
35
+ def __init__(
36
+ self,
37
+ label: str,
38
+ inputs: Optional[List['Flow']] = None,
39
+ outputs: Optional[List['Flow']] = None,
40
+ on_off_parameters: Optional[OnOffParameters] = None,
41
+ prevent_simultaneous_flows: Optional[List['Flow']] = None,
42
+ meta_data: Optional[Dict] = None,
43
+ ):
44
+ """
45
+ Args:
46
+ label: The label of the Element. Used to identify it in the FlowSystem
47
+ inputs: input flows.
48
+ outputs: output flows.
49
+ on_off_parameters: Information about on and off state of Component.
50
+ Component is On/Off, if all connected Flows are On/Off.
51
+ Induces On-Variable in all FLows!
52
+ See class OnOffParameters.
53
+ prevent_simultaneous_flows: Define a Group of Flows. Only one them can be on at a time.
54
+ Induces On-Variable in all Flows! If possible, use OnOffParameters in a single Flow instead.
55
+ meta_data: used to store more information about the Element. Is not used internally, but saved in the results. Only use python native types.
56
+ """
57
+ super().__init__(label, meta_data=meta_data)
58
+ self.inputs: List['Flow'] = inputs or []
59
+ self.outputs: List['Flow'] = outputs or []
60
+ self.on_off_parameters = on_off_parameters
61
+ self.prevent_simultaneous_flows: List['Flow'] = prevent_simultaneous_flows or []
62
+
63
+ self.flows: Dict[str, Flow] = {flow.label: flow for flow in self.inputs + self.outputs}
64
+
65
+ def create_model(self, model: SystemModel) -> 'ComponentModel':
66
+ self._plausibility_checks()
67
+ self.model = ComponentModel(model, self)
68
+ return self.model
69
+
70
+ def transform_data(self, flow_system: 'FlowSystem') -> None:
71
+ if self.on_off_parameters is not None:
72
+ self.on_off_parameters.transform_data(flow_system, self.label_full)
73
+
74
+ def infos(self, use_numpy=True, use_element_label: bool = False) -> Dict:
75
+ infos = super().infos(use_numpy, use_element_label)
76
+ infos['inputs'] = [flow.infos(use_numpy, use_element_label) for flow in self.inputs]
77
+ infos['outputs'] = [flow.infos(use_numpy, use_element_label) for flow in self.outputs]
78
+ return infos
79
+
80
+ def _plausibility_checks(self) -> None:
81
+ # TODO: Check for plausibility
82
+ pass
83
+
84
+
85
+ @register_class_for_io
86
+ class Bus(Element):
87
+ """
88
+ A Bus represents a nodal balance between the flow rates of its incoming and outgoing Flows.
89
+ """
90
+
91
+ def __init__(
92
+ self, label: str, excess_penalty_per_flow_hour: Optional[NumericDataTS] = 1e5, meta_data: Optional[Dict] = None
93
+ ):
94
+ """
95
+ Args:
96
+ label: The label of the Element. Used to identify it in the FlowSystem
97
+ excess_penalty_per_flow_hour: excess costs / penalty costs (bus balance compensation)
98
+ (none/ 0 -> no penalty). The default is 1e5.
99
+ (Take care: if you use a timeseries (no scalar), timeseries is aggregated if calculation_type = aggregated!)
100
+ meta_data: used to store more information about the Element. Is not used internally, but saved in the results. Only use python native types.
101
+ """
102
+ super().__init__(label, meta_data=meta_data)
103
+ self.excess_penalty_per_flow_hour = excess_penalty_per_flow_hour
104
+ self.inputs: List[Flow] = []
105
+ self.outputs: List[Flow] = []
106
+
107
+ def create_model(self, model: SystemModel) -> 'BusModel':
108
+ self._plausibility_checks()
109
+ self.model = BusModel(model, self)
110
+ return self.model
111
+
112
+ def transform_data(self, flow_system: 'FlowSystem'):
113
+ self.excess_penalty_per_flow_hour = flow_system.create_time_series(
114
+ f'{self.label_full}|excess_penalty_per_flow_hour', self.excess_penalty_per_flow_hour
115
+ )
116
+
117
+ def _plausibility_checks(self) -> None:
118
+ if self.excess_penalty_per_flow_hour is not None and (self.excess_penalty_per_flow_hour == 0).all():
119
+ logger.warning(f'In Bus {self.label}, the excess_penalty_per_flow_hour is 0. Use "None" or a value > 0.')
120
+
121
+ @property
122
+ def with_excess(self) -> bool:
123
+ return False if self.excess_penalty_per_flow_hour is None else True
124
+
125
+
126
+ @register_class_for_io
127
+ class Connection:
128
+ # input/output-dock (TODO:
129
+ # -> wäre cool, damit Komponenten auch auch ohne Knoten verbindbar
130
+ # input wären wie Flow,aber statt bus: connectsTo -> hier andere Connection oder aber Bus (dort keine Connection, weil nicht notwendig)
131
+
132
+ def __init__(self):
133
+ """
134
+ This class is not yet implemented!
135
+ """
136
+ raise NotImplementedError()
137
+
138
+
139
+ @register_class_for_io
140
+ class Flow(Element):
141
+ r"""
142
+ A **Flow** moves energy (or material) between a [Bus][flixopt.elements.Bus] and a [Component][flixopt.elements.Component] in a predefined direction.
143
+ The flow-rate is the main optimization variable of the **Flow**.
144
+ """
145
+
146
+ def __init__(
147
+ self,
148
+ label: str,
149
+ bus: str,
150
+ size: Union[Scalar, InvestParameters] = None,
151
+ fixed_relative_profile: Optional[NumericDataTS] = None,
152
+ relative_minimum: NumericDataTS = 0,
153
+ relative_maximum: NumericDataTS = 1,
154
+ effects_per_flow_hour: Optional[EffectValuesUser] = None,
155
+ on_off_parameters: Optional[OnOffParameters] = None,
156
+ flow_hours_total_max: Optional[Scalar] = None,
157
+ flow_hours_total_min: Optional[Scalar] = None,
158
+ load_factor_min: Optional[Scalar] = None,
159
+ load_factor_max: Optional[Scalar] = None,
160
+ previous_flow_rate: Optional[NumericData] = None,
161
+ meta_data: Optional[Dict] = None,
162
+ ):
163
+ r"""
164
+ Args:
165
+ label: The label of the FLow. Used to identify it in the FlowSystem. Its `full_label` consists of the label of the Component and the label of the Flow.
166
+ bus: blabel of the bus the flow is connected to.
167
+ size: size of the flow. If InvestmentParameters is used, size is optimized.
168
+ If size is None, a default value is used.
169
+ relative_minimum: min value is relative_minimum multiplied by size
170
+ relative_maximum: max value is relative_maximum multiplied by size. If size = max then relative_maximum=1
171
+ load_factor_min: minimal load factor general: avg Flow per nominalVal/investSize
172
+ (e.g. boiler, kW/kWh=h; solarthermal: kW/m²;
173
+ def: :math:`load\_factor:= sumFlowHours/ (nominal\_val \cdot \Delta t_{tot})`
174
+ load_factor_max: maximal load factor (see minimal load factor)
175
+ effects_per_flow_hour: operational costs, costs per flow-"work"
176
+ on_off_parameters: If present, flow can be "off", i.e. be zero (only relevant if relative_minimum > 0)
177
+ Therefore a binary var "on" is used. Further, several other restrictions and effects can be modeled
178
+ through this On/Off State (See OnOffParameters)
179
+ flow_hours_total_max: maximum flow-hours ("flow-work")
180
+ (if size is not const, maybe load_factor_max is the better choice!)
181
+ flow_hours_total_min: minimum flow-hours ("flow-work")
182
+ (if size is not predefined, maybe load_factor_min is the better choice!)
183
+ fixed_relative_profile: fixed relative values for flow (if given).
184
+ flow_rate(t) := fixed_relative_profile(t) * size(t)
185
+ With this value, the flow_rate is no optimization-variable anymore.
186
+ (relative_minimum and relative_maximum are ignored)
187
+ used for fixed load or supply profiles, i.g. heat demand, wind-power, solarthermal
188
+ If the load-profile is just an upper limit, use relative_maximum instead.
189
+ previous_flow_rate: previous flow rate of the component.
190
+ meta_data: used to store more information about the Element. Is not used internally, but saved in the results. Only use python native types.
191
+ """
192
+ super().__init__(label, meta_data=meta_data)
193
+ self.size = size or CONFIG.modeling.BIG # Default size
194
+ self.relative_minimum = relative_minimum
195
+ self.relative_maximum = relative_maximum
196
+ self.fixed_relative_profile = fixed_relative_profile
197
+
198
+ self.load_factor_min = load_factor_min
199
+ self.load_factor_max = load_factor_max
200
+ # self.positive_gradient = TimeSeries('positive_gradient', positive_gradient, self)
201
+ self.effects_per_flow_hour = effects_per_flow_hour if effects_per_flow_hour is not None else {}
202
+ self.flow_hours_total_max = flow_hours_total_max
203
+ self.flow_hours_total_min = flow_hours_total_min
204
+ self.on_off_parameters = on_off_parameters
205
+
206
+ self.previous_flow_rate = (
207
+ previous_flow_rate if not isinstance(previous_flow_rate, list) else np.array(previous_flow_rate)
208
+ )
209
+
210
+ self.component: str = 'UnknownComponent'
211
+ self.is_input_in_component: Optional[bool] = None
212
+ if isinstance(bus, Bus):
213
+ self.bus = bus.label_full
214
+ warnings.warn(
215
+ f'Bus {bus.label} is passed as a Bus object to {self.label}. This is deprecated and will be removed '
216
+ f'in the future. Add the Bus to the FlowSystem instead and pass its label to the Flow.',
217
+ UserWarning,
218
+ stacklevel=1,
219
+ )
220
+ self._bus_object = bus
221
+ else:
222
+ self.bus = bus
223
+ self._bus_object = None
224
+
225
+ def create_model(self, model: SystemModel) -> 'FlowModel':
226
+ self._plausibility_checks()
227
+ self.model = FlowModel(model, self)
228
+ return self.model
229
+
230
+ def transform_data(self, flow_system: 'FlowSystem'):
231
+ self.relative_minimum = flow_system.create_time_series(
232
+ f'{self.label_full}|relative_minimum', self.relative_minimum
233
+ )
234
+ self.relative_maximum = flow_system.create_time_series(
235
+ f'{self.label_full}|relative_maximum', self.relative_maximum
236
+ )
237
+ self.fixed_relative_profile = flow_system.create_time_series(
238
+ f'{self.label_full}|fixed_relative_profile', self.fixed_relative_profile
239
+ )
240
+ self.effects_per_flow_hour = flow_system.create_effect_time_series(
241
+ self.label_full, self.effects_per_flow_hour, 'per_flow_hour'
242
+ )
243
+ if self.on_off_parameters is not None:
244
+ self.on_off_parameters.transform_data(flow_system, self.label_full)
245
+ if isinstance(self.size, InvestParameters):
246
+ self.size.transform_data(flow_system)
247
+
248
+ def infos(self, use_numpy: bool = True, use_element_label: bool = False) -> Dict:
249
+ infos = super().infos(use_numpy, use_element_label)
250
+ infos['is_input_in_component'] = self.is_input_in_component
251
+ return infos
252
+
253
+ def to_dict(self) -> Dict:
254
+ data = super().to_dict()
255
+ if isinstance(data.get('previous_flow_rate'), np.ndarray):
256
+ data['previous_flow_rate'] = data['previous_flow_rate'].tolist()
257
+ return data
258
+
259
+ def _plausibility_checks(self) -> None:
260
+ # TODO: Incorporate into Variable? (Lower_bound can not be greater than upper bound
261
+ if np.any(self.relative_minimum > self.relative_maximum):
262
+ raise PlausibilityError(self.label_full + ': Take care, that relative_minimum <= relative_maximum!')
263
+
264
+ if (
265
+ self.size == CONFIG.modeling.BIG and self.fixed_relative_profile is not None
266
+ ): # Default Size --> Most likely by accident
267
+ logger.warning(
268
+ f'Flow "{self.label}" has no size assigned, but a "fixed_relative_profile". '
269
+ f'The default size is {CONFIG.modeling.BIG}. As "flow_rate = size * fixed_relative_profile", '
270
+ f'the resulting flow_rate will be very high. To fix this, assign a size to the Flow {self}.'
271
+ )
272
+
273
+ if self.fixed_relative_profile is not None and self.on_off_parameters is not None:
274
+ raise ValueError(
275
+ f'Flow {self.label} has both a fixed_relative_profile and an on_off_parameters. This is not supported. '
276
+ f'Use relative_minimum and relative_maximum instead, '
277
+ f'if you want to allow flows to be switched on and off.'
278
+ )
279
+
280
+ if (self.relative_minimum > 0).any() and self.on_off_parameters is None:
281
+ logger.warning(
282
+ f'Flow {self.label} has a relative_minimum of {self.relative_minimum.active_data} and no on_off_parameters. '
283
+ f'This prevents the flow_rate from switching off (flow_rate = 0). '
284
+ f'Consider using on_off_parameters to allow the flow to be switched on and off.'
285
+ )
286
+
287
+ @property
288
+ def label_full(self) -> str:
289
+ return f'{self.component}({self.label})'
290
+
291
+ @property
292
+ def size_is_fixed(self) -> bool:
293
+ # Wenn kein InvestParameters existiert --> True; Wenn Investparameter, den Wert davon nehmen
294
+ return False if (isinstance(self.size, InvestParameters) and self.size.fixed_size is None) else True
295
+
296
+ @property
297
+ def invest_is_optional(self) -> bool:
298
+ # Wenn kein InvestParameters existiert: # Investment ist nicht optional -> Keine Variable --> False
299
+ return False if (isinstance(self.size, InvestParameters) and not self.size.optional) else True
300
+
301
+
302
+ class FlowModel(ElementModel):
303
+ def __init__(self, model: SystemModel, element: Flow):
304
+ super().__init__(model, element)
305
+ self.element: Flow = element
306
+ self.flow_rate: Optional[linopy.Variable] = None
307
+ self.total_flow_hours: Optional[linopy.Variable] = None
308
+
309
+ self.on_off: Optional[OnOffModel] = None
310
+ self._investment: Optional[InvestmentModel] = None
311
+
312
+ def do_modeling(self):
313
+ # eq relative_minimum(t) * size <= flow_rate(t) <= relative_maximum(t) * size
314
+ self.flow_rate: linopy.Variable = self.add(
315
+ self._model.add_variables(
316
+ lower=self.absolute_flow_rate_bounds[0] if self.element.on_off_parameters is None else 0,
317
+ upper=self.absolute_flow_rate_bounds[1],
318
+ coords=self._model.coords,
319
+ name=f'{self.label_full}|flow_rate',
320
+ ),
321
+ 'flow_rate',
322
+ )
323
+
324
+ # OnOff
325
+ if self.element.on_off_parameters is not None:
326
+ self.on_off: OnOffModel = self.add(
327
+ OnOffModel(
328
+ model=self._model,
329
+ label_of_element=self.label_of_element,
330
+ on_off_parameters=self.element.on_off_parameters,
331
+ defining_variables=[self.flow_rate],
332
+ defining_bounds=[self.absolute_flow_rate_bounds],
333
+ previous_values=[self.element.previous_flow_rate],
334
+ ),
335
+ 'on_off',
336
+ )
337
+ self.on_off.do_modeling()
338
+
339
+ # Investment
340
+ if isinstance(self.element.size, InvestParameters):
341
+ self._investment: InvestmentModel = self.add(
342
+ InvestmentModel(
343
+ model=self._model,
344
+ label_of_element=self.label_of_element,
345
+ parameters=self.element.size,
346
+ defining_variable=self.flow_rate,
347
+ relative_bounds_of_defining_variable=self.relative_flow_rate_bounds,
348
+ on_variable=self.on_off.on if self.on_off is not None else None,
349
+ ),
350
+ 'investment',
351
+ )
352
+ self._investment.do_modeling()
353
+
354
+ self.total_flow_hours = self.add(
355
+ self._model.add_variables(
356
+ lower=self.element.flow_hours_total_min if self.element.flow_hours_total_min is not None else -np.inf,
357
+ upper=self.element.flow_hours_total_max if self.element.flow_hours_total_max is not None else np.inf,
358
+ coords=None,
359
+ name=f'{self.label_full}|total_flow_hours',
360
+ ),
361
+ 'total_flow_hours',
362
+ )
363
+
364
+ self.add(
365
+ self._model.add_constraints(
366
+ self.total_flow_hours == (self.flow_rate * self._model.hours_per_step).sum(),
367
+ name=f'{self.label_full}|total_flow_hours',
368
+ ),
369
+ 'total_flow_hours',
370
+ )
371
+
372
+ # Load factor
373
+ self._create_bounds_for_load_factor()
374
+
375
+ # Shares
376
+ self._create_shares()
377
+
378
+ def _create_shares(self):
379
+ # Arbeitskosten:
380
+ if self.element.effects_per_flow_hour != {}:
381
+ self._model.effects.add_share_to_effects(
382
+ name=self.label_full, # Use the full label of the element
383
+ expressions={
384
+ effect: self.flow_rate * self._model.hours_per_step * factor.active_data
385
+ for effect, factor in self.element.effects_per_flow_hour.items()
386
+ },
387
+ target='operation',
388
+ )
389
+
390
+ def _create_bounds_for_load_factor(self):
391
+ # TODO: Add Variable load_factor for better evaluation?
392
+
393
+ # eq: var_sumFlowHours <= size * dt_tot * load_factor_max
394
+ if self.element.load_factor_max is not None:
395
+ name_short = 'load_factor_max'
396
+ flow_hours_per_size_max = self._model.hours_per_step.sum() * self.element.load_factor_max
397
+ size = self.element.size if self._investment is None else self._investment.size
398
+
399
+ self.add(
400
+ self._model.add_constraints(
401
+ self.total_flow_hours <= size * flow_hours_per_size_max,
402
+ name=f'{self.label_full}|{name_short}',
403
+ ),
404
+ name_short,
405
+ )
406
+
407
+ # eq: size * sum(dt)* load_factor_min <= var_sumFlowHours
408
+ if self.element.load_factor_min is not None:
409
+ name_short = 'load_factor_min'
410
+ flow_hours_per_size_min = self._model.hours_per_step.sum() * self.element.load_factor_min
411
+ size = self.element.size if self._investment is None else self._investment.size
412
+
413
+ self.add(
414
+ self._model.add_constraints(
415
+ self.total_flow_hours >= size * flow_hours_per_size_min,
416
+ name=f'{self.label_full}|{name_short}',
417
+ ),
418
+ name_short,
419
+ )
420
+
421
+ @property
422
+ def absolute_flow_rate_bounds(self) -> Tuple[NumericData, NumericData]:
423
+ """Returns absolute flow rate bounds. Important for OnOffModel"""
424
+ relative_minimum, relative_maximum = self.relative_flow_rate_bounds
425
+ size = self.element.size
426
+ if not isinstance(size, InvestParameters):
427
+ return relative_minimum * size, relative_maximum * size
428
+ if size.fixed_size is not None:
429
+ return relative_minimum * size.fixed_size, relative_maximum * size.fixed_size
430
+ return relative_minimum * size.minimum_size, relative_maximum * size.maximum_size
431
+
432
+ @property
433
+ def relative_flow_rate_bounds(self) -> Tuple[NumericData, NumericData]:
434
+ """Returns relative flow rate bounds."""
435
+ fixed_profile = self.element.fixed_relative_profile
436
+ if fixed_profile is None:
437
+ return self.element.relative_minimum.active_data, self.element.relative_maximum.active_data
438
+ return fixed_profile.active_data, fixed_profile.active_data
439
+
440
+
441
+ class BusModel(ElementModel):
442
+ def __init__(self, model: SystemModel, element: Bus):
443
+ super().__init__(model, element)
444
+ self.element: Bus = element
445
+ self.excess_input: Optional[linopy.Variable] = None
446
+ self.excess_output: Optional[linopy.Variable] = None
447
+
448
+ def do_modeling(self) -> None:
449
+ # inputs == outputs
450
+ for flow in self.element.inputs + self.element.outputs:
451
+ self.add(flow.model.flow_rate, flow.label_full)
452
+ inputs = sum([flow.model.flow_rate for flow in self.element.inputs])
453
+ outputs = sum([flow.model.flow_rate for flow in self.element.outputs])
454
+ eq_bus_balance = self.add(self._model.add_constraints(inputs == outputs, name=f'{self.label_full}|balance'))
455
+
456
+ # Fehlerplus/-minus:
457
+ if self.element.with_excess:
458
+ excess_penalty = np.multiply(
459
+ self._model.hours_per_step, self.element.excess_penalty_per_flow_hour.active_data
460
+ )
461
+ self.excess_input = self.add(
462
+ self._model.add_variables(lower=0, coords=self._model.coords, name=f'{self.label_full}|excess_input'),
463
+ 'excess_input',
464
+ )
465
+ self.excess_output = self.add(
466
+ self._model.add_variables(lower=0, coords=self._model.coords, name=f'{self.label_full}|excess_output'),
467
+ 'excess_output',
468
+ )
469
+ eq_bus_balance.lhs -= -self.excess_input + self.excess_output
470
+
471
+ self._model.effects.add_share_to_penalty(self.label_of_element, (self.excess_input * excess_penalty).sum())
472
+ self._model.effects.add_share_to_penalty(self.label_of_element, (self.excess_output * excess_penalty).sum())
473
+
474
+ def results_structure(self):
475
+ inputs = [flow.model.flow_rate.name for flow in self.element.inputs]
476
+ outputs = [flow.model.flow_rate.name for flow in self.element.outputs]
477
+ if self.excess_input is not None:
478
+ inputs.append(self.excess_input.name)
479
+ if self.excess_output is not None:
480
+ outputs.append(self.excess_output.name)
481
+ return {**super().results_structure(), 'inputs': inputs, 'outputs': outputs}
482
+
483
+
484
+ class ComponentModel(ElementModel):
485
+ def __init__(self, model: SystemModel, element: Component):
486
+ super().__init__(model, element)
487
+ self.element: Component = element
488
+ self.on_off: Optional[OnOffModel] = None
489
+
490
+ def do_modeling(self):
491
+ """Initiates all FlowModels"""
492
+ all_flows = self.element.inputs + self.element.outputs
493
+ if self.element.on_off_parameters:
494
+ for flow in all_flows:
495
+ if flow.on_off_parameters is None:
496
+ flow.on_off_parameters = OnOffParameters()
497
+
498
+ if self.element.prevent_simultaneous_flows:
499
+ for flow in self.element.prevent_simultaneous_flows:
500
+ if flow.on_off_parameters is None:
501
+ flow.on_off_parameters = OnOffParameters()
502
+
503
+ for flow in all_flows:
504
+ self.add(flow.create_model(self._model), flow.label)
505
+
506
+ for sub_model in self.sub_models:
507
+ sub_model.do_modeling()
508
+
509
+ if self.element.on_off_parameters:
510
+ self.on_off = self.add(
511
+ OnOffModel(
512
+ self._model,
513
+ self.element.on_off_parameters,
514
+ self.label_of_element,
515
+ defining_variables=[flow.model.flow_rate for flow in all_flows],
516
+ defining_bounds=[flow.model.absolute_flow_rate_bounds for flow in all_flows],
517
+ previous_values=[flow.previous_flow_rate for flow in all_flows],
518
+ )
519
+ )
520
+
521
+ self.on_off.do_modeling()
522
+
523
+ if self.element.prevent_simultaneous_flows:
524
+ # Simultanious Useage --> Only One FLow is On at a time, but needs a Binary for every flow
525
+ on_variables = [flow.model.on_off.on for flow in self.element.prevent_simultaneous_flows]
526
+ simultaneous_use = self.add(PreventSimultaneousUsageModel(self._model, on_variables, self.label_full))
527
+ simultaneous_use.do_modeling()
528
+
529
+ def results_structure(self):
530
+ return {
531
+ **super().results_structure(),
532
+ 'inputs': [flow.model.flow_rate.name for flow in self.element.inputs],
533
+ 'outputs': [flow.model.flow_rate.name for flow in self.element.outputs],
534
+ }