flixopt 2.2.0b0__py3-none-any.whl → 3.0.0__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.
- flixopt/__init__.py +35 -1
- flixopt/aggregation.py +60 -81
- flixopt/calculation.py +381 -196
- flixopt/components.py +1022 -359
- flixopt/config.py +553 -191
- flixopt/core.py +475 -1315
- flixopt/effects.py +477 -214
- flixopt/elements.py +591 -344
- flixopt/features.py +403 -957
- flixopt/flow_system.py +781 -293
- flixopt/interface.py +1159 -189
- flixopt/io.py +50 -55
- flixopt/linear_converters.py +384 -92
- flixopt/modeling.py +759 -0
- flixopt/network_app.py +789 -0
- flixopt/plotting.py +273 -135
- flixopt/results.py +639 -383
- flixopt/solvers.py +25 -21
- flixopt/structure.py +928 -442
- flixopt/utils.py +34 -5
- flixopt-3.0.0.dist-info/METADATA +209 -0
- flixopt-3.0.0.dist-info/RECORD +26 -0
- {flixopt-2.2.0b0.dist-info → flixopt-3.0.0.dist-info}/WHEEL +1 -1
- flixopt-3.0.0.dist-info/top_level.txt +1 -0
- docs/examples/00-Minimal Example.md +0 -5
- docs/examples/01-Basic Example.md +0 -5
- docs/examples/02-Complex Example.md +0 -10
- docs/examples/03-Calculation Modes.md +0 -5
- docs/examples/index.md +0 -5
- docs/faq/contribute.md +0 -49
- docs/faq/index.md +0 -3
- docs/images/architecture_flixOpt-pre2.0.0.png +0 -0
- docs/images/architecture_flixOpt.png +0 -0
- docs/images/flixopt-icon.svg +0 -1
- docs/javascripts/mathjax.js +0 -18
- docs/release-notes/_template.txt +0 -32
- docs/release-notes/index.md +0 -7
- docs/release-notes/v2.0.0.md +0 -93
- docs/release-notes/v2.0.1.md +0 -12
- docs/release-notes/v2.1.0.md +0 -31
- docs/release-notes/v2.2.0.md +0 -55
- docs/user-guide/Mathematical Notation/Bus.md +0 -33
- docs/user-guide/Mathematical Notation/Effects, Penalty & Objective.md +0 -132
- docs/user-guide/Mathematical Notation/Flow.md +0 -26
- docs/user-guide/Mathematical Notation/Investment.md +0 -115
- docs/user-guide/Mathematical Notation/LinearConverter.md +0 -21
- docs/user-guide/Mathematical Notation/Piecewise.md +0 -49
- docs/user-guide/Mathematical Notation/Storage.md +0 -44
- docs/user-guide/Mathematical Notation/index.md +0 -22
- docs/user-guide/Mathematical Notation/others.md +0 -3
- docs/user-guide/index.md +0 -124
- flixopt/config.yaml +0 -10
- flixopt-2.2.0b0.dist-info/METADATA +0 -146
- flixopt-2.2.0b0.dist-info/RECORD +0 -59
- flixopt-2.2.0b0.dist-info/top_level.txt +0 -5
- pics/architecture_flixOpt-pre2.0.0.png +0 -0
- pics/architecture_flixOpt.png +0 -0
- pics/flixOpt_plotting.jpg +0 -0
- pics/flixopt-icon.svg +0 -1
- pics/pics.pptx +0 -0
- scripts/gen_ref_pages.py +0 -54
- tests/ressources/Zeitreihen2020.csv +0 -35137
- {flixopt-2.2.0b0.dist-info → flixopt-3.0.0.dist-info}/licenses/LICENSE +0 -0
flixopt/components.py
CHANGED
|
@@ -2,20 +2,25 @@
|
|
|
2
2
|
This module contains the basic components of the flixopt framework.
|
|
3
3
|
"""
|
|
4
4
|
|
|
5
|
+
from __future__ import annotations
|
|
6
|
+
|
|
5
7
|
import logging
|
|
6
|
-
|
|
8
|
+
import warnings
|
|
9
|
+
from typing import TYPE_CHECKING, Literal
|
|
7
10
|
|
|
8
|
-
import linopy
|
|
9
11
|
import numpy as np
|
|
12
|
+
import xarray as xr
|
|
10
13
|
|
|
11
|
-
from . import
|
|
12
|
-
from .core import PlausibilityError, Scalar, ScenarioData, TimeSeries, TimestepData
|
|
14
|
+
from .core import PeriodicDataUser, PlausibilityError, TemporalData, TemporalDataUser
|
|
13
15
|
from .elements import Component, ComponentModel, Flow
|
|
14
|
-
from .features import InvestmentModel,
|
|
16
|
+
from .features import InvestmentModel, PiecewiseModel
|
|
15
17
|
from .interface import InvestParameters, OnOffParameters, PiecewiseConversion
|
|
16
|
-
from .
|
|
18
|
+
from .modeling import BoundingPatterns
|
|
19
|
+
from .structure import FlowSystemModel, register_class_for_io
|
|
17
20
|
|
|
18
21
|
if TYPE_CHECKING:
|
|
22
|
+
import linopy
|
|
23
|
+
|
|
19
24
|
from .flow_system import FlowSystem
|
|
20
25
|
|
|
21
26
|
logger = logging.getLogger('flixopt')
|
|
@@ -24,43 +29,155 @@ logger = logging.getLogger('flixopt')
|
|
|
24
29
|
@register_class_for_io
|
|
25
30
|
class LinearConverter(Component):
|
|
26
31
|
"""
|
|
27
|
-
Converts input-Flows into output-Flows via linear conversion factors
|
|
32
|
+
Converts input-Flows into output-Flows via linear conversion factors.
|
|
33
|
+
|
|
34
|
+
LinearConverter models equipment that transforms one or more input flows into one or
|
|
35
|
+
more output flows through linear relationships. This includes heat exchangers,
|
|
36
|
+
electrical converters, chemical reactors, and other equipment where the
|
|
37
|
+
relationship between inputs and outputs can be expressed as linear equations.
|
|
38
|
+
|
|
39
|
+
The component supports two modeling approaches: simple conversion factors for
|
|
40
|
+
straightforward linear relationships, or piecewise conversion for complex non-linear
|
|
41
|
+
behavior approximated through piecewise linear segments.
|
|
42
|
+
|
|
43
|
+
Mathematical Formulation:
|
|
44
|
+
See the complete mathematical model in the documentation:
|
|
45
|
+
[LinearConverter](../user-guide/mathematical-notation/elements/LinearConverter.md)
|
|
46
|
+
|
|
47
|
+
Args:
|
|
48
|
+
label: The label of the Element. Used to identify it in the FlowSystem.
|
|
49
|
+
inputs: list of input Flows that feed into the converter.
|
|
50
|
+
outputs: list of output Flows that are produced by the converter.
|
|
51
|
+
on_off_parameters: Information about on and off state of LinearConverter.
|
|
52
|
+
Component is On/Off if all connected Flows are On/Off. This induces an
|
|
53
|
+
On-Variable (binary) in all Flows! If possible, use OnOffParameters in a
|
|
54
|
+
single Flow instead to keep the number of binary variables low.
|
|
55
|
+
conversion_factors: Linear relationships between flows expressed as a list of
|
|
56
|
+
dictionaries. Each dictionary maps flow labels to their coefficients in one
|
|
57
|
+
linear equation. The number of conversion factors must be less than the total
|
|
58
|
+
number of flows to ensure degrees of freedom > 0. Either 'conversion_factors'
|
|
59
|
+
OR 'piecewise_conversion' can be used, but not both.
|
|
60
|
+
For examples also look into the linear_converters.py file.
|
|
61
|
+
piecewise_conversion: Define piecewise linear relationships between flow rates
|
|
62
|
+
of different flows. Enables modeling of non-linear conversion behavior through
|
|
63
|
+
linear approximation. Either 'conversion_factors' or 'piecewise_conversion'
|
|
64
|
+
can be used, but not both.
|
|
65
|
+
meta_data: Used to store additional information about the Element. Not used
|
|
66
|
+
internally, but saved in results. Only use Python native types.
|
|
67
|
+
|
|
68
|
+
Examples:
|
|
69
|
+
Simple 1:1 heat exchanger with 95% efficiency:
|
|
70
|
+
|
|
71
|
+
```python
|
|
72
|
+
heat_exchanger = LinearConverter(
|
|
73
|
+
label='primary_hx',
|
|
74
|
+
inputs=[hot_water_in],
|
|
75
|
+
outputs=[hot_water_out],
|
|
76
|
+
conversion_factors=[{'hot_water_in': 0.95, 'hot_water_out': 1}],
|
|
77
|
+
)
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
Multi-input heat pump with COP=3:
|
|
81
|
+
|
|
82
|
+
```python
|
|
83
|
+
heat_pump = LinearConverter(
|
|
84
|
+
label='air_source_hp',
|
|
85
|
+
inputs=[electricity_in],
|
|
86
|
+
outputs=[heat_output],
|
|
87
|
+
conversion_factors=[{'electricity_in': 3, 'heat_output': 1}],
|
|
88
|
+
)
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
Combined heat and power (CHP) unit with multiple outputs:
|
|
92
|
+
|
|
93
|
+
```python
|
|
94
|
+
chp_unit = LinearConverter(
|
|
95
|
+
label='gas_chp',
|
|
96
|
+
inputs=[natural_gas],
|
|
97
|
+
outputs=[electricity_out, heat_out],
|
|
98
|
+
conversion_factors=[
|
|
99
|
+
{'natural_gas': 0.35, 'electricity_out': 1},
|
|
100
|
+
{'natural_gas': 0.45, 'heat_out': 1},
|
|
101
|
+
],
|
|
102
|
+
)
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
Electrolyzer with multiple conversion relationships:
|
|
106
|
+
|
|
107
|
+
```python
|
|
108
|
+
electrolyzer = LinearConverter(
|
|
109
|
+
label='pem_electrolyzer',
|
|
110
|
+
inputs=[electricity_in, water_in],
|
|
111
|
+
outputs=[hydrogen_out, oxygen_out],
|
|
112
|
+
conversion_factors=[
|
|
113
|
+
{'electricity_in': 1, 'hydrogen_out': 50}, # 50 kWh/kg H2
|
|
114
|
+
{'water_in': 1, 'hydrogen_out': 9}, # 9 kg H2O/kg H2
|
|
115
|
+
{'hydrogen_out': 8, 'oxygen_out': 1}, # Mass balance
|
|
116
|
+
],
|
|
117
|
+
)
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
Complex converter with piecewise efficiency:
|
|
121
|
+
|
|
122
|
+
```python
|
|
123
|
+
variable_efficiency_converter = LinearConverter(
|
|
124
|
+
label='variable_converter',
|
|
125
|
+
inputs=[fuel_in],
|
|
126
|
+
outputs=[power_out],
|
|
127
|
+
piecewise_conversion=PiecewiseConversion(
|
|
128
|
+
{
|
|
129
|
+
'fuel_in': Piecewise(
|
|
130
|
+
[
|
|
131
|
+
Piece(0, 10), # Low load operation
|
|
132
|
+
Piece(10, 25), # High load operation
|
|
133
|
+
]
|
|
134
|
+
),
|
|
135
|
+
'power_out': Piecewise(
|
|
136
|
+
[
|
|
137
|
+
Piece(0, 3.5), # Lower efficiency at part load
|
|
138
|
+
Piece(3.5, 10), # Higher efficiency at full load
|
|
139
|
+
]
|
|
140
|
+
),
|
|
141
|
+
}
|
|
142
|
+
),
|
|
143
|
+
)
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
Note:
|
|
147
|
+
Conversion factors define linear relationships where the sum of (coefficient × flow_rate)
|
|
148
|
+
equals zero for each equation: factor1×flow1 + factor2×flow2 + ... = 0
|
|
149
|
+
Conversion factors define linear relationships:
|
|
150
|
+
`{flow1: a1, flow2: a2, ...}` yields `a1×flow_rate1 + a2×flow_rate2 + ... = 0`.
|
|
151
|
+
Note: The input format may be unintuitive. For example,
|
|
152
|
+
`{"electricity": 1, "H2": 50}` implies `1×electricity = 50×H2`,
|
|
153
|
+
i.e., 50 units of electricity produce 1 unit of H2.
|
|
154
|
+
|
|
155
|
+
The system must have fewer conversion factors than total flows (degrees of freedom > 0)
|
|
156
|
+
to avoid over-constraining the problem. For n total flows, use at most n-1 conversion factors.
|
|
157
|
+
|
|
158
|
+
When using piecewise_conversion, the converter operates on one piece at a time,
|
|
159
|
+
with binary variables determining which piece is active.
|
|
28
160
|
|
|
29
161
|
"""
|
|
30
162
|
|
|
31
163
|
def __init__(
|
|
32
164
|
self,
|
|
33
165
|
label: str,
|
|
34
|
-
inputs:
|
|
35
|
-
outputs:
|
|
36
|
-
on_off_parameters: OnOffParameters = None,
|
|
37
|
-
conversion_factors:
|
|
38
|
-
piecewise_conversion:
|
|
39
|
-
meta_data:
|
|
166
|
+
inputs: list[Flow],
|
|
167
|
+
outputs: list[Flow],
|
|
168
|
+
on_off_parameters: OnOffParameters | None = None,
|
|
169
|
+
conversion_factors: list[dict[str, TemporalDataUser]] | None = None,
|
|
170
|
+
piecewise_conversion: PiecewiseConversion | None = None,
|
|
171
|
+
meta_data: dict | None = None,
|
|
40
172
|
):
|
|
41
|
-
"""
|
|
42
|
-
Args:
|
|
43
|
-
label: The label of the Element. Used to identify it in the FlowSystem
|
|
44
|
-
inputs: The input Flows
|
|
45
|
-
outputs: The output Flows
|
|
46
|
-
on_off_parameters: Information about on and off state of LinearConverter.
|
|
47
|
-
Component is On/Off, if all connected Flows are On/Off. This induces an On-Variable (binary) in all Flows!
|
|
48
|
-
If possible, use OnOffParameters in a single Flow instead to keep the number of binary variables low.
|
|
49
|
-
See class OnOffParameters.
|
|
50
|
-
conversion_factors: linear relation between flows.
|
|
51
|
-
Either 'conversion_factors' or 'piecewise_conversion' can be used!
|
|
52
|
-
piecewise_conversion: Define a piecewise linear relation between flow rates of different flows.
|
|
53
|
-
Either 'conversion_factors' or 'piecewise_conversion' can be used!
|
|
54
|
-
meta_data: used to store more information about the Element. Is not used internally, but saved in the results. Only use python native types.
|
|
55
|
-
"""
|
|
56
173
|
super().__init__(label, inputs, outputs, on_off_parameters, meta_data=meta_data)
|
|
57
174
|
self.conversion_factors = conversion_factors or []
|
|
58
175
|
self.piecewise_conversion = piecewise_conversion
|
|
59
176
|
|
|
60
|
-
def create_model(self, model:
|
|
177
|
+
def create_model(self, model: FlowSystemModel) -> LinearConverterModel:
|
|
61
178
|
self._plausibility_checks()
|
|
62
|
-
self.
|
|
63
|
-
return self.
|
|
179
|
+
self.submodel = LinearConverterModel(model, self)
|
|
180
|
+
return self.submodel
|
|
64
181
|
|
|
65
182
|
def _plausibility_checks(self) -> None:
|
|
66
183
|
super()._plausibility_checks()
|
|
@@ -87,28 +204,31 @@ class LinearConverter(Component):
|
|
|
87
204
|
for flow in self.flows.values():
|
|
88
205
|
if isinstance(flow.size, InvestParameters) and flow.size.fixed_size is None:
|
|
89
206
|
logger.warning(
|
|
90
|
-
f'Using a
|
|
91
|
-
f'
|
|
207
|
+
f'Using a Flow with variable size (InvestParameters without fixed_size) '
|
|
208
|
+
f'and a piecewise_conversion in {self.label_full} is uncommon. Please verify intent '
|
|
209
|
+
f'({flow.label_full}).'
|
|
92
210
|
)
|
|
93
211
|
|
|
94
|
-
def transform_data(self, flow_system:
|
|
95
|
-
|
|
212
|
+
def transform_data(self, flow_system: FlowSystem, name_prefix: str = '') -> None:
|
|
213
|
+
prefix = '|'.join(filter(None, [name_prefix, self.label_full]))
|
|
214
|
+
super().transform_data(flow_system, prefix)
|
|
96
215
|
if self.conversion_factors:
|
|
97
216
|
self.conversion_factors = self._transform_conversion_factors(flow_system)
|
|
98
217
|
if self.piecewise_conversion:
|
|
99
218
|
self.piecewise_conversion.has_time_dim = True
|
|
100
|
-
self.piecewise_conversion.transform_data(flow_system, f'{
|
|
219
|
+
self.piecewise_conversion.transform_data(flow_system, f'{prefix}|PiecewiseConversion')
|
|
101
220
|
|
|
102
|
-
def _transform_conversion_factors(self, flow_system:
|
|
103
|
-
"""
|
|
221
|
+
def _transform_conversion_factors(self, flow_system: FlowSystem) -> list[dict[str, xr.DataArray]]:
|
|
222
|
+
"""Converts all conversion factors to internal datatypes"""
|
|
104
223
|
list_of_conversion_factors = []
|
|
105
224
|
for idx, conversion_factor in enumerate(self.conversion_factors):
|
|
106
225
|
transformed_dict = {}
|
|
107
226
|
for flow, values in conversion_factor.items():
|
|
108
227
|
# TODO: Might be better to use the label of the component instead of the flow
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
228
|
+
ts = flow_system.fit_to_model_coords(f'{self.flows[flow].label_full}|conversion_factor{idx}', values)
|
|
229
|
+
if ts is None:
|
|
230
|
+
raise PlausibilityError(f'{self.label_full}: conversion factor for flow "{flow}" must not be None')
|
|
231
|
+
transformed_dict[flow] = ts
|
|
112
232
|
list_of_conversion_factors.append(transformed_dict)
|
|
113
233
|
return list_of_conversion_factors
|
|
114
234
|
|
|
@@ -120,7 +240,140 @@ class LinearConverter(Component):
|
|
|
120
240
|
@register_class_for_io
|
|
121
241
|
class Storage(Component):
|
|
122
242
|
"""
|
|
123
|
-
|
|
243
|
+
A Storage models the temporary storage and release of energy or material.
|
|
244
|
+
|
|
245
|
+
Storages have one incoming and one outgoing Flow, each with configurable efficiency
|
|
246
|
+
factors. They maintain a charge state variable that represents the stored amount,
|
|
247
|
+
bounded by capacity limits and evolving over time based on charging, discharging,
|
|
248
|
+
and self-discharge losses.
|
|
249
|
+
|
|
250
|
+
The storage model handles complex temporal dynamics including initial conditions,
|
|
251
|
+
final state constraints, and time-varying parameters. It supports both fixed-size
|
|
252
|
+
and investment-optimized storage systems with comprehensive techno-economic modeling.
|
|
253
|
+
|
|
254
|
+
Mathematical Formulation:
|
|
255
|
+
See the complete mathematical model in the documentation:
|
|
256
|
+
[Storage](../user-guide/mathematical-notation/elements/Storage.md)
|
|
257
|
+
|
|
258
|
+
- Equation (1): Charge state bounds
|
|
259
|
+
- Equation (3): Storage balance (charge state evolution)
|
|
260
|
+
|
|
261
|
+
Variable Mapping:
|
|
262
|
+
- ``capacity_in_flow_hours`` → C (storage capacity)
|
|
263
|
+
- ``charge_state`` → c(t_i) (state of charge at time t_i)
|
|
264
|
+
- ``relative_loss_per_hour`` → ċ_rel,loss (self-discharge rate)
|
|
265
|
+
- ``eta_charge`` → η_in (charging efficiency)
|
|
266
|
+
- ``eta_discharge`` → η_out (discharging efficiency)
|
|
267
|
+
|
|
268
|
+
Args:
|
|
269
|
+
label: Element identifier used in the FlowSystem.
|
|
270
|
+
charging: Incoming flow for loading the storage.
|
|
271
|
+
discharging: Outgoing flow for unloading the storage.
|
|
272
|
+
capacity_in_flow_hours: Storage capacity in flow-hours (kWh, m³, kg).
|
|
273
|
+
Scalar for fixed size or InvestParameters for optimization.
|
|
274
|
+
relative_minimum_charge_state: Minimum charge state (0-1). Default: 0.
|
|
275
|
+
relative_maximum_charge_state: Maximum charge state (0-1). Default: 1.
|
|
276
|
+
initial_charge_state: Charge at start. Numeric or 'lastValueOfSim'. Default: 0.
|
|
277
|
+
minimal_final_charge_state: Minimum absolute charge required at end (optional).
|
|
278
|
+
maximal_final_charge_state: Maximum absolute charge allowed at end (optional).
|
|
279
|
+
relative_minimum_final_charge_state: Minimum relative charge at end.
|
|
280
|
+
Defaults to last value of relative_minimum_charge_state.
|
|
281
|
+
relative_maximum_final_charge_state: Maximum relative charge at end.
|
|
282
|
+
Defaults to last value of relative_maximum_charge_state.
|
|
283
|
+
eta_charge: Charging efficiency (0-1). Default: 1.
|
|
284
|
+
eta_discharge: Discharging efficiency (0-1). Default: 1.
|
|
285
|
+
relative_loss_per_hour: Self-discharge per hour (0-0.1). Default: 0.
|
|
286
|
+
prevent_simultaneous_charge_and_discharge: Prevent charging and discharging
|
|
287
|
+
simultaneously. Adds binary variables. Default: True.
|
|
288
|
+
meta_data: Additional information stored in results. Python native types only.
|
|
289
|
+
|
|
290
|
+
Examples:
|
|
291
|
+
Battery energy storage system:
|
|
292
|
+
|
|
293
|
+
```python
|
|
294
|
+
battery = Storage(
|
|
295
|
+
label='lithium_battery',
|
|
296
|
+
charging=battery_charge_flow,
|
|
297
|
+
discharging=battery_discharge_flow,
|
|
298
|
+
capacity_in_flow_hours=100, # 100 kWh capacity
|
|
299
|
+
eta_charge=0.95, # 95% charging efficiency
|
|
300
|
+
eta_discharge=0.95, # 95% discharging efficiency
|
|
301
|
+
relative_loss_per_hour=0.001, # 0.1% loss per hour
|
|
302
|
+
relative_minimum_charge_state=0.1, # Never below 10% SOC
|
|
303
|
+
relative_maximum_charge_state=0.9, # Never above 90% SOC
|
|
304
|
+
)
|
|
305
|
+
```
|
|
306
|
+
|
|
307
|
+
Thermal storage with cycling constraints:
|
|
308
|
+
|
|
309
|
+
```python
|
|
310
|
+
thermal_storage = Storage(
|
|
311
|
+
label='hot_water_tank',
|
|
312
|
+
charging=heat_input,
|
|
313
|
+
discharging=heat_output,
|
|
314
|
+
capacity_in_flow_hours=500, # 500 kWh thermal capacity
|
|
315
|
+
initial_charge_state=250, # Start half full
|
|
316
|
+
# Impact of temperature on energy capacity
|
|
317
|
+
relative_maximum_charge_state=water_temperature_spread / rated_temeprature_spread,
|
|
318
|
+
eta_charge=0.90, # Heat exchanger losses
|
|
319
|
+
eta_discharge=0.85, # Distribution losses
|
|
320
|
+
relative_loss_per_hour=0.02, # 2% thermal loss per hour
|
|
321
|
+
prevent_simultaneous_charge_and_discharge=True,
|
|
322
|
+
)
|
|
323
|
+
```
|
|
324
|
+
|
|
325
|
+
Pumped hydro storage with investment optimization:
|
|
326
|
+
|
|
327
|
+
```python
|
|
328
|
+
pumped_hydro = Storage(
|
|
329
|
+
label='pumped_hydro',
|
|
330
|
+
charging=pump_flow,
|
|
331
|
+
discharging=turbine_flow,
|
|
332
|
+
capacity_in_flow_hours=InvestParameters(
|
|
333
|
+
minimum_size=1000, # Minimum economic scale
|
|
334
|
+
maximum_size=10000, # Site constraints
|
|
335
|
+
specific_effects={'cost': 150}, # €150/MWh capacity
|
|
336
|
+
fix_effects={'cost': 50_000_000}, # €50M fixed costs
|
|
337
|
+
),
|
|
338
|
+
eta_charge=0.85, # Pumping efficiency
|
|
339
|
+
eta_discharge=0.90, # Turbine efficiency
|
|
340
|
+
initial_charge_state='lastValueOfSim', # Ensuring no deficit compared to start
|
|
341
|
+
relative_loss_per_hour=0.0001, # Minimal evaporation
|
|
342
|
+
)
|
|
343
|
+
```
|
|
344
|
+
|
|
345
|
+
Material storage with inventory management:
|
|
346
|
+
|
|
347
|
+
```python
|
|
348
|
+
fuel_storage = Storage(
|
|
349
|
+
label='natural_gas_storage',
|
|
350
|
+
charging=gas_injection,
|
|
351
|
+
discharging=gas_withdrawal,
|
|
352
|
+
capacity_in_flow_hours=10000, # 10,000 m³ storage volume
|
|
353
|
+
initial_charge_state=3000, # Start with 3,000 m³
|
|
354
|
+
minimal_final_charge_state=1000, # Strategic reserve
|
|
355
|
+
maximal_final_charge_state=9000, # Prevent overflow
|
|
356
|
+
eta_charge=0.98, # Compression losses
|
|
357
|
+
eta_discharge=0.95, # Pressure reduction losses
|
|
358
|
+
relative_loss_per_hour=0.0005, # 0.05% leakage per hour
|
|
359
|
+
prevent_simultaneous_charge_and_discharge=False, # Allow flow-through
|
|
360
|
+
)
|
|
361
|
+
```
|
|
362
|
+
|
|
363
|
+
Note:
|
|
364
|
+
**Mathematical formulation**: See [Storage](../user-guide/mathematical-notation/elements/Storage.md)
|
|
365
|
+
for charge state evolution equations and balance constraints.
|
|
366
|
+
|
|
367
|
+
**Efficiency parameters** (eta_charge, eta_discharge) are dimensionless (0-1 range).
|
|
368
|
+
The relative_loss_per_hour represents exponential decay per hour.
|
|
369
|
+
|
|
370
|
+
**Binary variables**: When prevent_simultaneous_charge_and_discharge is True, binary
|
|
371
|
+
variables enforce mutual exclusivity, increasing solution time but preventing unrealistic
|
|
372
|
+
simultaneous charging and discharging.
|
|
373
|
+
|
|
374
|
+
**Units**: Flow rates and charge states are related by the concept of 'flow hours' (=flow_rate * time).
|
|
375
|
+
With flow rates in kW, the charge state is therefore (usually) kWh.
|
|
376
|
+
With flow rates in m3/h, the charge state is therefore in m3.
|
|
124
377
|
"""
|
|
125
378
|
|
|
126
379
|
def __init__(
|
|
@@ -128,45 +381,21 @@ class Storage(Component):
|
|
|
128
381
|
label: str,
|
|
129
382
|
charging: Flow,
|
|
130
383
|
discharging: Flow,
|
|
131
|
-
capacity_in_flow_hours:
|
|
132
|
-
relative_minimum_charge_state:
|
|
133
|
-
relative_maximum_charge_state:
|
|
134
|
-
initial_charge_state:
|
|
135
|
-
minimal_final_charge_state:
|
|
136
|
-
maximal_final_charge_state:
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
384
|
+
capacity_in_flow_hours: PeriodicDataUser | InvestParameters,
|
|
385
|
+
relative_minimum_charge_state: TemporalDataUser = 0,
|
|
386
|
+
relative_maximum_charge_state: TemporalDataUser = 1,
|
|
387
|
+
initial_charge_state: PeriodicDataUser | Literal['lastValueOfSim'] = 0,
|
|
388
|
+
minimal_final_charge_state: PeriodicDataUser | None = None,
|
|
389
|
+
maximal_final_charge_state: PeriodicDataUser | None = None,
|
|
390
|
+
relative_minimum_final_charge_state: PeriodicDataUser | None = None,
|
|
391
|
+
relative_maximum_final_charge_state: PeriodicDataUser | None = None,
|
|
392
|
+
eta_charge: TemporalDataUser = 1,
|
|
393
|
+
eta_discharge: TemporalDataUser = 1,
|
|
394
|
+
relative_loss_per_hour: TemporalDataUser = 0,
|
|
140
395
|
prevent_simultaneous_charge_and_discharge: bool = True,
|
|
141
396
|
balanced: bool = False,
|
|
142
|
-
meta_data:
|
|
397
|
+
meta_data: dict | None = None,
|
|
143
398
|
):
|
|
144
|
-
"""
|
|
145
|
-
Storages have one incoming and one outgoing Flow each with an efficiency.
|
|
146
|
-
Further, storages have a `size` and a `charge_state`.
|
|
147
|
-
Similarly to the flow-rate of a Flow, the `size` combined with a relative upper and lower bound
|
|
148
|
-
limits the `charge_state` of the storage.
|
|
149
|
-
|
|
150
|
-
For mathematical details take a look at our online documentation
|
|
151
|
-
|
|
152
|
-
Args:
|
|
153
|
-
label: The label of the Element. Used to identify it in the FlowSystem
|
|
154
|
-
charging: ingoing flow.
|
|
155
|
-
discharging: outgoing flow.
|
|
156
|
-
capacity_in_flow_hours: nominal capacity/size of the storage
|
|
157
|
-
relative_minimum_charge_state: minimum relative charge state. The default is 0.
|
|
158
|
-
relative_maximum_charge_state: maximum relative charge state. The default is 1.
|
|
159
|
-
initial_charge_state: storage charge_state at the beginning. The default is 0.
|
|
160
|
-
minimal_final_charge_state: minimal value of chargeState at the end of timeseries.
|
|
161
|
-
maximal_final_charge_state: maximal value of chargeState at the end of timeseries.
|
|
162
|
-
eta_charge: efficiency factor of charging/loading. The default is 1.
|
|
163
|
-
eta_discharge: efficiency factor of uncharging/unloading. The default is 1.
|
|
164
|
-
relative_loss_per_hour: loss per chargeState-Unit per hour. The default is 0.
|
|
165
|
-
prevent_simultaneous_charge_and_discharge: If True, loading and unloading at the same time is not possible.
|
|
166
|
-
Increases the number of binary variables, but is recommended for easier evaluation. The default is True.
|
|
167
|
-
balanced: Wether to equate the size of the charging and discharging flow. Only if not fixed.
|
|
168
|
-
meta_data: used to store more information about the Element. Is not used internally, but saved in the results. Only use python native types.
|
|
169
|
-
"""
|
|
170
399
|
# TODO: fixed_relative_chargeState implementieren
|
|
171
400
|
super().__init__(
|
|
172
401
|
label,
|
|
@@ -179,56 +408,68 @@ class Storage(Component):
|
|
|
179
408
|
self.charging = charging
|
|
180
409
|
self.discharging = discharging
|
|
181
410
|
self.capacity_in_flow_hours = capacity_in_flow_hours
|
|
182
|
-
self.relative_minimum_charge_state:
|
|
183
|
-
self.relative_maximum_charge_state:
|
|
411
|
+
self.relative_minimum_charge_state: TemporalDataUser = relative_minimum_charge_state
|
|
412
|
+
self.relative_maximum_charge_state: TemporalDataUser = relative_maximum_charge_state
|
|
413
|
+
|
|
414
|
+
self.relative_minimum_final_charge_state = relative_minimum_final_charge_state
|
|
415
|
+
self.relative_maximum_final_charge_state = relative_maximum_final_charge_state
|
|
184
416
|
|
|
185
417
|
self.initial_charge_state = initial_charge_state
|
|
186
418
|
self.minimal_final_charge_state = minimal_final_charge_state
|
|
187
419
|
self.maximal_final_charge_state = maximal_final_charge_state
|
|
188
420
|
|
|
189
|
-
self.eta_charge:
|
|
190
|
-
self.eta_discharge:
|
|
191
|
-
self.relative_loss_per_hour:
|
|
421
|
+
self.eta_charge: TemporalDataUser = eta_charge
|
|
422
|
+
self.eta_discharge: TemporalDataUser = eta_discharge
|
|
423
|
+
self.relative_loss_per_hour: TemporalDataUser = relative_loss_per_hour
|
|
192
424
|
self.prevent_simultaneous_charge_and_discharge = prevent_simultaneous_charge_and_discharge
|
|
193
425
|
self.balanced = balanced
|
|
194
426
|
|
|
195
|
-
def create_model(self, model:
|
|
427
|
+
def create_model(self, model: FlowSystemModel) -> StorageModel:
|
|
196
428
|
self._plausibility_checks()
|
|
197
|
-
self.
|
|
198
|
-
return self.
|
|
199
|
-
|
|
200
|
-
def transform_data(self, flow_system:
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
429
|
+
self.submodel = StorageModel(model, self)
|
|
430
|
+
return self.submodel
|
|
431
|
+
|
|
432
|
+
def transform_data(self, flow_system: FlowSystem, name_prefix: str = '') -> None:
|
|
433
|
+
prefix = '|'.join(filter(None, [name_prefix, self.label_full]))
|
|
434
|
+
super().transform_data(flow_system, prefix)
|
|
435
|
+
self.relative_minimum_charge_state = flow_system.fit_to_model_coords(
|
|
436
|
+
f'{prefix}|relative_minimum_charge_state',
|
|
204
437
|
self.relative_minimum_charge_state,
|
|
205
|
-
has_extra_timestep=True,
|
|
206
438
|
)
|
|
207
|
-
self.relative_maximum_charge_state = flow_system.
|
|
208
|
-
f'{
|
|
439
|
+
self.relative_maximum_charge_state = flow_system.fit_to_model_coords(
|
|
440
|
+
f'{prefix}|relative_maximum_charge_state',
|
|
209
441
|
self.relative_maximum_charge_state,
|
|
210
|
-
has_extra_timestep=True,
|
|
211
442
|
)
|
|
212
|
-
self.eta_charge = flow_system.
|
|
213
|
-
self.eta_discharge = flow_system.
|
|
214
|
-
self.relative_loss_per_hour = flow_system.
|
|
215
|
-
f'{
|
|
443
|
+
self.eta_charge = flow_system.fit_to_model_coords(f'{prefix}|eta_charge', self.eta_charge)
|
|
444
|
+
self.eta_discharge = flow_system.fit_to_model_coords(f'{prefix}|eta_discharge', self.eta_discharge)
|
|
445
|
+
self.relative_loss_per_hour = flow_system.fit_to_model_coords(
|
|
446
|
+
f'{prefix}|relative_loss_per_hour', self.relative_loss_per_hour
|
|
216
447
|
)
|
|
217
448
|
if not isinstance(self.initial_charge_state, str):
|
|
218
|
-
self.initial_charge_state = flow_system.
|
|
219
|
-
f'{
|
|
449
|
+
self.initial_charge_state = flow_system.fit_to_model_coords(
|
|
450
|
+
f'{prefix}|initial_charge_state', self.initial_charge_state, dims=['period', 'scenario']
|
|
220
451
|
)
|
|
221
|
-
self.minimal_final_charge_state = flow_system.
|
|
222
|
-
f'{
|
|
452
|
+
self.minimal_final_charge_state = flow_system.fit_to_model_coords(
|
|
453
|
+
f'{prefix}|minimal_final_charge_state', self.minimal_final_charge_state, dims=['period', 'scenario']
|
|
223
454
|
)
|
|
224
|
-
self.maximal_final_charge_state = flow_system.
|
|
225
|
-
f'{
|
|
455
|
+
self.maximal_final_charge_state = flow_system.fit_to_model_coords(
|
|
456
|
+
f'{prefix}|maximal_final_charge_state', self.maximal_final_charge_state, dims=['period', 'scenario']
|
|
457
|
+
)
|
|
458
|
+
self.relative_minimum_final_charge_state = flow_system.fit_to_model_coords(
|
|
459
|
+
f'{prefix}|relative_minimum_final_charge_state',
|
|
460
|
+
self.relative_minimum_final_charge_state,
|
|
461
|
+
dims=['period', 'scenario'],
|
|
462
|
+
)
|
|
463
|
+
self.relative_maximum_final_charge_state = flow_system.fit_to_model_coords(
|
|
464
|
+
f'{prefix}|relative_maximum_final_charge_state',
|
|
465
|
+
self.relative_maximum_final_charge_state,
|
|
466
|
+
dims=['period', 'scenario'],
|
|
226
467
|
)
|
|
227
468
|
if isinstance(self.capacity_in_flow_hours, InvestParameters):
|
|
228
|
-
self.capacity_in_flow_hours.transform_data(flow_system, f'{
|
|
469
|
+
self.capacity_in_flow_hours.transform_data(flow_system, f'{prefix}|InvestParameters')
|
|
229
470
|
else:
|
|
230
|
-
self.capacity_in_flow_hours = flow_system.
|
|
231
|
-
f'{
|
|
471
|
+
self.capacity_in_flow_hours = flow_system.fit_to_model_coords(
|
|
472
|
+
f'{prefix}|capacity_in_flow_hours', self.capacity_in_flow_hours, dims=['period', 'scenario']
|
|
232
473
|
)
|
|
233
474
|
|
|
234
475
|
def _plausibility_checks(self) -> None:
|
|
@@ -236,89 +477,183 @@ class Storage(Component):
|
|
|
236
477
|
Check for infeasible or uncommon combinations of parameters
|
|
237
478
|
"""
|
|
238
479
|
super()._plausibility_checks()
|
|
480
|
+
|
|
481
|
+
# Validate string values and set flag
|
|
482
|
+
initial_is_last = False
|
|
239
483
|
if isinstance(self.initial_charge_state, str):
|
|
240
|
-
if self.initial_charge_state
|
|
484
|
+
if self.initial_charge_state == 'lastValueOfSim':
|
|
485
|
+
initial_is_last = True
|
|
486
|
+
else:
|
|
241
487
|
raise PlausibilityError(f'initial_charge_state has undefined value: {self.initial_charge_state}')
|
|
242
|
-
|
|
488
|
+
|
|
489
|
+
# Use new InvestParameters methods to get capacity bounds
|
|
243
490
|
if isinstance(self.capacity_in_flow_hours, InvestParameters):
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
minimum_capacity = self.capacity_in_flow_hours.minimum_size
|
|
247
|
-
else:
|
|
248
|
-
maximum_capacity = self.capacity_in_flow_hours.fixed_size
|
|
249
|
-
minimum_capacity = self.capacity_in_flow_hours.fixed_size
|
|
491
|
+
minimum_capacity = self.capacity_in_flow_hours.minimum_or_fixed_size
|
|
492
|
+
maximum_capacity = self.capacity_in_flow_hours.maximum_or_fixed_size
|
|
250
493
|
else:
|
|
251
494
|
maximum_capacity = self.capacity_in_flow_hours
|
|
252
495
|
minimum_capacity = self.capacity_in_flow_hours
|
|
253
496
|
|
|
254
|
-
#
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
maximum_inital_capacity = minimum_capacity * self.relative_maximum_charge_state.isel(time=0)
|
|
258
|
-
# TODO: index=1 ??? I think index 0
|
|
497
|
+
# Initial capacity should not constraint investment decision
|
|
498
|
+
minimum_initial_capacity = maximum_capacity * self.relative_minimum_charge_state.isel(time=0)
|
|
499
|
+
maximum_initial_capacity = minimum_capacity * self.relative_maximum_charge_state.isel(time=0)
|
|
259
500
|
|
|
260
|
-
if
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
501
|
+
# Only perform numeric comparisons if not using 'lastValueOfSim'
|
|
502
|
+
if not initial_is_last:
|
|
503
|
+
if (self.initial_charge_state > maximum_initial_capacity).any():
|
|
504
|
+
raise PlausibilityError(
|
|
505
|
+
f'{self.label_full}: {self.initial_charge_state=} '
|
|
506
|
+
f'is constraining the investment decision. Chosse a value above {maximum_initial_capacity}'
|
|
507
|
+
)
|
|
508
|
+
if (self.initial_charge_state < minimum_initial_capacity).any():
|
|
509
|
+
raise PlausibilityError(
|
|
510
|
+
f'{self.label_full}: {self.initial_charge_state=} '
|
|
511
|
+
f'is constraining the investment decision. Chosse a value below {minimum_initial_capacity}'
|
|
512
|
+
)
|
|
270
513
|
|
|
271
514
|
if self.balanced:
|
|
272
|
-
if not isinstance(self.charging.size, InvestParameters) or not isinstance(
|
|
515
|
+
if not isinstance(self.charging.size, InvestParameters) or not isinstance(
|
|
516
|
+
self.discharging.size, InvestParameters
|
|
517
|
+
):
|
|
273
518
|
raise PlausibilityError(
|
|
274
|
-
f'Balancing charging and discharging Flows in {self.label_full} '
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
519
|
+
f'Balancing charging and discharging Flows in {self.label_full} is only possible with Investments.'
|
|
520
|
+
)
|
|
521
|
+
|
|
522
|
+
if (self.charging.size.minimum_size > self.discharging.size.maximum_size).any() or (
|
|
523
|
+
self.charging.size.maximum_size < self.discharging.size.minimum_size
|
|
524
|
+
).any():
|
|
278
525
|
raise PlausibilityError(
|
|
279
526
|
f'Balancing charging and discharging Flows in {self.label_full} need compatible minimum and maximum sizes.'
|
|
280
527
|
f'Got: {self.charging.size.minimum_size=}, {self.charging.size.maximum_size=} and '
|
|
281
|
-
f'{self.
|
|
528
|
+
f'{self.discharging.size.minimum_size=}, {self.discharging.size.maximum_size=}.'
|
|
529
|
+
)
|
|
282
530
|
|
|
283
531
|
|
|
284
532
|
@register_class_for_io
|
|
285
533
|
class Transmission(Component):
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
534
|
+
"""
|
|
535
|
+
Models transmission infrastructure that transports flows between two locations with losses.
|
|
536
|
+
|
|
537
|
+
Transmission components represent physical infrastructure like pipes, cables,
|
|
538
|
+
transmission lines, or conveyor systems that transport energy or materials between
|
|
539
|
+
two points. They can model both unidirectional and bidirectional flow with
|
|
540
|
+
configurable loss mechanisms and operational constraints.
|
|
541
|
+
|
|
542
|
+
The component supports complex transmission scenarios including relative losses
|
|
543
|
+
(proportional to flow), absolute losses (fixed when active), and bidirectional
|
|
544
|
+
operation with flow direction constraints.
|
|
545
|
+
|
|
546
|
+
Args:
|
|
547
|
+
label: The label of the Element. Used to identify it in the FlowSystem.
|
|
548
|
+
in1: The primary inflow (side A). Pass InvestParameters here for capacity optimization.
|
|
549
|
+
out1: The primary outflow (side B).
|
|
550
|
+
in2: Optional secondary inflow (side B) for bidirectional operation.
|
|
551
|
+
If in1 has InvestParameters, in2 will automatically have matching capacity.
|
|
552
|
+
out2: Optional secondary outflow (side A) for bidirectional operation.
|
|
553
|
+
relative_losses: Proportional losses as fraction of throughput (e.g., 0.02 for 2% loss).
|
|
554
|
+
Applied as: output = input × (1 - relative_losses)
|
|
555
|
+
absolute_losses: Fixed losses that occur when transmission is active.
|
|
556
|
+
Automatically creates binary variables for on/off states.
|
|
557
|
+
on_off_parameters: Parameters defining binary operation constraints and costs.
|
|
558
|
+
prevent_simultaneous_flows_in_both_directions: If True, prevents simultaneous
|
|
559
|
+
flow in both directions. Increases binary variables but reflects physical
|
|
560
|
+
reality for most transmission systems. Default is True.
|
|
561
|
+
balanced: Whether to equate the size of the in1 and in2 Flow. Needs InvestParameters in both Flows.
|
|
562
|
+
meta_data: Used to store additional information. Not used internally but saved
|
|
563
|
+
in results. Only use Python native types.
|
|
564
|
+
|
|
565
|
+
Examples:
|
|
566
|
+
Simple electrical transmission line:
|
|
567
|
+
|
|
568
|
+
```python
|
|
569
|
+
power_line = Transmission(
|
|
570
|
+
label='110kv_line',
|
|
571
|
+
in1=substation_a_out,
|
|
572
|
+
out1=substation_b_in,
|
|
573
|
+
relative_losses=0.03, # 3% line losses
|
|
574
|
+
)
|
|
575
|
+
```
|
|
576
|
+
|
|
577
|
+
Bidirectional natural gas pipeline:
|
|
578
|
+
|
|
579
|
+
```python
|
|
580
|
+
gas_pipeline = Transmission(
|
|
581
|
+
label='interstate_pipeline',
|
|
582
|
+
in1=compressor_station_a,
|
|
583
|
+
out1=distribution_hub_b,
|
|
584
|
+
in2=compressor_station_b,
|
|
585
|
+
out2=distribution_hub_a,
|
|
586
|
+
relative_losses=0.005, # 0.5% friction losses
|
|
587
|
+
absolute_losses=50, # 50 kW compressor power when active
|
|
588
|
+
prevent_simultaneous_flows_in_both_directions=True,
|
|
589
|
+
)
|
|
590
|
+
```
|
|
591
|
+
|
|
592
|
+
District heating network with investment optimization:
|
|
593
|
+
|
|
594
|
+
```python
|
|
595
|
+
heating_network = Transmission(
|
|
596
|
+
label='dh_main_line',
|
|
597
|
+
in1=Flow(
|
|
598
|
+
label='heat_supply',
|
|
599
|
+
bus=central_plant_bus,
|
|
600
|
+
size=InvestParameters(
|
|
601
|
+
minimum_size=1000, # Minimum 1 MW capacity
|
|
602
|
+
maximum_size=10000, # Maximum 10 MW capacity
|
|
603
|
+
specific_effects={'cost': 200}, # €200/kW capacity
|
|
604
|
+
fix_effects={'cost': 500000}, # €500k fixed installation
|
|
605
|
+
),
|
|
606
|
+
),
|
|
607
|
+
out1=district_heat_demand,
|
|
608
|
+
relative_losses=0.15, # 15% thermal losses in distribution
|
|
609
|
+
)
|
|
610
|
+
```
|
|
611
|
+
|
|
612
|
+
Material conveyor with on/off operation:
|
|
613
|
+
|
|
614
|
+
```python
|
|
615
|
+
conveyor_belt = Transmission(
|
|
616
|
+
label='material_transport',
|
|
617
|
+
in1=loading_station,
|
|
618
|
+
out1=unloading_station,
|
|
619
|
+
absolute_losses=25, # 25 kW motor power when running
|
|
620
|
+
on_off_parameters=OnOffParameters(
|
|
621
|
+
effects_per_switch_on={'maintenance': 0.1},
|
|
622
|
+
consecutive_on_hours_min=2, # Minimum 2-hour operation
|
|
623
|
+
switch_on_total_max=10, # Maximum 10 starts per day
|
|
624
|
+
),
|
|
625
|
+
)
|
|
626
|
+
```
|
|
627
|
+
|
|
628
|
+
Note:
|
|
629
|
+
The transmission equation balances flows with losses:
|
|
630
|
+
output_flow = input_flow × (1 - relative_losses) - absolute_losses
|
|
631
|
+
|
|
632
|
+
For bidirectional transmission, each direction has independent loss calculations.
|
|
633
|
+
|
|
634
|
+
When using InvestParameters on in1, the capacity automatically applies to in2
|
|
635
|
+
to maintain consistent bidirectional capacity without additional investment variables.
|
|
636
|
+
|
|
637
|
+
Absolute losses force the creation of binary on/off variables, which increases
|
|
638
|
+
computational complexity but enables realistic modeling of equipment with
|
|
639
|
+
standby power consumption.
|
|
640
|
+
|
|
641
|
+
"""
|
|
291
642
|
|
|
292
643
|
def __init__(
|
|
293
644
|
self,
|
|
294
645
|
label: str,
|
|
295
646
|
in1: Flow,
|
|
296
647
|
out1: Flow,
|
|
297
|
-
in2:
|
|
298
|
-
out2:
|
|
299
|
-
relative_losses:
|
|
300
|
-
absolute_losses:
|
|
648
|
+
in2: Flow | None = None,
|
|
649
|
+
out2: Flow | None = None,
|
|
650
|
+
relative_losses: TemporalDataUser | None = None,
|
|
651
|
+
absolute_losses: TemporalDataUser | None = None,
|
|
301
652
|
on_off_parameters: OnOffParameters = None,
|
|
302
653
|
prevent_simultaneous_flows_in_both_directions: bool = True,
|
|
303
|
-
|
|
654
|
+
balanced: bool = False,
|
|
655
|
+
meta_data: dict | None = None,
|
|
304
656
|
):
|
|
305
|
-
"""
|
|
306
|
-
Initializes a Transmission component (Pipe, cable, ...) that models the flows between two sides
|
|
307
|
-
with potential losses.
|
|
308
|
-
|
|
309
|
-
Args:
|
|
310
|
-
label: The label of the Element. Used to identify it in the FlowSystem
|
|
311
|
-
in1: The inflow at side A. Pass InvestmentParameters here.
|
|
312
|
-
out1: The outflow at side B.
|
|
313
|
-
in2: The optional inflow at side B.
|
|
314
|
-
If in1 got InvestParameters, the size of this Flow will be equal to in1 (with no extra effects!)
|
|
315
|
-
out2: The optional outflow at side A.
|
|
316
|
-
relative_losses: The relative loss between inflow and outflow, e.g., 0.02 for 2% loss.
|
|
317
|
-
absolute_losses: The absolute loss, occur only when the Flow is on. Induces the creation of the ON-Variable
|
|
318
|
-
on_off_parameters: Parameters defining the on/off behavior of the component.
|
|
319
|
-
prevent_simultaneous_flows_in_both_directions: If True, inflow and outflow are not allowed to be both non-zero at same timestep.
|
|
320
|
-
meta_data: used to store more information about the Element. Is not used internally, but saved in the results. Only use python native types.
|
|
321
|
-
"""
|
|
322
657
|
super().__init__(
|
|
323
658
|
label,
|
|
324
659
|
inputs=[flow for flow in (in1, in2) if flow is not None],
|
|
@@ -336,6 +671,7 @@ class Transmission(Component):
|
|
|
336
671
|
|
|
337
672
|
self.relative_losses = relative_losses
|
|
338
673
|
self.absolute_losses = absolute_losses
|
|
674
|
+
self.balanced = balanced
|
|
339
675
|
|
|
340
676
|
def _plausibility_checks(self):
|
|
341
677
|
super()._plausibility_checks()
|
|
@@ -348,51 +684,47 @@ class Transmission(Component):
|
|
|
348
684
|
assert self.out2.bus == self.in1.bus, (
|
|
349
685
|
f'Input 1 and Output 2 do not start/end at the same Bus: {self.in1.bus=}, {self.out2.bus=}'
|
|
350
686
|
)
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
if
|
|
687
|
+
|
|
688
|
+
if self.balanced:
|
|
689
|
+
if self.in2 is None:
|
|
690
|
+
raise ValueError('Balanced Transmission needs InvestParameters in both in-Flows')
|
|
691
|
+
if not isinstance(self.in1.size, InvestParameters) or not isinstance(self.in2.size, InvestParameters):
|
|
692
|
+
raise ValueError('Balanced Transmission needs InvestParameters in both in-Flows')
|
|
693
|
+
if (self.in1.size.minimum_or_fixed_size > self.in2.size.maximum_or_fixed_size).any() or (
|
|
694
|
+
self.in1.size.maximum_or_fixed_size < self.in2.size.minimum_or_fixed_size
|
|
695
|
+
).any():
|
|
354
696
|
raise ValueError(
|
|
355
|
-
'Transmission
|
|
356
|
-
'
|
|
697
|
+
f'Balanced Transmission needs compatible minimum and maximum sizes.'
|
|
698
|
+
f'Got: {self.in1.size.minimum_size=}, {self.in1.size.maximum_size=}, {self.in1.size.fixed_size=} and '
|
|
699
|
+
f'{self.in2.size.minimum_size=}, {self.in2.size.maximum_size=}, {self.in2.size.fixed_size=}.'
|
|
357
700
|
)
|
|
358
701
|
|
|
359
|
-
def create_model(self, model) ->
|
|
702
|
+
def create_model(self, model) -> TransmissionModel:
|
|
360
703
|
self._plausibility_checks()
|
|
361
|
-
self.
|
|
362
|
-
return self.
|
|
704
|
+
self.submodel = TransmissionModel(model, self)
|
|
705
|
+
return self.submodel
|
|
363
706
|
|
|
364
|
-
def transform_data(self, flow_system:
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
)
|
|
369
|
-
self.absolute_losses = flow_system.create_time_series(
|
|
370
|
-
f'{self.label_full}|absolute_losses', self.absolute_losses
|
|
371
|
-
)
|
|
707
|
+
def transform_data(self, flow_system: FlowSystem, name_prefix: str = '') -> None:
|
|
708
|
+
prefix = '|'.join(filter(None, [name_prefix, self.label_full]))
|
|
709
|
+
super().transform_data(flow_system, prefix)
|
|
710
|
+
self.relative_losses = flow_system.fit_to_model_coords(f'{prefix}|relative_losses', self.relative_losses)
|
|
711
|
+
self.absolute_losses = flow_system.fit_to_model_coords(f'{prefix}|absolute_losses', self.absolute_losses)
|
|
372
712
|
|
|
373
713
|
|
|
374
714
|
class TransmissionModel(ComponentModel):
|
|
375
|
-
|
|
376
|
-
super().__init__(model, element)
|
|
377
|
-
self.element: Transmission = element
|
|
378
|
-
self.on_off: Optional[OnOffModel] = None
|
|
715
|
+
element: Transmission
|
|
379
716
|
|
|
380
|
-
def
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
if (self.element.absolute_losses is not None) and np.any(self.element.absolute_losses.selected_data != 0):
|
|
384
|
-
for flow in self.element.inputs + self.element.outputs:
|
|
717
|
+
def __init__(self, model: FlowSystemModel, element: Transmission):
|
|
718
|
+
if (element.absolute_losses is not None) and np.any(element.absolute_losses != 0):
|
|
719
|
+
for flow in element.inputs + element.outputs:
|
|
385
720
|
if flow.on_off_parameters is None:
|
|
386
721
|
flow.on_off_parameters = OnOffParameters()
|
|
387
722
|
|
|
388
|
-
|
|
389
|
-
if self.element.in2 is not None:
|
|
390
|
-
if isinstance(self.element.in1.size, InvestParameters) and not isinstance(
|
|
391
|
-
self.element.in2.size, InvestParameters
|
|
392
|
-
):
|
|
393
|
-
self.element.in2.size = InvestParameters(maximum_size=self.element.in1.size.maximum_size)
|
|
723
|
+
super().__init__(model, element)
|
|
394
724
|
|
|
395
|
-
|
|
725
|
+
def _do_modeling(self):
|
|
726
|
+
"""Initiates all FlowModels"""
|
|
727
|
+
super()._do_modeling()
|
|
396
728
|
|
|
397
729
|
# first direction
|
|
398
730
|
self.create_transmission_equation('dir1', self.element.in1, self.element.out1)
|
|
@@ -402,43 +734,37 @@ class TransmissionModel(ComponentModel):
|
|
|
402
734
|
self.create_transmission_equation('dir2', self.element.in2, self.element.out2)
|
|
403
735
|
|
|
404
736
|
# equate size of both directions
|
|
405
|
-
if
|
|
737
|
+
if self.element.balanced:
|
|
406
738
|
# eq: in1.size = in2.size
|
|
407
|
-
self.
|
|
408
|
-
self.
|
|
409
|
-
|
|
410
|
-
name=f'{self.label_full}|same_size',
|
|
411
|
-
),
|
|
412
|
-
'same_size',
|
|
739
|
+
self.add_constraints(
|
|
740
|
+
self.element.in1.submodel._investment.size == self.element.in2.submodel._investment.size,
|
|
741
|
+
short_name='same_size',
|
|
413
742
|
)
|
|
414
743
|
|
|
415
744
|
def create_transmission_equation(self, name: str, in_flow: Flow, out_flow: Flow) -> linopy.Constraint:
|
|
416
745
|
"""Creates an Equation for the Transmission efficiency and adds it to the model"""
|
|
417
746
|
# eq: out(t) + on(t)*loss_abs(t) = in(t)*(1 - loss_rel(t))
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
),
|
|
423
|
-
name,
|
|
747
|
+
rel_losses = 0 if self.element.relative_losses is None else self.element.relative_losses
|
|
748
|
+
con_transmission = self.add_constraints(
|
|
749
|
+
out_flow.submodel.flow_rate == in_flow.submodel.flow_rate * (1 - rel_losses),
|
|
750
|
+
short_name=name,
|
|
424
751
|
)
|
|
425
752
|
|
|
426
753
|
if self.element.absolute_losses is not None:
|
|
427
|
-
con_transmission.lhs += in_flow.
|
|
754
|
+
con_transmission.lhs += in_flow.submodel.on_off.on * self.element.absolute_losses
|
|
428
755
|
|
|
429
756
|
return con_transmission
|
|
430
757
|
|
|
431
758
|
|
|
432
759
|
class LinearConverterModel(ComponentModel):
|
|
433
|
-
|
|
434
|
-
super().__init__(model, element)
|
|
435
|
-
self.element: LinearConverter = element
|
|
436
|
-
self.on_off: Optional[OnOffModel] = None
|
|
437
|
-
self.piecewise_conversion: Optional[PiecewiseConversion] = None
|
|
760
|
+
element: LinearConverter
|
|
438
761
|
|
|
439
|
-
def
|
|
440
|
-
|
|
762
|
+
def __init__(self, model: FlowSystemModel, element: LinearConverter):
|
|
763
|
+
self.piecewise_conversion: PiecewiseConversion | None = None
|
|
764
|
+
super().__init__(model, element)
|
|
441
765
|
|
|
766
|
+
def _do_modeling(self):
|
|
767
|
+
super()._do_modeling()
|
|
442
768
|
# conversion_factors:
|
|
443
769
|
if self.element.conversion_factors:
|
|
444
770
|
all_input_flows = set(self.element.inputs)
|
|
@@ -447,159 +773,135 @@ class LinearConverterModel(ComponentModel):
|
|
|
447
773
|
# für alle linearen Gleichungen:
|
|
448
774
|
for i, conv_factors in enumerate(self.element.conversion_factors):
|
|
449
775
|
used_flows = set([self.element.flows[flow_label] for flow_label in conv_factors])
|
|
450
|
-
used_inputs:
|
|
451
|
-
used_outputs:
|
|
452
|
-
|
|
453
|
-
self.
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
[flow.model.flow_rate * conv_factors[flow.label].selected_data for flow in used_outputs]
|
|
458
|
-
),
|
|
459
|
-
name=f'{self.label_full}|conversion_{i}',
|
|
460
|
-
)
|
|
776
|
+
used_inputs: set[Flow] = all_input_flows & used_flows
|
|
777
|
+
used_outputs: set[Flow] = all_output_flows & used_flows
|
|
778
|
+
|
|
779
|
+
self.add_constraints(
|
|
780
|
+
sum([flow.submodel.flow_rate * conv_factors[flow.label] for flow in used_inputs])
|
|
781
|
+
== sum([flow.submodel.flow_rate * conv_factors[flow.label] for flow in used_outputs]),
|
|
782
|
+
short_name=f'conversion_{i}',
|
|
461
783
|
)
|
|
462
784
|
|
|
463
785
|
else:
|
|
464
786
|
# TODO: Improve Inclusion of OnOffParameters. Instead of creating a Binary in every flow, the binary could only be part of the Piece itself
|
|
465
787
|
piecewise_conversion = {
|
|
466
|
-
self.element.flows[flow].
|
|
788
|
+
self.element.flows[flow].submodel.flow_rate.name: piecewise
|
|
467
789
|
for flow, piecewise in self.element.piecewise_conversion.items()
|
|
468
790
|
}
|
|
469
791
|
|
|
470
|
-
self.piecewise_conversion = self.
|
|
792
|
+
self.piecewise_conversion = self.add_submodels(
|
|
471
793
|
PiecewiseModel(
|
|
472
794
|
model=self._model,
|
|
473
795
|
label_of_element=self.label_of_element,
|
|
796
|
+
label_of_model=f'{self.label_of_element}',
|
|
474
797
|
piecewise_variables=piecewise_conversion,
|
|
475
798
|
zero_point=self.on_off.on if self.on_off is not None else False,
|
|
476
|
-
|
|
477
|
-
)
|
|
799
|
+
dims=('time', 'period', 'scenario'),
|
|
800
|
+
),
|
|
801
|
+
short_name='PiecewiseConversion',
|
|
478
802
|
)
|
|
479
|
-
self.piecewise_conversion.do_modeling()
|
|
480
803
|
|
|
481
804
|
|
|
482
805
|
class StorageModel(ComponentModel):
|
|
483
|
-
"""
|
|
806
|
+
"""Submodel of Storage"""
|
|
484
807
|
|
|
485
|
-
|
|
808
|
+
element: Storage
|
|
809
|
+
|
|
810
|
+
def __init__(self, model: FlowSystemModel, element: Storage):
|
|
486
811
|
super().__init__(model, element)
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
self._model.add_variables(
|
|
498
|
-
lower=lb,
|
|
499
|
-
upper=ub,
|
|
500
|
-
coords=self._model.get_coords(extra_timestep=True),
|
|
501
|
-
name=f'{self.label_full}|charge_state',
|
|
502
|
-
),
|
|
503
|
-
'charge_state',
|
|
504
|
-
)
|
|
505
|
-
self.netto_discharge = self.add(
|
|
506
|
-
self._model.add_variables(coords=self._model.get_coords(), name=f'{self.label_full}|netto_discharge'),
|
|
507
|
-
'netto_discharge',
|
|
812
|
+
|
|
813
|
+
def _do_modeling(self):
|
|
814
|
+
super()._do_modeling()
|
|
815
|
+
|
|
816
|
+
lb, ub = self._absolute_charge_state_bounds
|
|
817
|
+
self.add_variables(
|
|
818
|
+
lower=lb,
|
|
819
|
+
upper=ub,
|
|
820
|
+
coords=self._model.get_coords(extra_timestep=True),
|
|
821
|
+
short_name='charge_state',
|
|
508
822
|
)
|
|
823
|
+
|
|
824
|
+
self.add_variables(coords=self._model.get_coords(), short_name='netto_discharge')
|
|
825
|
+
|
|
509
826
|
# netto_discharge:
|
|
510
827
|
# eq: nettoFlow(t) - discharging(t) + charging(t) = 0
|
|
511
|
-
self.
|
|
512
|
-
self.
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
name=f'{self.label_full}|netto_discharge',
|
|
516
|
-
),
|
|
517
|
-
'netto_discharge',
|
|
828
|
+
self.add_constraints(
|
|
829
|
+
self.netto_discharge
|
|
830
|
+
== self.element.discharging.submodel.flow_rate - self.element.charging.submodel.flow_rate,
|
|
831
|
+
short_name='netto_discharge',
|
|
518
832
|
)
|
|
519
833
|
|
|
520
834
|
charge_state = self.charge_state
|
|
521
|
-
rel_loss = self.element.relative_loss_per_hour
|
|
835
|
+
rel_loss = self.element.relative_loss_per_hour
|
|
522
836
|
hours_per_step = self._model.hours_per_step
|
|
523
|
-
charge_rate = self.element.charging.
|
|
524
|
-
discharge_rate = self.element.discharging.
|
|
525
|
-
eff_charge = self.element.eta_charge
|
|
526
|
-
eff_discharge = self.element.eta_discharge
|
|
527
|
-
|
|
528
|
-
self.
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
name=f'{self.label_full}|charge_state',
|
|
535
|
-
),
|
|
536
|
-
'charge_state',
|
|
837
|
+
charge_rate = self.element.charging.submodel.flow_rate
|
|
838
|
+
discharge_rate = self.element.discharging.submodel.flow_rate
|
|
839
|
+
eff_charge = self.element.eta_charge
|
|
840
|
+
eff_discharge = self.element.eta_discharge
|
|
841
|
+
|
|
842
|
+
self.add_constraints(
|
|
843
|
+
charge_state.isel(time=slice(1, None))
|
|
844
|
+
== charge_state.isel(time=slice(None, -1)) * ((1 - rel_loss) ** hours_per_step)
|
|
845
|
+
+ charge_rate * eff_charge * hours_per_step
|
|
846
|
+
- discharge_rate * hours_per_step / eff_discharge,
|
|
847
|
+
short_name='charge_state',
|
|
537
848
|
)
|
|
538
849
|
|
|
539
850
|
if isinstance(self.element.capacity_in_flow_hours, InvestParameters):
|
|
540
|
-
self.
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
851
|
+
self.add_submodels(
|
|
852
|
+
InvestmentModel(
|
|
853
|
+
model=self._model,
|
|
854
|
+
label_of_element=self.label_of_element,
|
|
855
|
+
label_of_model=self.label_of_element,
|
|
856
|
+
parameters=self.element.capacity_in_flow_hours,
|
|
857
|
+
),
|
|
858
|
+
short_name='investment',
|
|
859
|
+
)
|
|
860
|
+
|
|
861
|
+
BoundingPatterns.scaled_bounds(
|
|
862
|
+
self,
|
|
863
|
+
variable=self.charge_state,
|
|
864
|
+
scaling_variable=self.investment.size,
|
|
865
|
+
relative_bounds=self._relative_charge_state_bounds,
|
|
546
866
|
)
|
|
547
|
-
self.sub_models.append(self._investment)
|
|
548
|
-
self._investment.do_modeling()
|
|
549
867
|
|
|
550
868
|
# Initial charge state
|
|
551
869
|
self._initial_and_final_charge_state()
|
|
552
870
|
|
|
553
871
|
if self.element.balanced:
|
|
554
|
-
self.
|
|
555
|
-
self.
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
),
|
|
559
|
-
'balanced_sizes'
|
|
872
|
+
self.add_constraints(
|
|
873
|
+
self.element.charging.submodel._investment.size * 1
|
|
874
|
+
== self.element.discharging.submodel._investment.size * 1,
|
|
875
|
+
short_name='balanced_sizes',
|
|
560
876
|
)
|
|
561
877
|
|
|
562
878
|
def _initial_and_final_charge_state(self):
|
|
563
879
|
if self.element.initial_charge_state is not None:
|
|
564
|
-
name_short = 'initial_charge_state'
|
|
565
|
-
name = f'{self.label_full}|{name_short}'
|
|
566
|
-
|
|
567
880
|
if isinstance(self.element.initial_charge_state, str):
|
|
568
|
-
self.
|
|
569
|
-
self.
|
|
570
|
-
self.charge_state.isel(time=0) == self.charge_state.isel(time=-1), name=name
|
|
571
|
-
),
|
|
572
|
-
name_short,
|
|
881
|
+
self.add_constraints(
|
|
882
|
+
self.charge_state.isel(time=0) == self.charge_state.isel(time=-1), short_name='initial_charge_state'
|
|
573
883
|
)
|
|
574
884
|
else:
|
|
575
|
-
self.
|
|
576
|
-
self.
|
|
577
|
-
|
|
578
|
-
),
|
|
579
|
-
name_short,
|
|
885
|
+
self.add_constraints(
|
|
886
|
+
self.charge_state.isel(time=0) == self.element.initial_charge_state,
|
|
887
|
+
short_name='initial_charge_state',
|
|
580
888
|
)
|
|
581
889
|
|
|
582
890
|
if self.element.maximal_final_charge_state is not None:
|
|
583
|
-
self.
|
|
584
|
-
self.
|
|
585
|
-
|
|
586
|
-
name=f'{self.label_full}|final_charge_max',
|
|
587
|
-
),
|
|
588
|
-
'final_charge_max',
|
|
891
|
+
self.add_constraints(
|
|
892
|
+
self.charge_state.isel(time=-1) <= self.element.maximal_final_charge_state,
|
|
893
|
+
short_name='final_charge_max',
|
|
589
894
|
)
|
|
590
895
|
|
|
591
896
|
if self.element.minimal_final_charge_state is not None:
|
|
592
|
-
self.
|
|
593
|
-
self.
|
|
594
|
-
|
|
595
|
-
name=f'{self.label_full}|final_charge_min',
|
|
596
|
-
),
|
|
597
|
-
'final_charge_min',
|
|
897
|
+
self.add_constraints(
|
|
898
|
+
self.charge_state.isel(time=-1) >= self.element.minimal_final_charge_state,
|
|
899
|
+
short_name='final_charge_min',
|
|
598
900
|
)
|
|
599
901
|
|
|
600
902
|
@property
|
|
601
|
-
def
|
|
602
|
-
relative_lower_bound, relative_upper_bound = self.
|
|
903
|
+
def _absolute_charge_state_bounds(self) -> tuple[TemporalData, TemporalData]:
|
|
904
|
+
relative_lower_bound, relative_upper_bound = self._relative_charge_state_bounds
|
|
603
905
|
if not isinstance(self.element.capacity_in_flow_hours, InvestParameters):
|
|
604
906
|
return (
|
|
605
907
|
relative_lower_bound * self.element.capacity_in_flow_hours,
|
|
@@ -612,68 +914,429 @@ class StorageModel(ComponentModel):
|
|
|
612
914
|
)
|
|
613
915
|
|
|
614
916
|
@property
|
|
615
|
-
def
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
917
|
+
def _relative_charge_state_bounds(self) -> tuple[xr.DataArray, xr.DataArray]:
|
|
918
|
+
"""
|
|
919
|
+
Get relative charge state bounds with final timestep values.
|
|
920
|
+
|
|
921
|
+
Returns:
|
|
922
|
+
Tuple of (minimum_bounds, maximum_bounds) DataArrays extending to final timestep
|
|
923
|
+
"""
|
|
924
|
+
final_coords = {'time': [self._model.flow_system.timesteps_extra[-1]]}
|
|
925
|
+
|
|
926
|
+
# Get final minimum charge state
|
|
927
|
+
if self.element.relative_minimum_final_charge_state is None:
|
|
928
|
+
min_final = self.element.relative_minimum_charge_state.isel(time=-1, drop=True)
|
|
929
|
+
else:
|
|
930
|
+
min_final = self.element.relative_minimum_final_charge_state
|
|
931
|
+
min_final = min_final.expand_dims('time').assign_coords(time=final_coords['time'])
|
|
932
|
+
|
|
933
|
+
# Get final maximum charge state
|
|
934
|
+
if self.element.relative_maximum_final_charge_state is None:
|
|
935
|
+
max_final = self.element.relative_maximum_charge_state.isel(time=-1, drop=True)
|
|
936
|
+
else:
|
|
937
|
+
max_final = self.element.relative_maximum_final_charge_state
|
|
938
|
+
max_final = max_final.expand_dims('time').assign_coords(time=final_coords['time'])
|
|
939
|
+
# Concatenate with original bounds
|
|
940
|
+
min_bounds = xr.concat([self.element.relative_minimum_charge_state, min_final], dim='time')
|
|
941
|
+
max_bounds = xr.concat([self.element.relative_maximum_charge_state, max_final], dim='time')
|
|
942
|
+
|
|
943
|
+
return min_bounds, max_bounds
|
|
944
|
+
|
|
945
|
+
@property
|
|
946
|
+
def _investment(self) -> InvestmentModel | None:
|
|
947
|
+
"""Deprecated alias for investment"""
|
|
948
|
+
return self.investment
|
|
949
|
+
|
|
950
|
+
@property
|
|
951
|
+
def investment(self) -> InvestmentModel | None:
|
|
952
|
+
"""OnOff feature"""
|
|
953
|
+
if 'investment' not in self.submodels:
|
|
954
|
+
return None
|
|
955
|
+
return self.submodels['investment']
|
|
956
|
+
|
|
957
|
+
@property
|
|
958
|
+
def charge_state(self) -> linopy.Variable:
|
|
959
|
+
"""Charge state variable"""
|
|
960
|
+
return self['charge_state']
|
|
961
|
+
|
|
962
|
+
@property
|
|
963
|
+
def netto_discharge(self) -> linopy.Variable:
|
|
964
|
+
"""Netto discharge variable"""
|
|
965
|
+
return self['netto_discharge']
|
|
620
966
|
|
|
621
967
|
|
|
622
968
|
@register_class_for_io
|
|
623
969
|
class SourceAndSink(Component):
|
|
624
970
|
"""
|
|
625
|
-
|
|
971
|
+
A SourceAndSink combines both supply and demand capabilities in a single component.
|
|
972
|
+
|
|
973
|
+
SourceAndSink components can both consume AND provide energy or material flows
|
|
974
|
+
from and to the system, making them ideal for modeling markets, (simple) storage facilities,
|
|
975
|
+
or bidirectional grid connections where buying and selling occur at the same location.
|
|
976
|
+
|
|
977
|
+
Args:
|
|
978
|
+
label: The label of the Element. Used to identify it in the FlowSystem.
|
|
979
|
+
inputs: Input-flows into the SourceAndSink representing consumption/demand side.
|
|
980
|
+
outputs: Output-flows from the SourceAndSink representing supply/generation side.
|
|
981
|
+
prevent_simultaneous_flow_rates: If True, prevents simultaneous input and output
|
|
982
|
+
flows. This enforces that the component operates either as a source OR sink
|
|
983
|
+
at any given time, but not both simultaneously. Default is True.
|
|
984
|
+
meta_data: Used to store additional information about the Element. Not used
|
|
985
|
+
internally but saved in results. Only use Python native types.
|
|
986
|
+
|
|
987
|
+
Examples:
|
|
988
|
+
Electricity market connection (buy/sell to grid):
|
|
989
|
+
|
|
990
|
+
```python
|
|
991
|
+
electricity_market = SourceAndSink(
|
|
992
|
+
label='grid_connection',
|
|
993
|
+
inputs=[electricity_purchase], # Buy from grid
|
|
994
|
+
outputs=[electricity_sale], # Sell to grid
|
|
995
|
+
prevent_simultaneous_flow_rates=True, # Can't buy and sell simultaneously
|
|
996
|
+
)
|
|
997
|
+
```
|
|
998
|
+
|
|
999
|
+
Natural gas storage facility:
|
|
1000
|
+
|
|
1001
|
+
```python
|
|
1002
|
+
gas_storage_facility = SourceAndSink(
|
|
1003
|
+
label='underground_gas_storage',
|
|
1004
|
+
inputs=[gas_injection_flow], # Inject gas into storage
|
|
1005
|
+
outputs=[gas_withdrawal_flow], # Withdraw gas from storage
|
|
1006
|
+
prevent_simultaneous_flow_rates=True, # Injection or withdrawal, not both
|
|
1007
|
+
)
|
|
1008
|
+
```
|
|
1009
|
+
|
|
1010
|
+
District heating network connection:
|
|
1011
|
+
|
|
1012
|
+
```python
|
|
1013
|
+
dh_connection = SourceAndSink(
|
|
1014
|
+
label='district_heating_tie',
|
|
1015
|
+
inputs=[heat_purchase_flow], # Purchase heat from network
|
|
1016
|
+
outputs=[heat_sale_flow], # Sell excess heat to network
|
|
1017
|
+
prevent_simultaneous_flow_rates=False, # May allow simultaneous flows
|
|
1018
|
+
)
|
|
1019
|
+
```
|
|
1020
|
+
|
|
1021
|
+
Industrial waste heat exchange:
|
|
1022
|
+
|
|
1023
|
+
```python
|
|
1024
|
+
waste_heat_exchange = SourceAndSink(
|
|
1025
|
+
label='industrial_heat_hub',
|
|
1026
|
+
inputs=[
|
|
1027
|
+
waste_heat_input_a, # Receive waste heat from process A
|
|
1028
|
+
waste_heat_input_b, # Receive waste heat from process B
|
|
1029
|
+
],
|
|
1030
|
+
outputs=[
|
|
1031
|
+
useful_heat_supply_c, # Supply heat to process C
|
|
1032
|
+
useful_heat_supply_d, # Supply heat to process D
|
|
1033
|
+
],
|
|
1034
|
+
prevent_simultaneous_flow_rates=False, # Multiple simultaneous flows allowed
|
|
1035
|
+
)
|
|
1036
|
+
```
|
|
1037
|
+
|
|
1038
|
+
Note:
|
|
1039
|
+
When prevent_simultaneous_flow_rates is True, binary variables are created to
|
|
1040
|
+
ensure mutually exclusive operation between input and output flows, which
|
|
1041
|
+
increases computational complexity but reflects realistic market or storage
|
|
1042
|
+
operation constraints.
|
|
1043
|
+
|
|
1044
|
+
SourceAndSink is particularly useful for modeling:
|
|
1045
|
+
- Energy markets with bidirectional trading
|
|
1046
|
+
- Storage facilities with injection/withdrawal operations
|
|
1047
|
+
- Grid tie points with import/export capabilities
|
|
1048
|
+
- Waste exchange networks with multiple participants
|
|
1049
|
+
|
|
1050
|
+
Deprecated:
|
|
1051
|
+
The deprecated `sink` and `source` kwargs are accepted for compatibility but will be removed in future releases.
|
|
626
1052
|
"""
|
|
627
1053
|
|
|
628
1054
|
def __init__(
|
|
629
1055
|
self,
|
|
630
1056
|
label: str,
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
meta_data:
|
|
1057
|
+
inputs: list[Flow] | None = None,
|
|
1058
|
+
outputs: list[Flow] | None = None,
|
|
1059
|
+
prevent_simultaneous_flow_rates: bool = True,
|
|
1060
|
+
meta_data: dict | None = None,
|
|
1061
|
+
**kwargs,
|
|
635
1062
|
):
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
prevent_simultaneous_sink_and_source
|
|
642
|
-
|
|
643
|
-
|
|
1063
|
+
# Handle deprecated parameters using centralized helper
|
|
1064
|
+
outputs = self._handle_deprecated_kwarg(kwargs, 'source', 'outputs', outputs, transform=lambda x: [x])
|
|
1065
|
+
inputs = self._handle_deprecated_kwarg(kwargs, 'sink', 'inputs', inputs, transform=lambda x: [x])
|
|
1066
|
+
prevent_simultaneous_flow_rates = self._handle_deprecated_kwarg(
|
|
1067
|
+
kwargs,
|
|
1068
|
+
'prevent_simultaneous_sink_and_source',
|
|
1069
|
+
'prevent_simultaneous_flow_rates',
|
|
1070
|
+
prevent_simultaneous_flow_rates,
|
|
1071
|
+
check_conflict=False,
|
|
1072
|
+
)
|
|
1073
|
+
|
|
1074
|
+
# Validate any remaining unexpected kwargs
|
|
1075
|
+
self._validate_kwargs(kwargs)
|
|
1076
|
+
|
|
644
1077
|
super().__init__(
|
|
645
1078
|
label,
|
|
646
|
-
inputs=
|
|
647
|
-
outputs=
|
|
648
|
-
prevent_simultaneous_flows=[
|
|
1079
|
+
inputs=inputs,
|
|
1080
|
+
outputs=outputs,
|
|
1081
|
+
prevent_simultaneous_flows=(inputs or []) + (outputs or []) if prevent_simultaneous_flow_rates else None,
|
|
649
1082
|
meta_data=meta_data,
|
|
650
1083
|
)
|
|
651
|
-
self.
|
|
652
|
-
|
|
653
|
-
|
|
1084
|
+
self.prevent_simultaneous_flow_rates = prevent_simultaneous_flow_rates
|
|
1085
|
+
|
|
1086
|
+
@property
|
|
1087
|
+
def source(self) -> Flow:
|
|
1088
|
+
warnings.warn(
|
|
1089
|
+
'The source property is deprecated. Use the outputs property instead.',
|
|
1090
|
+
DeprecationWarning,
|
|
1091
|
+
stacklevel=2,
|
|
1092
|
+
)
|
|
1093
|
+
return self.outputs[0]
|
|
1094
|
+
|
|
1095
|
+
@property
|
|
1096
|
+
def sink(self) -> Flow:
|
|
1097
|
+
warnings.warn(
|
|
1098
|
+
'The sink property is deprecated. Use the inputs property instead.',
|
|
1099
|
+
DeprecationWarning,
|
|
1100
|
+
stacklevel=2,
|
|
1101
|
+
)
|
|
1102
|
+
return self.inputs[0]
|
|
1103
|
+
|
|
1104
|
+
@property
|
|
1105
|
+
def prevent_simultaneous_sink_and_source(self) -> bool:
|
|
1106
|
+
warnings.warn(
|
|
1107
|
+
'The prevent_simultaneous_sink_and_source property is deprecated. Use the prevent_simultaneous_flow_rates property instead.',
|
|
1108
|
+
DeprecationWarning,
|
|
1109
|
+
stacklevel=2,
|
|
1110
|
+
)
|
|
1111
|
+
return self.prevent_simultaneous_flow_rates
|
|
654
1112
|
|
|
655
1113
|
|
|
656
1114
|
@register_class_for_io
|
|
657
1115
|
class Source(Component):
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
1116
|
+
"""
|
|
1117
|
+
A Source generates or provides energy or material flows into the system.
|
|
1118
|
+
|
|
1119
|
+
Sources represent supply points like power plants, fuel suppliers, renewable
|
|
1120
|
+
energy sources, or any system boundary where flows originate. They provide
|
|
1121
|
+
unlimited supply capability subject to flow constraints, demand patterns and effects.
|
|
1122
|
+
|
|
1123
|
+
Args:
|
|
1124
|
+
label: The label of the Element. Used to identify it in the FlowSystem.
|
|
1125
|
+
outputs: Output-flows from the source. Can be single flow or list of flows
|
|
1126
|
+
for sources providing multiple commodities or services.
|
|
1127
|
+
meta_data: Used to store additional information about the Element. Not used
|
|
1128
|
+
internally but saved in results. Only use Python native types.
|
|
1129
|
+
prevent_simultaneous_flow_rates: If True, only one output flow can be active
|
|
1130
|
+
at a time. Useful for modeling mutually exclusive supply options. Default is False.
|
|
1131
|
+
|
|
1132
|
+
Examples:
|
|
1133
|
+
Simple electricity grid connection:
|
|
1134
|
+
|
|
1135
|
+
```python
|
|
1136
|
+
grid_source = Source(label='electrical_grid', outputs=[grid_electricity_flow])
|
|
1137
|
+
```
|
|
1138
|
+
|
|
1139
|
+
Natural gas supply with cost and capacity constraints:
|
|
1140
|
+
|
|
1141
|
+
```python
|
|
1142
|
+
gas_supply = Source(
|
|
1143
|
+
label='gas_network',
|
|
1144
|
+
outputs=[
|
|
1145
|
+
Flow(
|
|
1146
|
+
label='natural_gas_flow',
|
|
1147
|
+
bus=gas_bus,
|
|
1148
|
+
size=1000, # Maximum 1000 kW supply capacity
|
|
1149
|
+
effects_per_flow_hour={'cost': 0.04}, # €0.04/kWh gas cost
|
|
1150
|
+
)
|
|
1151
|
+
],
|
|
1152
|
+
)
|
|
1153
|
+
```
|
|
1154
|
+
|
|
1155
|
+
Multi-fuel power plant with switching constraints:
|
|
1156
|
+
|
|
1157
|
+
```python
|
|
1158
|
+
multi_fuel_plant = Source(
|
|
1159
|
+
label='flexible_generator',
|
|
1160
|
+
outputs=[coal_electricity, gas_electricity, biomass_electricity],
|
|
1161
|
+
prevent_simultaneous_flow_rates=True, # Can only use one fuel at a time
|
|
1162
|
+
)
|
|
1163
|
+
```
|
|
1164
|
+
|
|
1165
|
+
Renewable energy source with investment optimization:
|
|
1166
|
+
|
|
1167
|
+
```python
|
|
1168
|
+
solar_farm = Source(
|
|
1169
|
+
label='solar_pv',
|
|
1170
|
+
outputs=[
|
|
1171
|
+
Flow(
|
|
1172
|
+
label='solar_power',
|
|
1173
|
+
bus=electricity_bus,
|
|
1174
|
+
size=InvestParameters(
|
|
1175
|
+
minimum_size=0,
|
|
1176
|
+
maximum_size=50000, # Up to 50 MW
|
|
1177
|
+
specific_effects={'cost': 800}, # €800/kW installed
|
|
1178
|
+
fix_effects={'cost': 100000}, # €100k development costs
|
|
1179
|
+
),
|
|
1180
|
+
fixed_relative_profile=solar_profile, # Hourly generation profile
|
|
1181
|
+
)
|
|
1182
|
+
],
|
|
1183
|
+
)
|
|
1184
|
+
```
|
|
1185
|
+
|
|
1186
|
+
Deprecated:
|
|
1187
|
+
The deprecated `source` kwarg is accepted for compatibility but will be removed in future releases.
|
|
1188
|
+
"""
|
|
1189
|
+
|
|
1190
|
+
def __init__(
|
|
1191
|
+
self,
|
|
1192
|
+
label: str,
|
|
1193
|
+
outputs: list[Flow] | None = None,
|
|
1194
|
+
meta_data: dict | None = None,
|
|
1195
|
+
prevent_simultaneous_flow_rates: bool = False,
|
|
1196
|
+
**kwargs,
|
|
1197
|
+
):
|
|
1198
|
+
# Handle deprecated parameter using centralized helper
|
|
1199
|
+
outputs = self._handle_deprecated_kwarg(kwargs, 'source', 'outputs', outputs, transform=lambda x: [x])
|
|
1200
|
+
|
|
1201
|
+
# Validate any remaining unexpected kwargs
|
|
1202
|
+
self._validate_kwargs(kwargs)
|
|
1203
|
+
|
|
1204
|
+
self.prevent_simultaneous_flow_rates = prevent_simultaneous_flow_rates
|
|
1205
|
+
super().__init__(
|
|
1206
|
+
label,
|
|
1207
|
+
outputs=outputs,
|
|
1208
|
+
meta_data=meta_data,
|
|
1209
|
+
prevent_simultaneous_flows=outputs if prevent_simultaneous_flow_rates else None,
|
|
1210
|
+
)
|
|
1211
|
+
|
|
1212
|
+
@property
|
|
1213
|
+
def source(self) -> Flow:
|
|
1214
|
+
warnings.warn(
|
|
1215
|
+
'The source property is deprecated. Use the outputs property instead.',
|
|
1216
|
+
DeprecationWarning,
|
|
1217
|
+
stacklevel=2,
|
|
1218
|
+
)
|
|
1219
|
+
return self.outputs[0]
|
|
667
1220
|
|
|
668
1221
|
|
|
669
1222
|
@register_class_for_io
|
|
670
1223
|
class Sink(Component):
|
|
671
|
-
|
|
1224
|
+
"""
|
|
1225
|
+
A Sink consumes energy or material flows from the system.
|
|
1226
|
+
|
|
1227
|
+
Sinks represent demand points like electrical loads, heat demands, material
|
|
1228
|
+
consumption, or any system boundary where flows terminate. They provide
|
|
1229
|
+
unlimited consumption capability subject to flow constraints, demand patterns and effects.
|
|
1230
|
+
|
|
1231
|
+
Args:
|
|
1232
|
+
label: The label of the Element. Used to identify it in the FlowSystem.
|
|
1233
|
+
inputs: Input-flows into the sink. Can be single flow or list of flows
|
|
1234
|
+
for sinks consuming multiple commodities or services.
|
|
1235
|
+
meta_data: Used to store additional information about the Element. Not used
|
|
1236
|
+
internally but saved in results. Only use Python native types.
|
|
1237
|
+
prevent_simultaneous_flow_rates: If True, only one input flow can be active
|
|
1238
|
+
at a time. Useful for modeling mutually exclusive consumption options. Default is False.
|
|
1239
|
+
|
|
1240
|
+
Examples:
|
|
1241
|
+
Simple electrical demand:
|
|
1242
|
+
|
|
1243
|
+
```python
|
|
1244
|
+
electrical_load = Sink(label='building_load', inputs=[electricity_demand_flow])
|
|
1245
|
+
```
|
|
1246
|
+
|
|
1247
|
+
Heat demand with time-varying profile:
|
|
1248
|
+
|
|
1249
|
+
```python
|
|
1250
|
+
heat_demand = Sink(
|
|
1251
|
+
label='district_heating_load',
|
|
1252
|
+
inputs=[
|
|
1253
|
+
Flow(
|
|
1254
|
+
label='heat_consumption',
|
|
1255
|
+
bus=heat_bus,
|
|
1256
|
+
fixed_relative_profile=hourly_heat_profile, # Demand profile
|
|
1257
|
+
size=2000, # Peak demand of 2000 kW
|
|
1258
|
+
)
|
|
1259
|
+
],
|
|
1260
|
+
)
|
|
1261
|
+
```
|
|
1262
|
+
|
|
1263
|
+
Multi-energy building with switching capabilities:
|
|
1264
|
+
|
|
1265
|
+
```python
|
|
1266
|
+
flexible_building = Sink(
|
|
1267
|
+
label='smart_building',
|
|
1268
|
+
inputs=[electricity_heating, gas_heating, heat_pump_heating],
|
|
1269
|
+
prevent_simultaneous_flow_rates=True, # Can only use one heating mode
|
|
1270
|
+
)
|
|
1271
|
+
```
|
|
1272
|
+
|
|
1273
|
+
Industrial process with variable demand:
|
|
1274
|
+
|
|
1275
|
+
```python
|
|
1276
|
+
factory_load = Sink(
|
|
1277
|
+
label='manufacturing_plant',
|
|
1278
|
+
inputs=[
|
|
1279
|
+
Flow(
|
|
1280
|
+
label='electricity_process',
|
|
1281
|
+
bus=electricity_bus,
|
|
1282
|
+
size=5000, # Base electrical load
|
|
1283
|
+
effects_per_flow_hour={'cost': -0.1}, # Value of service (negative cost)
|
|
1284
|
+
),
|
|
1285
|
+
Flow(
|
|
1286
|
+
label='steam_process',
|
|
1287
|
+
bus=steam_bus,
|
|
1288
|
+
size=3000, # Process steam demand
|
|
1289
|
+
fixed_relative_profile=production_schedule,
|
|
1290
|
+
),
|
|
1291
|
+
],
|
|
1292
|
+
)
|
|
1293
|
+
```
|
|
1294
|
+
|
|
1295
|
+
Deprecated:
|
|
1296
|
+
The deprecated `sink` kwarg is accepted for compatibility but will be removed in future releases.
|
|
1297
|
+
"""
|
|
1298
|
+
|
|
1299
|
+
def __init__(
|
|
1300
|
+
self,
|
|
1301
|
+
label: str,
|
|
1302
|
+
inputs: list[Flow] | None = None,
|
|
1303
|
+
meta_data: dict | None = None,
|
|
1304
|
+
prevent_simultaneous_flow_rates: bool = False,
|
|
1305
|
+
**kwargs,
|
|
1306
|
+
):
|
|
672
1307
|
"""
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
1308
|
+
Initialize a Sink (consumes flow from the system).
|
|
1309
|
+
|
|
1310
|
+
Supports legacy `sink=` keyword for backward compatibility (deprecated): if `sink` is provided it is used as the single input flow and a DeprecationWarning is issued; specifying both `inputs` and `sink` raises ValueError.
|
|
1311
|
+
|
|
1312
|
+
Parameters:
|
|
1313
|
+
label (str): Unique element label.
|
|
1314
|
+
inputs (list[Flow], optional): Input flows for the sink.
|
|
1315
|
+
meta_data (dict, optional): Arbitrary metadata attached to the element.
|
|
1316
|
+
prevent_simultaneous_flow_rates (bool, optional): If True, prevents simultaneous nonzero flow rates across the element's inputs by wiring that restriction into the base Component setup.
|
|
1317
|
+
|
|
1318
|
+
Note:
|
|
1319
|
+
The deprecated `sink` kwarg is accepted for compatibility but will be removed in future releases.
|
|
677
1320
|
"""
|
|
678
|
-
|
|
679
|
-
self.sink =
|
|
1321
|
+
# Handle deprecated parameter using centralized helper
|
|
1322
|
+
inputs = self._handle_deprecated_kwarg(kwargs, 'sink', 'inputs', inputs, transform=lambda x: [x])
|
|
1323
|
+
|
|
1324
|
+
# Validate any remaining unexpected kwargs
|
|
1325
|
+
self._validate_kwargs(kwargs)
|
|
1326
|
+
|
|
1327
|
+
self.prevent_simultaneous_flow_rates = prevent_simultaneous_flow_rates
|
|
1328
|
+
super().__init__(
|
|
1329
|
+
label,
|
|
1330
|
+
inputs=inputs,
|
|
1331
|
+
meta_data=meta_data,
|
|
1332
|
+
prevent_simultaneous_flows=inputs if prevent_simultaneous_flow_rates else None,
|
|
1333
|
+
)
|
|
1334
|
+
|
|
1335
|
+
@property
|
|
1336
|
+
def sink(self) -> Flow:
|
|
1337
|
+
warnings.warn(
|
|
1338
|
+
'The sink property is deprecated. Use the inputs property instead.',
|
|
1339
|
+
DeprecationWarning,
|
|
1340
|
+
stacklevel=2,
|
|
1341
|
+
)
|
|
1342
|
+
return self.inputs[0]
|