flixopt 2.2.0rc2__py3-none-any.whl → 3.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.
- flixopt/__init__.py +33 -4
- flixopt/aggregation.py +60 -80
- flixopt/calculation.py +403 -182
- flixopt/commons.py +1 -10
- flixopt/components.py +939 -448
- flixopt/config.py +553 -191
- flixopt/core.py +513 -846
- flixopt/effects.py +644 -178
- flixopt/elements.py +610 -355
- flixopt/features.py +394 -966
- flixopt/flow_system.py +736 -219
- flixopt/interface.py +1104 -302
- flixopt/io.py +103 -79
- flixopt/linear_converters.py +387 -95
- flixopt/modeling.py +757 -0
- flixopt/network_app.py +73 -39
- flixopt/plotting.py +294 -138
- flixopt/results.py +1254 -300
- flixopt/solvers.py +25 -21
- flixopt/structure.py +938 -396
- flixopt/utils.py +36 -12
- flixopt-3.0.1.dist-info/METADATA +209 -0
- flixopt-3.0.1.dist-info/RECORD +26 -0
- flixopt-3.0.1.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 -61
- 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/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/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.0rc2.dist-info/METADATA +0 -167
- flixopt-2.2.0rc2.dist-info/RECORD +0 -54
- flixopt-2.2.0rc2.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/extract_release_notes.py +0 -45
- scripts/gen_ref_pages.py +0 -54
- tests/ressources/Zeitreihen2020.csv +0 -35137
- {flixopt-2.2.0rc2.dist-info → flixopt-3.0.1.dist-info}/WHEEL +0 -0
- {flixopt-2.2.0rc2.dist-info → flixopt-3.0.1.dist-info}/licenses/LICENSE +0 -0
flixopt/components.py
CHANGED
|
@@ -2,21 +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
|
|
7
|
-
from typing import TYPE_CHECKING,
|
|
9
|
+
from typing import TYPE_CHECKING, Literal
|
|
8
10
|
|
|
9
|
-
import linopy
|
|
10
11
|
import numpy as np
|
|
12
|
+
import xarray as xr
|
|
11
13
|
|
|
12
|
-
from . import
|
|
13
|
-
from .core import NumericData, NumericDataTS, PlausibilityError, Scalar, TimeSeries
|
|
14
|
+
from .core import PeriodicDataUser, PlausibilityError, TemporalData, TemporalDataUser
|
|
14
15
|
from .elements import Component, ComponentModel, Flow
|
|
15
|
-
from .features import InvestmentModel,
|
|
16
|
+
from .features import InvestmentModel, PiecewiseModel
|
|
16
17
|
from .interface import InvestParameters, OnOffParameters, PiecewiseConversion
|
|
17
|
-
from .
|
|
18
|
+
from .modeling import BoundingPatterns
|
|
19
|
+
from .structure import FlowSystemModel, register_class_for_io
|
|
18
20
|
|
|
19
21
|
if TYPE_CHECKING:
|
|
22
|
+
import linopy
|
|
23
|
+
|
|
20
24
|
from .flow_system import FlowSystem
|
|
21
25
|
|
|
22
26
|
logger = logging.getLogger('flixopt')
|
|
@@ -24,82 +28,114 @@ logger = logging.getLogger('flixopt')
|
|
|
24
28
|
|
|
25
29
|
@register_class_for_io
|
|
26
30
|
class LinearConverter(Component):
|
|
27
|
-
"""
|
|
31
|
+
"""
|
|
32
|
+
Converts input-Flows into output-Flows via linear conversion factors.
|
|
28
33
|
|
|
29
|
-
|
|
30
|
-
|
|
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.
|
|
31
38
|
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
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)
|
|
37
46
|
|
|
38
47
|
Args:
|
|
39
|
-
label:
|
|
40
|
-
inputs:
|
|
41
|
-
outputs:
|
|
42
|
-
on_off_parameters:
|
|
43
|
-
|
|
44
|
-
all
|
|
45
|
-
|
|
46
|
-
conversion_factors: Linear
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
This prevents unintended zero flows and maintains mathematical consistency.
|
|
59
|
-
|
|
60
|
-
To allow zero flow rates:
|
|
61
|
-
|
|
62
|
-
- Add `on_off_parameters` to enable complete shutdown, or
|
|
63
|
-
- Include explicit zero pieces in your `piecewise_conversion` definition
|
|
64
|
-
|
|
65
|
-
This behavior was clarified in v2.1.7 to prevent optimization edge cases.
|
|
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.
|
|
66
67
|
|
|
67
68
|
Examples:
|
|
68
|
-
Simple heat
|
|
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:
|
|
69
81
|
|
|
70
82
|
```python
|
|
71
|
-
heat_pump =
|
|
72
|
-
label='
|
|
73
|
-
inputs=[
|
|
74
|
-
outputs=[
|
|
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],
|
|
75
98
|
conversion_factors=[
|
|
76
|
-
{
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
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
|
|
80
116
|
],
|
|
81
117
|
)
|
|
82
118
|
```
|
|
83
119
|
|
|
84
|
-
|
|
120
|
+
Complex converter with piecewise efficiency:
|
|
85
121
|
|
|
86
122
|
```python
|
|
87
|
-
|
|
88
|
-
label='
|
|
89
|
-
inputs=[
|
|
90
|
-
outputs=[
|
|
91
|
-
piecewise_conversion=
|
|
123
|
+
variable_efficiency_converter = LinearConverter(
|
|
124
|
+
label='variable_converter',
|
|
125
|
+
inputs=[fuel_in],
|
|
126
|
+
outputs=[power_out],
|
|
127
|
+
piecewise_conversion=PiecewiseConversion(
|
|
92
128
|
{
|
|
93
|
-
'
|
|
129
|
+
'fuel_in': Piecewise(
|
|
94
130
|
[
|
|
95
|
-
|
|
96
|
-
|
|
131
|
+
Piece(0, 10), # Low load operation
|
|
132
|
+
Piece(10, 25), # High load operation
|
|
97
133
|
]
|
|
98
134
|
),
|
|
99
|
-
'
|
|
135
|
+
'power_out': Piecewise(
|
|
100
136
|
[
|
|
101
|
-
|
|
102
|
-
|
|
137
|
+
Piece(0, 3.5), # Lower efficiency at part load
|
|
138
|
+
Piece(3.5, 10), # Higher efficiency at full load
|
|
103
139
|
]
|
|
104
140
|
),
|
|
105
141
|
}
|
|
@@ -107,56 +143,41 @@ class LinearConverter(Component):
|
|
|
107
143
|
)
|
|
108
144
|
```
|
|
109
145
|
|
|
110
|
-
|
|
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.
|
|
111
154
|
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
conversion_factors=[
|
|
118
|
-
{
|
|
119
|
-
'natural_gas_flow': 1.0, # 1 MW fuel input
|
|
120
|
-
'electricity_flow': 0.4, # 40% electrical efficiency
|
|
121
|
-
'heat_flow': 0.45, # 45% thermal efficiency
|
|
122
|
-
}
|
|
123
|
-
],
|
|
124
|
-
on_off_parameters=fx.OnOffParameters(
|
|
125
|
-
min_on_hours=4, # Minimum 4-hour operation
|
|
126
|
-
min_off_hours=2, # Minimum 2-hour downtime
|
|
127
|
-
),
|
|
128
|
-
)
|
|
129
|
-
```
|
|
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.
|
|
130
160
|
|
|
131
|
-
Note:
|
|
132
|
-
Either `conversion_factors` or `piecewise_conversion` must be specified, but not both.
|
|
133
|
-
The component automatically handles the mathematical relationships between all
|
|
134
|
-
connected flows according to the specified conversion ratios.
|
|
135
|
-
|
|
136
|
-
See Also:
|
|
137
|
-
PiecewiseConversion: For variable efficiency modeling
|
|
138
|
-
OnOffParameters: For binary on/off control
|
|
139
|
-
Flow: Input and output flow definitions
|
|
140
161
|
"""
|
|
141
162
|
|
|
142
163
|
def __init__(
|
|
143
164
|
self,
|
|
144
165
|
label: str,
|
|
145
|
-
inputs:
|
|
146
|
-
outputs:
|
|
147
|
-
on_off_parameters: OnOffParameters = None,
|
|
148
|
-
conversion_factors:
|
|
149
|
-
piecewise_conversion:
|
|
150
|
-
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,
|
|
151
172
|
):
|
|
152
173
|
super().__init__(label, inputs, outputs, on_off_parameters, meta_data=meta_data)
|
|
153
174
|
self.conversion_factors = conversion_factors or []
|
|
154
175
|
self.piecewise_conversion = piecewise_conversion
|
|
155
176
|
|
|
156
|
-
def create_model(self, model:
|
|
177
|
+
def create_model(self, model: FlowSystemModel) -> LinearConverterModel:
|
|
157
178
|
self._plausibility_checks()
|
|
158
|
-
self.
|
|
159
|
-
return self.
|
|
179
|
+
self.submodel = LinearConverterModel(model, self)
|
|
180
|
+
return self.submodel
|
|
160
181
|
|
|
161
182
|
def _plausibility_checks(self) -> None:
|
|
162
183
|
super()._plausibility_checks()
|
|
@@ -182,28 +203,32 @@ class LinearConverter(Component):
|
|
|
182
203
|
if self.piecewise_conversion:
|
|
183
204
|
for flow in self.flows.values():
|
|
184
205
|
if isinstance(flow.size, InvestParameters) and flow.size.fixed_size is None:
|
|
185
|
-
|
|
186
|
-
f'
|
|
187
|
-
f'
|
|
206
|
+
logger.warning(
|
|
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}).'
|
|
188
210
|
)
|
|
189
211
|
|
|
190
|
-
def transform_data(self, flow_system:
|
|
191
|
-
|
|
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)
|
|
192
215
|
if self.conversion_factors:
|
|
193
216
|
self.conversion_factors = self._transform_conversion_factors(flow_system)
|
|
194
217
|
if self.piecewise_conversion:
|
|
195
|
-
self.piecewise_conversion.
|
|
218
|
+
self.piecewise_conversion.has_time_dim = True
|
|
219
|
+
self.piecewise_conversion.transform_data(flow_system, f'{prefix}|PiecewiseConversion')
|
|
196
220
|
|
|
197
|
-
def _transform_conversion_factors(self, flow_system:
|
|
198
|
-
"""
|
|
221
|
+
def _transform_conversion_factors(self, flow_system: FlowSystem) -> list[dict[str, xr.DataArray]]:
|
|
222
|
+
"""Converts all conversion factors to internal datatypes"""
|
|
199
223
|
list_of_conversion_factors = []
|
|
200
224
|
for idx, conversion_factor in enumerate(self.conversion_factors):
|
|
201
225
|
transformed_dict = {}
|
|
202
226
|
for flow, values in conversion_factor.items():
|
|
203
227
|
# TODO: Might be better to use the label of the component instead of the flow
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
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
|
|
207
232
|
list_of_conversion_factors.append(transformed_dict)
|
|
208
233
|
return list_of_conversion_factors
|
|
209
234
|
|
|
@@ -215,7 +240,140 @@ class LinearConverter(Component):
|
|
|
215
240
|
@register_class_for_io
|
|
216
241
|
class Storage(Component):
|
|
217
242
|
"""
|
|
218
|
-
|
|
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.
|
|
219
377
|
"""
|
|
220
378
|
|
|
221
379
|
def __init__(
|
|
@@ -223,43 +381,21 @@ class Storage(Component):
|
|
|
223
381
|
label: str,
|
|
224
382
|
charging: Flow,
|
|
225
383
|
discharging: Flow,
|
|
226
|
-
capacity_in_flow_hours:
|
|
227
|
-
relative_minimum_charge_state:
|
|
228
|
-
relative_maximum_charge_state:
|
|
229
|
-
initial_charge_state:
|
|
230
|
-
minimal_final_charge_state:
|
|
231
|
-
maximal_final_charge_state:
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
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,
|
|
235
395
|
prevent_simultaneous_charge_and_discharge: bool = True,
|
|
236
|
-
|
|
396
|
+
balanced: bool = False,
|
|
397
|
+
meta_data: dict | None = None,
|
|
237
398
|
):
|
|
238
|
-
"""
|
|
239
|
-
Storages have one incoming and one outgoing Flow each with an efficiency.
|
|
240
|
-
Further, storages have a `size` and a `charge_state`.
|
|
241
|
-
Similarly to the flow-rate of a Flow, the `size` combined with a relative upper and lower bound
|
|
242
|
-
limits the `charge_state` of the storage.
|
|
243
|
-
|
|
244
|
-
For mathematical details take a look at our online documentation
|
|
245
|
-
|
|
246
|
-
Args:
|
|
247
|
-
label: The label of the Element. Used to identify it in the FlowSystem
|
|
248
|
-
charging: ingoing flow.
|
|
249
|
-
discharging: outgoing flow.
|
|
250
|
-
capacity_in_flow_hours: nominal capacity/size of the storage
|
|
251
|
-
relative_minimum_charge_state: minimum relative charge state. The default is 0.
|
|
252
|
-
relative_maximum_charge_state: maximum relative charge state. The default is 1.
|
|
253
|
-
initial_charge_state: storage charge_state at the beginning. The default is 0.
|
|
254
|
-
minimal_final_charge_state: minimal value of chargeState at the end of timeseries.
|
|
255
|
-
maximal_final_charge_state: maximal value of chargeState at the end of timeseries.
|
|
256
|
-
eta_charge: efficiency factor of charging/loading. The default is 1.
|
|
257
|
-
eta_discharge: efficiency factor of uncharging/unloading. The default is 1.
|
|
258
|
-
relative_loss_per_hour: loss per chargeState-Unit per hour. The default is 0.
|
|
259
|
-
prevent_simultaneous_charge_and_discharge: If True, loading and unloading at the same time is not possible.
|
|
260
|
-
Increases the number of binary variables, but is recommended for easier evaluation. The default is True.
|
|
261
|
-
meta_data: used to store more information about the Element. Is not used internally, but saved in the results. Only use python native types.
|
|
262
|
-
"""
|
|
263
399
|
# TODO: fixed_relative_chargeState implementieren
|
|
264
400
|
super().__init__(
|
|
265
401
|
label,
|
|
@@ -272,117 +408,252 @@ class Storage(Component):
|
|
|
272
408
|
self.charging = charging
|
|
273
409
|
self.discharging = discharging
|
|
274
410
|
self.capacity_in_flow_hours = capacity_in_flow_hours
|
|
275
|
-
self.relative_minimum_charge_state:
|
|
276
|
-
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
|
|
277
416
|
|
|
278
417
|
self.initial_charge_state = initial_charge_state
|
|
279
418
|
self.minimal_final_charge_state = minimal_final_charge_state
|
|
280
419
|
self.maximal_final_charge_state = maximal_final_charge_state
|
|
281
420
|
|
|
282
|
-
self.eta_charge:
|
|
283
|
-
self.eta_discharge:
|
|
284
|
-
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
|
|
285
424
|
self.prevent_simultaneous_charge_and_discharge = prevent_simultaneous_charge_and_discharge
|
|
425
|
+
self.balanced = balanced
|
|
286
426
|
|
|
287
|
-
def create_model(self, model:
|
|
427
|
+
def create_model(self, model: FlowSystemModel) -> StorageModel:
|
|
288
428
|
self._plausibility_checks()
|
|
289
|
-
self.
|
|
290
|
-
return self.
|
|
291
|
-
|
|
292
|
-
def transform_data(self, flow_system:
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
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',
|
|
296
437
|
self.relative_minimum_charge_state,
|
|
297
|
-
needs_extra_timestep=True,
|
|
298
438
|
)
|
|
299
|
-
self.relative_maximum_charge_state = flow_system.
|
|
300
|
-
f'{
|
|
439
|
+
self.relative_maximum_charge_state = flow_system.fit_to_model_coords(
|
|
440
|
+
f'{prefix}|relative_maximum_charge_state',
|
|
301
441
|
self.relative_maximum_charge_state,
|
|
302
|
-
needs_extra_timestep=True,
|
|
303
442
|
)
|
|
304
|
-
self.eta_charge = flow_system.
|
|
305
|
-
self.eta_discharge = flow_system.
|
|
306
|
-
self.relative_loss_per_hour = flow_system.
|
|
307
|
-
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
|
|
447
|
+
)
|
|
448
|
+
if not isinstance(self.initial_charge_state, str):
|
|
449
|
+
self.initial_charge_state = flow_system.fit_to_model_coords(
|
|
450
|
+
f'{prefix}|initial_charge_state', self.initial_charge_state, dims=['period', 'scenario']
|
|
451
|
+
)
|
|
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']
|
|
454
|
+
)
|
|
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'],
|
|
308
467
|
)
|
|
309
468
|
if isinstance(self.capacity_in_flow_hours, InvestParameters):
|
|
310
|
-
self.capacity_in_flow_hours.transform_data(flow_system)
|
|
469
|
+
self.capacity_in_flow_hours.transform_data(flow_system, f'{prefix}|InvestParameters')
|
|
470
|
+
else:
|
|
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']
|
|
473
|
+
)
|
|
311
474
|
|
|
312
475
|
def _plausibility_checks(self) -> None:
|
|
313
476
|
"""
|
|
314
477
|
Check for infeasible or uncommon combinations of parameters
|
|
315
478
|
"""
|
|
316
479
|
super()._plausibility_checks()
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
maximum_capacity = self.capacity_in_flow_hours.fixed_size
|
|
324
|
-
minimum_capacity = self.capacity_in_flow_hours.fixed_size
|
|
480
|
+
|
|
481
|
+
# Validate string values and set flag
|
|
482
|
+
initial_is_last = False
|
|
483
|
+
if isinstance(self.initial_charge_state, str):
|
|
484
|
+
if self.initial_charge_state == 'lastValueOfSim':
|
|
485
|
+
initial_is_last = True
|
|
325
486
|
else:
|
|
326
|
-
|
|
327
|
-
minimum_capacity = self.capacity_in_flow_hours
|
|
487
|
+
raise PlausibilityError(f'initial_charge_state has undefined value: {self.initial_charge_state}')
|
|
328
488
|
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
489
|
+
# Use new InvestParameters methods to get capacity bounds
|
|
490
|
+
if isinstance(self.capacity_in_flow_hours, InvestParameters):
|
|
491
|
+
minimum_capacity = self.capacity_in_flow_hours.minimum_or_fixed_size
|
|
492
|
+
maximum_capacity = self.capacity_in_flow_hours.maximum_or_fixed_size
|
|
493
|
+
else:
|
|
494
|
+
maximum_capacity = self.capacity_in_flow_hours
|
|
495
|
+
minimum_capacity = self.capacity_in_flow_hours
|
|
333
496
|
|
|
334
|
-
|
|
335
|
-
|
|
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)
|
|
500
|
+
|
|
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(
|
|
336
505
|
f'{self.label_full}: {self.initial_charge_state=} '
|
|
337
|
-
f'is
|
|
506
|
+
f'is constraining the investment decision. Chosse a value above {maximum_initial_capacity}'
|
|
338
507
|
)
|
|
339
|
-
if self.initial_charge_state <
|
|
340
|
-
raise
|
|
508
|
+
if (self.initial_charge_state < minimum_initial_capacity).any():
|
|
509
|
+
raise PlausibilityError(
|
|
341
510
|
f'{self.label_full}: {self.initial_charge_state=} '
|
|
342
|
-
f'is
|
|
511
|
+
f'is constraining the investment decision. Chosse a value below {minimum_initial_capacity}'
|
|
512
|
+
)
|
|
513
|
+
|
|
514
|
+
if self.balanced:
|
|
515
|
+
if not isinstance(self.charging.size, InvestParameters) or not isinstance(
|
|
516
|
+
self.discharging.size, InvestParameters
|
|
517
|
+
):
|
|
518
|
+
raise PlausibilityError(
|
|
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():
|
|
525
|
+
raise PlausibilityError(
|
|
526
|
+
f'Balancing charging and discharging Flows in {self.label_full} need compatible minimum and maximum sizes.'
|
|
527
|
+
f'Got: {self.charging.size.minimum_size=}, {self.charging.size.maximum_size=} and '
|
|
528
|
+
f'{self.discharging.size.minimum_size=}, {self.discharging.size.maximum_size=}.'
|
|
343
529
|
)
|
|
344
|
-
elif self.initial_charge_state != 'lastValueOfSim':
|
|
345
|
-
raise ValueError(f'{self.label_full}: {self.initial_charge_state=} has an invalid value')
|
|
346
530
|
|
|
347
531
|
|
|
348
532
|
@register_class_for_io
|
|
349
533
|
class Transmission(Component):
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
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
|
+
"""
|
|
355
642
|
|
|
356
643
|
def __init__(
|
|
357
644
|
self,
|
|
358
645
|
label: str,
|
|
359
646
|
in1: Flow,
|
|
360
647
|
out1: Flow,
|
|
361
|
-
in2:
|
|
362
|
-
out2:
|
|
363
|
-
relative_losses:
|
|
364
|
-
absolute_losses:
|
|
648
|
+
in2: Flow | None = None,
|
|
649
|
+
out2: Flow | None = None,
|
|
650
|
+
relative_losses: TemporalDataUser | None = None,
|
|
651
|
+
absolute_losses: TemporalDataUser | None = None,
|
|
365
652
|
on_off_parameters: OnOffParameters = None,
|
|
366
653
|
prevent_simultaneous_flows_in_both_directions: bool = True,
|
|
367
|
-
|
|
654
|
+
balanced: bool = False,
|
|
655
|
+
meta_data: dict | None = None,
|
|
368
656
|
):
|
|
369
|
-
"""
|
|
370
|
-
Initializes a Transmission component (Pipe, cable, ...) that models the flows between two sides
|
|
371
|
-
with potential losses.
|
|
372
|
-
|
|
373
|
-
Args:
|
|
374
|
-
label: The label of the Element. Used to identify it in the FlowSystem
|
|
375
|
-
in1: The inflow at side A. Pass InvestmentParameters here.
|
|
376
|
-
out1: The outflow at side B.
|
|
377
|
-
in2: The optional inflow at side B.
|
|
378
|
-
If in1 got InvestParameters, the size of this Flow will be equal to in1 (with no extra effects!)
|
|
379
|
-
out2: The optional outflow at side A.
|
|
380
|
-
relative_losses: The relative loss between inflow and outflow, e.g., 0.02 for 2% loss.
|
|
381
|
-
absolute_losses: The absolute loss, occur only when the Flow is on. Induces the creation of the ON-Variable
|
|
382
|
-
on_off_parameters: Parameters defining the on/off behavior of the component.
|
|
383
|
-
prevent_simultaneous_flows_in_both_directions: If True, inflow and outflow are not allowed to be both non-zero at same timestep.
|
|
384
|
-
meta_data: used to store more information about the Element. Is not used internally, but saved in the results. Only use python native types.
|
|
385
|
-
"""
|
|
386
657
|
super().__init__(
|
|
387
658
|
label,
|
|
388
659
|
inputs=[flow for flow in (in1, in2) if flow is not None],
|
|
@@ -400,6 +671,7 @@ class Transmission(Component):
|
|
|
400
671
|
|
|
401
672
|
self.relative_losses = relative_losses
|
|
402
673
|
self.absolute_losses = absolute_losses
|
|
674
|
+
self.balanced = balanced
|
|
403
675
|
|
|
404
676
|
def _plausibility_checks(self):
|
|
405
677
|
super()._plausibility_checks()
|
|
@@ -412,51 +684,47 @@ class Transmission(Component):
|
|
|
412
684
|
assert self.out2.bus == self.in1.bus, (
|
|
413
685
|
f'Input 1 and Output 2 do not start/end at the same Bus: {self.in1.bus=}, {self.out2.bus=}'
|
|
414
686
|
)
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
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():
|
|
418
696
|
raise ValueError(
|
|
419
|
-
'Transmission
|
|
420
|
-
'
|
|
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=}.'
|
|
421
700
|
)
|
|
422
701
|
|
|
423
|
-
def create_model(self, model) ->
|
|
702
|
+
def create_model(self, model) -> TransmissionModel:
|
|
424
703
|
self._plausibility_checks()
|
|
425
|
-
self.
|
|
426
|
-
return self.
|
|
704
|
+
self.submodel = TransmissionModel(model, self)
|
|
705
|
+
return self.submodel
|
|
427
706
|
|
|
428
|
-
def transform_data(self, flow_system:
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
)
|
|
433
|
-
self.absolute_losses = flow_system.create_time_series(
|
|
434
|
-
f'{self.label_full}|absolute_losses', self.absolute_losses
|
|
435
|
-
)
|
|
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)
|
|
436
712
|
|
|
437
713
|
|
|
438
714
|
class TransmissionModel(ComponentModel):
|
|
439
|
-
|
|
440
|
-
super().__init__(model, element)
|
|
441
|
-
self.element: Transmission = element
|
|
442
|
-
self.on_off: Optional[OnOffModel] = None
|
|
715
|
+
element: Transmission
|
|
443
716
|
|
|
444
|
-
def
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
if (self.element.absolute_losses is not None) and np.any(self.element.absolute_losses.active_data != 0):
|
|
448
|
-
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:
|
|
449
720
|
if flow.on_off_parameters is None:
|
|
450
721
|
flow.on_off_parameters = OnOffParameters()
|
|
451
722
|
|
|
452
|
-
|
|
453
|
-
if self.element.in2 is not None:
|
|
454
|
-
if isinstance(self.element.in1.size, InvestParameters) and not isinstance(
|
|
455
|
-
self.element.in2.size, InvestParameters
|
|
456
|
-
):
|
|
457
|
-
self.element.in2.size = InvestParameters(maximum_size=self.element.in1.size.maximum_size)
|
|
723
|
+
super().__init__(model, element)
|
|
458
724
|
|
|
459
|
-
|
|
725
|
+
def _do_modeling(self):
|
|
726
|
+
"""Initiates all FlowModels"""
|
|
727
|
+
super()._do_modeling()
|
|
460
728
|
|
|
461
729
|
# first direction
|
|
462
730
|
self.create_transmission_equation('dir1', self.element.in1, self.element.out1)
|
|
@@ -466,43 +734,37 @@ class TransmissionModel(ComponentModel):
|
|
|
466
734
|
self.create_transmission_equation('dir2', self.element.in2, self.element.out2)
|
|
467
735
|
|
|
468
736
|
# equate size of both directions
|
|
469
|
-
if
|
|
737
|
+
if self.element.balanced:
|
|
470
738
|
# eq: in1.size = in2.size
|
|
471
|
-
self.
|
|
472
|
-
self.
|
|
473
|
-
|
|
474
|
-
name=f'{self.label_full}|same_size',
|
|
475
|
-
),
|
|
476
|
-
'same_size',
|
|
739
|
+
self.add_constraints(
|
|
740
|
+
self.element.in1.submodel._investment.size == self.element.in2.submodel._investment.size,
|
|
741
|
+
short_name='same_size',
|
|
477
742
|
)
|
|
478
743
|
|
|
479
744
|
def create_transmission_equation(self, name: str, in_flow: Flow, out_flow: Flow) -> linopy.Constraint:
|
|
480
745
|
"""Creates an Equation for the Transmission efficiency and adds it to the model"""
|
|
481
746
|
# eq: out(t) + on(t)*loss_abs(t) = in(t)*(1 - loss_rel(t))
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
),
|
|
487
|
-
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,
|
|
488
751
|
)
|
|
489
752
|
|
|
490
753
|
if self.element.absolute_losses is not None:
|
|
491
|
-
con_transmission.lhs += in_flow.
|
|
754
|
+
con_transmission.lhs += in_flow.submodel.on_off.on * self.element.absolute_losses
|
|
492
755
|
|
|
493
756
|
return con_transmission
|
|
494
757
|
|
|
495
758
|
|
|
496
759
|
class LinearConverterModel(ComponentModel):
|
|
497
|
-
|
|
498
|
-
super().__init__(model, element)
|
|
499
|
-
self.element: LinearConverter = element
|
|
500
|
-
self.on_off: Optional[OnOffModel] = None
|
|
501
|
-
self.piecewise_conversion: Optional[PiecewiseConversion] = None
|
|
760
|
+
element: LinearConverter
|
|
502
761
|
|
|
503
|
-
def
|
|
504
|
-
|
|
762
|
+
def __init__(self, model: FlowSystemModel, element: LinearConverter):
|
|
763
|
+
self.piecewise_conversion: PiecewiseConversion | None = None
|
|
764
|
+
super().__init__(model, element)
|
|
505
765
|
|
|
766
|
+
def _do_modeling(self):
|
|
767
|
+
super()._do_modeling()
|
|
506
768
|
# conversion_factors:
|
|
507
769
|
if self.element.conversion_factors:
|
|
508
770
|
all_input_flows = set(self.element.inputs)
|
|
@@ -511,149 +773,135 @@ class LinearConverterModel(ComponentModel):
|
|
|
511
773
|
# für alle linearen Gleichungen:
|
|
512
774
|
for i, conv_factors in enumerate(self.element.conversion_factors):
|
|
513
775
|
used_flows = set([self.element.flows[flow_label] for flow_label in conv_factors])
|
|
514
|
-
used_inputs:
|
|
515
|
-
used_outputs:
|
|
516
|
-
|
|
517
|
-
self.
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
name=f'{self.label_full}|conversion_{i}',
|
|
522
|
-
)
|
|
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}',
|
|
523
783
|
)
|
|
524
784
|
|
|
525
785
|
else:
|
|
526
786
|
# TODO: Improve Inclusion of OnOffParameters. Instead of creating a Binary in every flow, the binary could only be part of the Piece itself
|
|
527
787
|
piecewise_conversion = {
|
|
528
|
-
self.element.flows[flow].
|
|
788
|
+
self.element.flows[flow].submodel.flow_rate.name: piecewise
|
|
529
789
|
for flow, piecewise in self.element.piecewise_conversion.items()
|
|
530
790
|
}
|
|
531
791
|
|
|
532
|
-
self.piecewise_conversion = self.
|
|
792
|
+
self.piecewise_conversion = self.add_submodels(
|
|
533
793
|
PiecewiseModel(
|
|
534
794
|
model=self._model,
|
|
535
795
|
label_of_element=self.label_of_element,
|
|
796
|
+
label_of_model=f'{self.label_of_element}',
|
|
536
797
|
piecewise_variables=piecewise_conversion,
|
|
537
798
|
zero_point=self.on_off.on if self.on_off is not None else False,
|
|
538
|
-
|
|
539
|
-
)
|
|
799
|
+
dims=('time', 'period', 'scenario'),
|
|
800
|
+
),
|
|
801
|
+
short_name='PiecewiseConversion',
|
|
540
802
|
)
|
|
541
|
-
self.piecewise_conversion.do_modeling()
|
|
542
803
|
|
|
543
804
|
|
|
544
805
|
class StorageModel(ComponentModel):
|
|
545
|
-
"""
|
|
806
|
+
"""Submodel of Storage"""
|
|
807
|
+
|
|
808
|
+
element: Storage
|
|
546
809
|
|
|
547
|
-
def __init__(self, model:
|
|
810
|
+
def __init__(self, model: FlowSystemModel, element: Storage):
|
|
548
811
|
super().__init__(model, element)
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
self._model.add_variables(
|
|
560
|
-
lower=lb, upper=ub, coords=self._model.coords_extra, name=f'{self.label_full}|charge_state'
|
|
561
|
-
),
|
|
562
|
-
'charge_state',
|
|
563
|
-
)
|
|
564
|
-
self.netto_discharge = self.add(
|
|
565
|
-
self._model.add_variables(coords=self._model.coords, name=f'{self.label_full}|netto_discharge'),
|
|
566
|
-
'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',
|
|
567
822
|
)
|
|
823
|
+
|
|
824
|
+
self.add_variables(coords=self._model.get_coords(), short_name='netto_discharge')
|
|
825
|
+
|
|
568
826
|
# netto_discharge:
|
|
569
827
|
# eq: nettoFlow(t) - discharging(t) + charging(t) = 0
|
|
570
|
-
self.
|
|
571
|
-
self.
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
name=f'{self.label_full}|netto_discharge',
|
|
575
|
-
),
|
|
576
|
-
'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',
|
|
577
832
|
)
|
|
578
833
|
|
|
579
834
|
charge_state = self.charge_state
|
|
580
|
-
rel_loss = self.element.relative_loss_per_hour
|
|
835
|
+
rel_loss = self.element.relative_loss_per_hour
|
|
581
836
|
hours_per_step = self._model.hours_per_step
|
|
582
|
-
charge_rate = self.element.charging.
|
|
583
|
-
discharge_rate = self.element.discharging.
|
|
584
|
-
eff_charge = self.element.eta_charge
|
|
585
|
-
eff_discharge = self.element.eta_discharge
|
|
586
|
-
|
|
587
|
-
self.
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
name=f'{self.label_full}|charge_state',
|
|
594
|
-
),
|
|
595
|
-
'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',
|
|
596
848
|
)
|
|
597
849
|
|
|
598
850
|
if isinstance(self.element.capacity_in_flow_hours, InvestParameters):
|
|
599
|
-
self.
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
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,
|
|
605
866
|
)
|
|
606
|
-
self.sub_models.append(self._investment)
|
|
607
|
-
self._investment.do_modeling()
|
|
608
867
|
|
|
609
868
|
# Initial charge state
|
|
610
869
|
self._initial_and_final_charge_state()
|
|
611
870
|
|
|
871
|
+
if self.element.balanced:
|
|
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',
|
|
876
|
+
)
|
|
877
|
+
|
|
612
878
|
def _initial_and_final_charge_state(self):
|
|
613
879
|
if self.element.initial_charge_state is not None:
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
if utils.is_number(self.element.initial_charge_state):
|
|
618
|
-
self.add(
|
|
619
|
-
self._model.add_constraints(
|
|
620
|
-
self.charge_state.isel(time=0) == self.element.initial_charge_state, name=name
|
|
621
|
-
),
|
|
622
|
-
name_short,
|
|
880
|
+
if isinstance(self.element.initial_charge_state, str):
|
|
881
|
+
self.add_constraints(
|
|
882
|
+
self.charge_state.isel(time=0) == self.charge_state.isel(time=-1), short_name='initial_charge_state'
|
|
623
883
|
)
|
|
624
|
-
|
|
625
|
-
self.
|
|
626
|
-
self.
|
|
627
|
-
|
|
628
|
-
),
|
|
629
|
-
name_short,
|
|
630
|
-
)
|
|
631
|
-
else: # TODO: Validation in Storage Class, not in Model
|
|
632
|
-
raise PlausibilityError(
|
|
633
|
-
f'initial_charge_state has undefined value: {self.element.initial_charge_state}'
|
|
884
|
+
else:
|
|
885
|
+
self.add_constraints(
|
|
886
|
+
self.charge_state.isel(time=0) == self.element.initial_charge_state,
|
|
887
|
+
short_name='initial_charge_state',
|
|
634
888
|
)
|
|
635
889
|
|
|
636
890
|
if self.element.maximal_final_charge_state is not None:
|
|
637
|
-
self.
|
|
638
|
-
self.
|
|
639
|
-
|
|
640
|
-
name=f'{self.label_full}|final_charge_max',
|
|
641
|
-
),
|
|
642
|
-
'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',
|
|
643
894
|
)
|
|
644
895
|
|
|
645
896
|
if self.element.minimal_final_charge_state is not None:
|
|
646
|
-
self.
|
|
647
|
-
self.
|
|
648
|
-
|
|
649
|
-
name=f'{self.label_full}|final_charge_min',
|
|
650
|
-
),
|
|
651
|
-
'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',
|
|
652
900
|
)
|
|
653
901
|
|
|
654
902
|
@property
|
|
655
|
-
def
|
|
656
|
-
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
|
|
657
905
|
if not isinstance(self.element.capacity_in_flow_hours, InvestParameters):
|
|
658
906
|
return (
|
|
659
907
|
relative_lower_bound * self.element.capacity_in_flow_hours,
|
|
@@ -666,69 +914,171 @@ class StorageModel(ComponentModel):
|
|
|
666
914
|
)
|
|
667
915
|
|
|
668
916
|
@property
|
|
669
|
-
def
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
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']
|
|
674
966
|
|
|
675
967
|
|
|
676
968
|
@register_class_for_io
|
|
677
969
|
class SourceAndSink(Component):
|
|
678
970
|
"""
|
|
679
|
-
|
|
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.
|
|
680
1052
|
"""
|
|
681
1053
|
|
|
682
1054
|
def __init__(
|
|
683
1055
|
self,
|
|
684
1056
|
label: str,
|
|
685
|
-
inputs:
|
|
686
|
-
outputs:
|
|
1057
|
+
inputs: list[Flow] | None = None,
|
|
1058
|
+
outputs: list[Flow] | None = None,
|
|
687
1059
|
prevent_simultaneous_flow_rates: bool = True,
|
|
688
|
-
meta_data:
|
|
1060
|
+
meta_data: dict | None = None,
|
|
689
1061
|
**kwargs,
|
|
690
1062
|
):
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
'The use of the source argument is deprecated. Use the outputs argument instead.',
|
|
705
|
-
stacklevel=2,
|
|
706
|
-
)
|
|
707
|
-
if outputs is not None:
|
|
708
|
-
raise ValueError('Either source or outputs can be specified, but not both.')
|
|
709
|
-
outputs = [source]
|
|
710
|
-
|
|
711
|
-
if sink is not None:
|
|
712
|
-
warnings.deprecated(
|
|
713
|
-
'The use of the sink argument is deprecated. Use the outputs argument instead.',
|
|
714
|
-
stacklevel=2,
|
|
715
|
-
)
|
|
716
|
-
if inputs is not None:
|
|
717
|
-
raise ValueError('Either sink or outputs can be specified, but not both.')
|
|
718
|
-
inputs = [sink]
|
|
719
|
-
|
|
720
|
-
if prevent_simultaneous_sink_and_source is not None:
|
|
721
|
-
warnings.deprecated(
|
|
722
|
-
'The use of the prevent_simultaneous_sink_and_source argument is deprecated. Use the prevent_simultaneous_flow_rates argument instead.',
|
|
723
|
-
stacklevel=2,
|
|
724
|
-
)
|
|
725
|
-
prevent_simultaneous_flow_rates = prevent_simultaneous_sink_and_source
|
|
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)
|
|
726
1076
|
|
|
727
1077
|
super().__init__(
|
|
728
1078
|
label,
|
|
729
1079
|
inputs=inputs,
|
|
730
1080
|
outputs=outputs,
|
|
731
|
-
prevent_simultaneous_flows=inputs + outputs if prevent_simultaneous_flow_rates
|
|
1081
|
+
prevent_simultaneous_flows=(inputs or []) + (outputs or []) if prevent_simultaneous_flow_rates else None,
|
|
732
1082
|
meta_data=meta_data,
|
|
733
1083
|
)
|
|
734
1084
|
self.prevent_simultaneous_flow_rates = prevent_simultaneous_flow_rates
|
|
@@ -745,7 +1095,7 @@ class SourceAndSink(Component):
|
|
|
745
1095
|
@property
|
|
746
1096
|
def sink(self) -> Flow:
|
|
747
1097
|
warnings.warn(
|
|
748
|
-
'The sink property is deprecated. Use the
|
|
1098
|
+
'The sink property is deprecated. Use the inputs property instead.',
|
|
749
1099
|
DeprecationWarning,
|
|
750
1100
|
stacklevel=2,
|
|
751
1101
|
)
|
|
@@ -763,30 +1113,93 @@ class SourceAndSink(Component):
|
|
|
763
1113
|
|
|
764
1114
|
@register_class_for_io
|
|
765
1115
|
class Source(Component):
|
|
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
|
+
|
|
766
1190
|
def __init__(
|
|
767
1191
|
self,
|
|
768
1192
|
label: str,
|
|
769
|
-
outputs:
|
|
770
|
-
meta_data:
|
|
1193
|
+
outputs: list[Flow] | None = None,
|
|
1194
|
+
meta_data: dict | None = None,
|
|
771
1195
|
prevent_simultaneous_flow_rates: bool = False,
|
|
772
1196
|
**kwargs,
|
|
773
1197
|
):
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
"""
|
|
780
|
-
source = kwargs.pop('source', None)
|
|
781
|
-
if source is not None:
|
|
782
|
-
warnings.warn(
|
|
783
|
-
'The use of the source argument is deprecated. Use the outputs argument instead.',
|
|
784
|
-
DeprecationWarning,
|
|
785
|
-
stacklevel=2,
|
|
786
|
-
)
|
|
787
|
-
if outputs is not None:
|
|
788
|
-
raise ValueError('Either source or outputs can be specified, but not both.')
|
|
789
|
-
outputs = [source]
|
|
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)
|
|
790
1203
|
|
|
791
1204
|
self.prevent_simultaneous_flow_rates = prevent_simultaneous_flow_rates
|
|
792
1205
|
super().__init__(
|
|
@@ -808,30 +1221,108 @@ class Source(Component):
|
|
|
808
1221
|
|
|
809
1222
|
@register_class_for_io
|
|
810
1223
|
class Sink(Component):
|
|
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
|
+
|
|
811
1299
|
def __init__(
|
|
812
1300
|
self,
|
|
813
1301
|
label: str,
|
|
814
|
-
inputs:
|
|
815
|
-
meta_data:
|
|
1302
|
+
inputs: list[Flow] | None = None,
|
|
1303
|
+
meta_data: dict | None = None,
|
|
816
1304
|
prevent_simultaneous_flow_rates: bool = False,
|
|
817
1305
|
**kwargs,
|
|
818
1306
|
):
|
|
819
1307
|
"""
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
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.
|
|
824
1320
|
"""
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
stacklevel=2,
|
|
831
|
-
)
|
|
832
|
-
if inputs is not None:
|
|
833
|
-
raise ValueError('Either sink or outputs can be specified, but not both.')
|
|
834
|
-
inputs = [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)
|
|
835
1326
|
|
|
836
1327
|
self.prevent_simultaneous_flow_rates = prevent_simultaneous_flow_rates
|
|
837
1328
|
super().__init__(
|
|
@@ -844,7 +1335,7 @@ class Sink(Component):
|
|
|
844
1335
|
@property
|
|
845
1336
|
def sink(self) -> Flow:
|
|
846
1337
|
warnings.warn(
|
|
847
|
-
'The sink property is deprecated. Use the
|
|
1338
|
+
'The sink property is deprecated. Use the inputs property instead.',
|
|
848
1339
|
DeprecationWarning,
|
|
849
1340
|
stacklevel=2,
|
|
850
1341
|
)
|