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/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
|
+
}
|