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/elements.py
CHANGED
|
@@ -2,21 +2,26 @@
|
|
|
2
2
|
This module contains the basic elements 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
|
|
8
10
|
|
|
9
|
-
import linopy
|
|
10
11
|
import numpy as np
|
|
12
|
+
import xarray as xr
|
|
11
13
|
|
|
12
14
|
from .config import CONFIG
|
|
13
|
-
from .core import PlausibilityError, Scalar,
|
|
14
|
-
from .
|
|
15
|
-
from .features import InvestmentModel, OnOffModel, PreventSimultaneousUsageModel
|
|
15
|
+
from .core import PlausibilityError, Scalar, TemporalData, TemporalDataUser
|
|
16
|
+
from .features import InvestmentModel, OnOffModel
|
|
16
17
|
from .interface import InvestParameters, OnOffParameters
|
|
17
|
-
from .
|
|
18
|
+
from .modeling import BoundingPatterns, ModelingPrimitives, ModelingUtilitiesAbstract
|
|
19
|
+
from .structure import Element, ElementModel, FlowSystemModel, register_class_for_io
|
|
18
20
|
|
|
19
21
|
if TYPE_CHECKING:
|
|
22
|
+
import linopy
|
|
23
|
+
|
|
24
|
+
from .effects import TemporalEffectsUser
|
|
20
25
|
from .flow_system import FlowSystem
|
|
21
26
|
|
|
22
27
|
logger = logging.getLogger('flixopt')
|
|
@@ -25,58 +30,80 @@ logger = logging.getLogger('flixopt')
|
|
|
25
30
|
@register_class_for_io
|
|
26
31
|
class Component(Element):
|
|
27
32
|
"""
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
+
Base class for all system components that transform, convert, or process flows.
|
|
34
|
+
|
|
35
|
+
Components are the active elements in energy systems that define how input and output
|
|
36
|
+
Flows interact with each other. They represent equipment, processes, or logical
|
|
37
|
+
operations that transform energy or materials between different states, carriers,
|
|
38
|
+
or locations.
|
|
39
|
+
|
|
40
|
+
Components serve as connection points between Buses through their associated Flows,
|
|
41
|
+
enabling the modeling of complex energy system topologies and operational constraints.
|
|
42
|
+
|
|
43
|
+
Args:
|
|
44
|
+
label: The label of the Element. Used to identify it in the FlowSystem.
|
|
45
|
+
inputs: list of input Flows feeding into the component. These represent
|
|
46
|
+
energy/material consumption by the component.
|
|
47
|
+
outputs: list of output Flows leaving the component. These represent
|
|
48
|
+
energy/material production by the component.
|
|
49
|
+
on_off_parameters: Defines binary operation constraints and costs when the
|
|
50
|
+
component has discrete on/off states. Creates binary variables for all
|
|
51
|
+
connected Flows. For better performance, prefer defining OnOffParameters
|
|
52
|
+
on individual Flows when possible.
|
|
53
|
+
prevent_simultaneous_flows: list of Flows that cannot be active simultaneously.
|
|
54
|
+
Creates binary variables to enforce mutual exclusivity. Use sparingly as
|
|
55
|
+
it increases computational complexity.
|
|
56
|
+
meta_data: Used to store additional information. Not used internally but saved
|
|
57
|
+
in results. Only use Python native types.
|
|
58
|
+
|
|
59
|
+
Note:
|
|
60
|
+
Component operational state is determined by its connected Flows:
|
|
61
|
+
- Component is "on" if ANY of its Flows is active (flow_rate > 0)
|
|
62
|
+
- Component is "off" only when ALL Flows are inactive (flow_rate = 0)
|
|
63
|
+
|
|
64
|
+
Binary variables and constraints:
|
|
65
|
+
- on_off_parameters creates binary variables for ALL connected Flows
|
|
66
|
+
- prevent_simultaneous_flows creates binary variables for specified Flows
|
|
67
|
+
- For better computational performance, prefer Flow-level OnOffParameters
|
|
68
|
+
|
|
69
|
+
Component is an abstract base class. In practice, use specialized subclasses:
|
|
70
|
+
- LinearConverter: Linear input/output relationships
|
|
71
|
+
- Storage: Temporal energy/material storage
|
|
72
|
+
- Transmission: Transport between locations
|
|
73
|
+
- Source/Sink: System boundaries
|
|
74
|
+
|
|
33
75
|
"""
|
|
34
76
|
|
|
35
77
|
def __init__(
|
|
36
78
|
self,
|
|
37
79
|
label: str,
|
|
38
|
-
inputs:
|
|
39
|
-
outputs:
|
|
40
|
-
on_off_parameters:
|
|
41
|
-
prevent_simultaneous_flows:
|
|
42
|
-
meta_data:
|
|
80
|
+
inputs: list[Flow] | None = None,
|
|
81
|
+
outputs: list[Flow] | None = None,
|
|
82
|
+
on_off_parameters: OnOffParameters | None = None,
|
|
83
|
+
prevent_simultaneous_flows: list[Flow] | None = None,
|
|
84
|
+
meta_data: dict | None = None,
|
|
43
85
|
):
|
|
44
|
-
"""
|
|
45
|
-
Args:
|
|
46
|
-
label: The label of the Element. Used to identify it in the FlowSystem
|
|
47
|
-
inputs: input flows.
|
|
48
|
-
outputs: output flows.
|
|
49
|
-
on_off_parameters: Information about on and off state of Component.
|
|
50
|
-
Component is On/Off, if all connected Flows are On/Off. This induces an On-Variable (binary) in all Flows!
|
|
51
|
-
If possible, use OnOffParameters in a single Flow instead to keep the number of binary variables low.
|
|
52
|
-
See class OnOffParameters.
|
|
53
|
-
prevent_simultaneous_flows: Define a Group of Flows. Only one them can be on at a time.
|
|
54
|
-
Induces On-Variable in all Flows! If possible, use OnOffParameters in a single Flow instead.
|
|
55
|
-
meta_data: used to store more information about the Element. Is not used internally, but saved in the results. Only use python native types.
|
|
56
|
-
"""
|
|
57
86
|
super().__init__(label, meta_data=meta_data)
|
|
58
|
-
self.inputs:
|
|
59
|
-
self.outputs:
|
|
87
|
+
self.inputs: list[Flow] = inputs or []
|
|
88
|
+
self.outputs: list[Flow] = outputs or []
|
|
60
89
|
self._check_unique_flow_labels()
|
|
61
90
|
self.on_off_parameters = on_off_parameters
|
|
62
|
-
self.prevent_simultaneous_flows:
|
|
91
|
+
self.prevent_simultaneous_flows: list[Flow] = prevent_simultaneous_flows or []
|
|
63
92
|
|
|
64
|
-
self.flows:
|
|
93
|
+
self.flows: dict[str, Flow] = {flow.label: flow for flow in self.inputs + self.outputs}
|
|
65
94
|
|
|
66
|
-
def create_model(self, model:
|
|
95
|
+
def create_model(self, model: FlowSystemModel) -> ComponentModel:
|
|
67
96
|
self._plausibility_checks()
|
|
68
|
-
self.
|
|
69
|
-
return self.
|
|
97
|
+
self.submodel = ComponentModel(model, self)
|
|
98
|
+
return self.submodel
|
|
70
99
|
|
|
71
|
-
def transform_data(self, flow_system:
|
|
100
|
+
def transform_data(self, flow_system: FlowSystem, name_prefix: str = '') -> None:
|
|
101
|
+
prefix = '|'.join(filter(None, [name_prefix, self.label_full]))
|
|
72
102
|
if self.on_off_parameters is not None:
|
|
73
|
-
self.on_off_parameters.transform_data(flow_system,
|
|
103
|
+
self.on_off_parameters.transform_data(flow_system, prefix)
|
|
74
104
|
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
infos['inputs'] = [flow.infos(use_numpy, use_element_label) for flow in self.inputs]
|
|
78
|
-
infos['outputs'] = [flow.infos(use_numpy, use_element_label) for flow in self.outputs]
|
|
79
|
-
return infos
|
|
105
|
+
for flow in self.inputs + self.outputs:
|
|
106
|
+
flow.transform_data(flow_system) # Flow doesnt need the name_prefix
|
|
80
107
|
|
|
81
108
|
def _check_unique_flow_labels(self):
|
|
82
109
|
all_flow_labels = [flow.label for flow in self.inputs + self.outputs]
|
|
@@ -92,38 +119,94 @@ class Component(Element):
|
|
|
92
119
|
@register_class_for_io
|
|
93
120
|
class Bus(Element):
|
|
94
121
|
"""
|
|
95
|
-
|
|
122
|
+
Buses represent nodal balances between flow rates, serving as connection points.
|
|
123
|
+
|
|
124
|
+
A Bus enforces energy or material balance constraints where the sum of all incoming
|
|
125
|
+
flows must equal the sum of all outgoing flows at each time step. Buses represent
|
|
126
|
+
physical or logical connection points for energy carriers (electricity, heat, gas)
|
|
127
|
+
or material flows between different Components.
|
|
128
|
+
|
|
129
|
+
Mathematical Formulation:
|
|
130
|
+
See the complete mathematical model in the documentation:
|
|
131
|
+
[Bus](../user-guide/mathematical-notation/elements/Bus.md)
|
|
132
|
+
|
|
133
|
+
Args:
|
|
134
|
+
label: The label of the Element. Used to identify it in the FlowSystem.
|
|
135
|
+
excess_penalty_per_flow_hour: Penalty costs for bus balance violations.
|
|
136
|
+
When None, no excess/deficit is allowed (hard constraint). When set to a
|
|
137
|
+
value > 0, allows bus imbalances at penalty cost. Default is 1e5 (high penalty).
|
|
138
|
+
meta_data: Used to store additional information. Not used internally but saved
|
|
139
|
+
in results. Only use Python native types.
|
|
140
|
+
|
|
141
|
+
Examples:
|
|
142
|
+
Electrical bus with strict balance:
|
|
143
|
+
|
|
144
|
+
```python
|
|
145
|
+
electricity_bus = Bus(
|
|
146
|
+
label='main_electrical_bus',
|
|
147
|
+
excess_penalty_per_flow_hour=None, # No imbalance allowed
|
|
148
|
+
)
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
Heat network with penalty for imbalances:
|
|
152
|
+
|
|
153
|
+
```python
|
|
154
|
+
heat_network = Bus(
|
|
155
|
+
label='district_heating_network',
|
|
156
|
+
excess_penalty_per_flow_hour=1000, # €1000/MWh penalty for imbalance
|
|
157
|
+
)
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
Material flow with time-varying penalties:
|
|
161
|
+
|
|
162
|
+
```python
|
|
163
|
+
material_hub = Bus(
|
|
164
|
+
label='material_processing_hub',
|
|
165
|
+
excess_penalty_per_flow_hour=waste_disposal_costs, # Time series
|
|
166
|
+
)
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
Note:
|
|
170
|
+
The bus balance equation enforced is: Σ(inflows) = Σ(outflows) + excess - deficit
|
|
171
|
+
|
|
172
|
+
When excess_penalty_per_flow_hour is None, excess and deficit are forced to zero.
|
|
173
|
+
When a penalty cost is specified, the optimization can choose to violate the
|
|
174
|
+
balance if economically beneficial, paying the penalty.
|
|
175
|
+
The penalty is added to the objective directly.
|
|
176
|
+
|
|
177
|
+
Empty `inputs` and `outputs` lists are initialized and populated automatically
|
|
178
|
+
by the FlowSystem during system setup.
|
|
96
179
|
"""
|
|
97
180
|
|
|
98
181
|
def __init__(
|
|
99
|
-
self,
|
|
182
|
+
self,
|
|
183
|
+
label: str,
|
|
184
|
+
excess_penalty_per_flow_hour: TemporalDataUser | None = 1e5,
|
|
185
|
+
meta_data: dict | None = None,
|
|
100
186
|
):
|
|
101
|
-
"""
|
|
102
|
-
Args:
|
|
103
|
-
label: The label of the Element. Used to identify it in the FlowSystem
|
|
104
|
-
excess_penalty_per_flow_hour: excess costs / penalty costs (bus balance compensation)
|
|
105
|
-
(none/ 0 -> no penalty). The default is 1e5.
|
|
106
|
-
(Take care: if you use a timeseries (no scalar), timeseries is aggregated if calculation_type = aggregated!)
|
|
107
|
-
meta_data: used to store more information about the Element. Is not used internally, but saved in the results. Only use python native types.
|
|
108
|
-
"""
|
|
109
187
|
super().__init__(label, meta_data=meta_data)
|
|
110
188
|
self.excess_penalty_per_flow_hour = excess_penalty_per_flow_hour
|
|
111
|
-
self.inputs:
|
|
112
|
-
self.outputs:
|
|
189
|
+
self.inputs: list[Flow] = []
|
|
190
|
+
self.outputs: list[Flow] = []
|
|
113
191
|
|
|
114
|
-
def create_model(self, model:
|
|
192
|
+
def create_model(self, model: FlowSystemModel) -> BusModel:
|
|
115
193
|
self._plausibility_checks()
|
|
116
|
-
self.
|
|
117
|
-
return self.
|
|
194
|
+
self.submodel = BusModel(model, self)
|
|
195
|
+
return self.submodel
|
|
118
196
|
|
|
119
|
-
def transform_data(self, flow_system:
|
|
120
|
-
|
|
121
|
-
|
|
197
|
+
def transform_data(self, flow_system: FlowSystem, name_prefix: str = '') -> None:
|
|
198
|
+
prefix = '|'.join(filter(None, [name_prefix, self.label_full]))
|
|
199
|
+
self.excess_penalty_per_flow_hour = flow_system.fit_to_model_coords(
|
|
200
|
+
f'{prefix}|excess_penalty_per_flow_hour', self.excess_penalty_per_flow_hour
|
|
122
201
|
)
|
|
123
202
|
|
|
124
203
|
def _plausibility_checks(self) -> None:
|
|
125
|
-
if self.excess_penalty_per_flow_hour is not None
|
|
126
|
-
|
|
204
|
+
if self.excess_penalty_per_flow_hour is not None:
|
|
205
|
+
zero_penalty = np.all(np.equal(self.excess_penalty_per_flow_hour, 0))
|
|
206
|
+
if zero_penalty:
|
|
207
|
+
logger.warning(
|
|
208
|
+
f'In Bus {self.label_full}, the excess_penalty_per_flow_hour is 0. Use "None" or a value > 0.'
|
|
209
|
+
)
|
|
127
210
|
|
|
128
211
|
@property
|
|
129
212
|
def with_excess(self) -> bool:
|
|
@@ -145,60 +228,155 @@ class Connection:
|
|
|
145
228
|
|
|
146
229
|
@register_class_for_io
|
|
147
230
|
class Flow(Element):
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
231
|
+
"""Define a directed flow of energy or material between bus and component.
|
|
232
|
+
|
|
233
|
+
A Flow represents the transfer of energy (electricity, heat, fuel) or material
|
|
234
|
+
between a Bus and a Component in a specific direction. The flow rate is the
|
|
235
|
+
primary optimization variable, with constraints and costs defined through
|
|
236
|
+
various parameters. Flows can have fixed or variable sizes, operational
|
|
237
|
+
constraints, and complex on/off behavior.
|
|
238
|
+
|
|
239
|
+
Key Concepts:
|
|
240
|
+
**Flow Rate**: The instantaneous rate of energy/material transfer (optimization variable) [kW, m³/h, kg/h]
|
|
241
|
+
**Flow Hours**: Amount of energy/material transferred per timestep. [kWh, m³, kg]
|
|
242
|
+
**Flow Size**: The maximum capacity or nominal rating of the flow [kW, m³/h, kg/h]
|
|
243
|
+
**Relative Bounds**: Flow rate limits expressed as fractions of flow size
|
|
244
|
+
|
|
245
|
+
Integration with Parameter Classes:
|
|
246
|
+
- **InvestParameters**: Used for `size` when flow Size is an investment decision
|
|
247
|
+
- **OnOffParameters**: Used for `on_off_parameters` when flow has discrete states
|
|
248
|
+
|
|
249
|
+
Mathematical Formulation:
|
|
250
|
+
See the complete mathematical model in the documentation:
|
|
251
|
+
[Flow](../user-guide/mathematical-notation/elements/Flow.md)
|
|
252
|
+
|
|
253
|
+
Args:
|
|
254
|
+
label: Unique flow identifier within its component.
|
|
255
|
+
bus: Bus label this flow connects to.
|
|
256
|
+
size: Flow capacity. Scalar, InvestParameters, or None (uses CONFIG.Modeling.big).
|
|
257
|
+
relative_minimum: Minimum flow rate as fraction of size (0-1). Default: 0.
|
|
258
|
+
relative_maximum: Maximum flow rate as fraction of size. Default: 1.
|
|
259
|
+
load_factor_min: Minimum average utilization (0-1). Default: 0.
|
|
260
|
+
load_factor_max: Maximum average utilization (0-1). Default: 1.
|
|
261
|
+
effects_per_flow_hour: Operational costs/impacts per flow-hour.
|
|
262
|
+
Dict mapping effect names to values (e.g., {'cost': 45, 'CO2': 0.8}).
|
|
263
|
+
on_off_parameters: Binary operation constraints (OnOffParameters). Default: None.
|
|
264
|
+
flow_hours_total_max: Maximum cumulative flow-hours. Alternative to load_factor_max.
|
|
265
|
+
flow_hours_total_min: Minimum cumulative flow-hours. Alternative to load_factor_min.
|
|
266
|
+
fixed_relative_profile: Predetermined pattern as fraction of size.
|
|
267
|
+
Flow rate = size × fixed_relative_profile(t).
|
|
268
|
+
previous_flow_rate: Initial flow state for on/off dynamics. Default: None (off).
|
|
269
|
+
meta_data: Additional info stored in results. Python native types only.
|
|
270
|
+
|
|
271
|
+
Examples:
|
|
272
|
+
Basic power flow with fixed capacity:
|
|
273
|
+
|
|
274
|
+
```python
|
|
275
|
+
generator_output = Flow(
|
|
276
|
+
label='electricity_out',
|
|
277
|
+
bus='electricity_grid',
|
|
278
|
+
size=100, # 100 MW capacity
|
|
279
|
+
relative_minimum=0.4, # Cannot operate below 40 MW
|
|
280
|
+
effects_per_flow_hour={'fuel_cost': 45, 'co2_emissions': 0.8},
|
|
281
|
+
)
|
|
282
|
+
```
|
|
283
|
+
|
|
284
|
+
Investment decision for battery capacity:
|
|
285
|
+
|
|
286
|
+
```python
|
|
287
|
+
battery_flow = Flow(
|
|
288
|
+
label='electricity_storage',
|
|
289
|
+
bus='electricity_grid',
|
|
290
|
+
size=InvestParameters(
|
|
291
|
+
minimum_size=10, # Minimum 10 MWh
|
|
292
|
+
maximum_size=100, # Maximum 100 MWh
|
|
293
|
+
specific_effects={'cost': 150_000}, # €150k/MWh annualized
|
|
294
|
+
),
|
|
295
|
+
)
|
|
296
|
+
```
|
|
297
|
+
|
|
298
|
+
Heat pump with startup costs and minimum run times:
|
|
299
|
+
|
|
300
|
+
```python
|
|
301
|
+
heat_pump = Flow(
|
|
302
|
+
label='heat_output',
|
|
303
|
+
bus='heating_network',
|
|
304
|
+
size=50, # 50 kW thermal
|
|
305
|
+
relative_minimum=0.3, # Minimum 15 kW output when on
|
|
306
|
+
effects_per_flow_hour={'electricity_cost': 25, 'maintenance': 2},
|
|
307
|
+
on_off_parameters=OnOffParameters(
|
|
308
|
+
effects_per_switch_on={'startup_cost': 100, 'wear': 0.1},
|
|
309
|
+
consecutive_on_hours_min=2, # Must run at least 2 hours
|
|
310
|
+
consecutive_off_hours_min=1, # Must stay off at least 1 hour
|
|
311
|
+
switch_on_total_max=200, # Maximum 200 starts per period
|
|
312
|
+
),
|
|
313
|
+
)
|
|
314
|
+
```
|
|
315
|
+
|
|
316
|
+
Fixed renewable generation profile:
|
|
317
|
+
|
|
318
|
+
```python
|
|
319
|
+
solar_generation = Flow(
|
|
320
|
+
label='solar_power',
|
|
321
|
+
bus='electricity_grid',
|
|
322
|
+
size=25, # 25 MW installed capacity
|
|
323
|
+
fixed_relative_profile=np.array([0, 0.1, 0.4, 0.8, 0.9, 0.7, 0.3, 0.1, 0]),
|
|
324
|
+
effects_per_flow_hour={'maintenance_costs': 5}, # €5/MWh maintenance
|
|
325
|
+
)
|
|
326
|
+
```
|
|
327
|
+
|
|
328
|
+
Industrial process with annual utilization limits:
|
|
329
|
+
|
|
330
|
+
```python
|
|
331
|
+
production_line = Flow(
|
|
332
|
+
label='product_output',
|
|
333
|
+
bus='product_market',
|
|
334
|
+
size=1000, # 1000 units/hour capacity
|
|
335
|
+
load_factor_min=0.6, # Must achieve 60% annual utilization
|
|
336
|
+
load_factor_max=0.85, # Cannot exceed 85% for maintenance
|
|
337
|
+
effects_per_flow_hour={'variable_cost': 12, 'quality_control': 0.5},
|
|
338
|
+
)
|
|
339
|
+
```
|
|
340
|
+
|
|
341
|
+
Design Considerations:
|
|
342
|
+
**Size vs Load Factors**: Use `flow_hours_total_min/max` for absolute limits,
|
|
343
|
+
`load_factor_min/max` for utilization-based constraints.
|
|
344
|
+
|
|
345
|
+
**Relative Bounds**: Set `relative_minimum > 0` only when equipment cannot
|
|
346
|
+
operate below that level. Use `on_off_parameters` for discrete on/off behavior.
|
|
347
|
+
|
|
348
|
+
**Fixed Profiles**: Use `fixed_relative_profile` for known exact patterns,
|
|
349
|
+
`relative_maximum` for upper bounds on optimization variables.
|
|
350
|
+
|
|
351
|
+
Notes:
|
|
352
|
+
- Default size (CONFIG.Modeling.big) is used when size=None
|
|
353
|
+
- list inputs for previous_flow_rate are converted to NumPy arrays
|
|
354
|
+
- Flow direction is determined by component input/output designation
|
|
355
|
+
|
|
356
|
+
Deprecated:
|
|
357
|
+
Passing Bus objects to `bus` parameter. Use bus label strings instead.
|
|
358
|
+
|
|
151
359
|
"""
|
|
152
360
|
|
|
153
361
|
def __init__(
|
|
154
362
|
self,
|
|
155
363
|
label: str,
|
|
156
364
|
bus: str,
|
|
157
|
-
size:
|
|
158
|
-
fixed_relative_profile:
|
|
159
|
-
relative_minimum:
|
|
160
|
-
relative_maximum:
|
|
161
|
-
effects_per_flow_hour:
|
|
162
|
-
on_off_parameters:
|
|
163
|
-
flow_hours_total_max:
|
|
164
|
-
flow_hours_total_min:
|
|
165
|
-
load_factor_min:
|
|
166
|
-
load_factor_max:
|
|
167
|
-
previous_flow_rate:
|
|
168
|
-
meta_data:
|
|
365
|
+
size: Scalar | InvestParameters = None,
|
|
366
|
+
fixed_relative_profile: TemporalDataUser | None = None,
|
|
367
|
+
relative_minimum: TemporalDataUser = 0,
|
|
368
|
+
relative_maximum: TemporalDataUser = 1,
|
|
369
|
+
effects_per_flow_hour: TemporalEffectsUser | None = None,
|
|
370
|
+
on_off_parameters: OnOffParameters | None = None,
|
|
371
|
+
flow_hours_total_max: Scalar | None = None,
|
|
372
|
+
flow_hours_total_min: Scalar | None = None,
|
|
373
|
+
load_factor_min: Scalar | None = None,
|
|
374
|
+
load_factor_max: Scalar | None = None,
|
|
375
|
+
previous_flow_rate: Scalar | list[Scalar] | None = None,
|
|
376
|
+
meta_data: dict | None = None,
|
|
169
377
|
):
|
|
170
|
-
r"""
|
|
171
|
-
Args:
|
|
172
|
-
label: The label of the FLow. Used to identify it in the FlowSystem. Its `full_label` consists of the label of the Component and the label of the Flow.
|
|
173
|
-
bus: blabel of the bus the flow is connected to.
|
|
174
|
-
size: size of the flow. If InvestmentParameters is used, size is optimized.
|
|
175
|
-
If size is None, a default value is used.
|
|
176
|
-
relative_minimum: min value is relative_minimum multiplied by size
|
|
177
|
-
relative_maximum: max value is relative_maximum multiplied by size. If size = max then relative_maximum=1
|
|
178
|
-
load_factor_min: minimal load factor general: avg Flow per nominalVal/investSize
|
|
179
|
-
(e.g. boiler, kW/kWh=h; solarthermal: kW/m²;
|
|
180
|
-
def: :math:`load\_factor:= sumFlowHours/ (nominal\_val \cdot \Delta t_{tot})`
|
|
181
|
-
load_factor_max: maximal load factor (see minimal load factor)
|
|
182
|
-
effects_per_flow_hour: operational costs, costs per flow-"work"
|
|
183
|
-
on_off_parameters: If present, flow can be "off", i.e. be zero (only relevant if relative_minimum > 0)
|
|
184
|
-
Therefore a binary var "on" is used. Further, several other restrictions and effects can be modeled
|
|
185
|
-
through this On/Off State (See OnOffParameters)
|
|
186
|
-
flow_hours_total_max: maximum flow-hours ("flow-work")
|
|
187
|
-
(if size is not const, maybe load_factor_max is the better choice!)
|
|
188
|
-
flow_hours_total_min: minimum flow-hours ("flow-work")
|
|
189
|
-
(if size is not predefined, maybe load_factor_min is the better choice!)
|
|
190
|
-
fixed_relative_profile: fixed relative values for flow (if given).
|
|
191
|
-
flow_rate(t) := fixed_relative_profile(t) * size(t)
|
|
192
|
-
With this value, the flow_rate is no optimization-variable anymore.
|
|
193
|
-
(relative_minimum and relative_maximum are ignored)
|
|
194
|
-
used for fixed load or supply profiles, i.g. heat demand, wind-power, solarthermal
|
|
195
|
-
If the load-profile is just an upper limit, use relative_maximum instead.
|
|
196
|
-
previous_flow_rate: previous flow rate of the flow. Used to determine if and how long the
|
|
197
|
-
flow is already on / off. If None, the flow is considered to be off for one timestep.
|
|
198
|
-
meta_data: used to store more information about the Element. Is not used internally, but saved in the results. Only use python native types.
|
|
199
|
-
"""
|
|
200
378
|
super().__init__(label, meta_data=meta_data)
|
|
201
|
-
self.size =
|
|
379
|
+
self.size = CONFIG.Modeling.big if size is None else size
|
|
202
380
|
self.relative_minimum = relative_minimum
|
|
203
381
|
self.relative_maximum = relative_maximum
|
|
204
382
|
self.fixed_relative_profile = fixed_relative_profile
|
|
@@ -211,12 +389,10 @@ class Flow(Element):
|
|
|
211
389
|
self.flow_hours_total_min = flow_hours_total_min
|
|
212
390
|
self.on_off_parameters = on_off_parameters
|
|
213
391
|
|
|
214
|
-
self.previous_flow_rate =
|
|
215
|
-
previous_flow_rate if not isinstance(previous_flow_rate, list) else np.array(previous_flow_rate)
|
|
216
|
-
)
|
|
392
|
+
self.previous_flow_rate = previous_flow_rate
|
|
217
393
|
|
|
218
394
|
self.component: str = 'UnknownComponent'
|
|
219
|
-
self.is_input_in_component:
|
|
395
|
+
self.is_input_in_component: bool | None = None
|
|
220
396
|
if isinstance(bus, Bus):
|
|
221
397
|
self.bus = bus.label_full
|
|
222
398
|
warnings.warn(
|
|
@@ -230,82 +406,80 @@ class Flow(Element):
|
|
|
230
406
|
self.bus = bus
|
|
231
407
|
self._bus_object = None
|
|
232
408
|
|
|
233
|
-
def create_model(self, model:
|
|
409
|
+
def create_model(self, model: FlowSystemModel) -> FlowModel:
|
|
234
410
|
self._plausibility_checks()
|
|
235
|
-
self.
|
|
236
|
-
return self.
|
|
237
|
-
|
|
238
|
-
def transform_data(self, flow_system:
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
)
|
|
242
|
-
self.
|
|
243
|
-
f'{
|
|
411
|
+
self.submodel = FlowModel(model, self)
|
|
412
|
+
return self.submodel
|
|
413
|
+
|
|
414
|
+
def transform_data(self, flow_system: FlowSystem, name_prefix: str = '') -> None:
|
|
415
|
+
prefix = '|'.join(filter(None, [name_prefix, self.label_full]))
|
|
416
|
+
self.relative_minimum = flow_system.fit_to_model_coords(f'{prefix}|relative_minimum', self.relative_minimum)
|
|
417
|
+
self.relative_maximum = flow_system.fit_to_model_coords(f'{prefix}|relative_maximum', self.relative_maximum)
|
|
418
|
+
self.fixed_relative_profile = flow_system.fit_to_model_coords(
|
|
419
|
+
f'{prefix}|fixed_relative_profile', self.fixed_relative_profile
|
|
244
420
|
)
|
|
245
|
-
self.
|
|
246
|
-
|
|
421
|
+
self.effects_per_flow_hour = flow_system.fit_effects_to_model_coords(
|
|
422
|
+
prefix, self.effects_per_flow_hour, 'per_flow_hour'
|
|
247
423
|
)
|
|
248
|
-
self.
|
|
249
|
-
|
|
424
|
+
self.flow_hours_total_max = flow_system.fit_to_model_coords(
|
|
425
|
+
f'{prefix}|flow_hours_total_max', self.flow_hours_total_max, dims=['period', 'scenario']
|
|
250
426
|
)
|
|
251
|
-
self.
|
|
252
|
-
f'{
|
|
427
|
+
self.flow_hours_total_min = flow_system.fit_to_model_coords(
|
|
428
|
+
f'{prefix}|flow_hours_total_min', self.flow_hours_total_min, dims=['period', 'scenario']
|
|
253
429
|
)
|
|
254
|
-
self.
|
|
255
|
-
f'{
|
|
430
|
+
self.load_factor_max = flow_system.fit_to_model_coords(
|
|
431
|
+
f'{prefix}|load_factor_max', self.load_factor_max, dims=['period', 'scenario']
|
|
256
432
|
)
|
|
257
|
-
self.
|
|
258
|
-
f'{
|
|
259
|
-
)
|
|
260
|
-
self.load_factor_min = flow_system.create_time_series(
|
|
261
|
-
f'{self.label_full}|load_factor_min', self.load_factor_min, has_time_dim=False
|
|
433
|
+
self.load_factor_min = flow_system.fit_to_model_coords(
|
|
434
|
+
f'{prefix}|load_factor_min', self.load_factor_min, dims=['period', 'scenario']
|
|
262
435
|
)
|
|
263
436
|
|
|
264
437
|
if self.on_off_parameters is not None:
|
|
265
|
-
self.on_off_parameters.transform_data(flow_system,
|
|
438
|
+
self.on_off_parameters.transform_data(flow_system, prefix)
|
|
266
439
|
if isinstance(self.size, InvestParameters):
|
|
267
|
-
self.size.transform_data(flow_system,
|
|
440
|
+
self.size.transform_data(flow_system, prefix)
|
|
268
441
|
else:
|
|
269
|
-
self.size = flow_system.
|
|
270
|
-
|
|
271
|
-
def infos(self, use_numpy: bool = True, use_element_label: bool = False) -> Dict:
|
|
272
|
-
infos = super().infos(use_numpy, use_element_label)
|
|
273
|
-
infos['is_input_in_component'] = self.is_input_in_component
|
|
274
|
-
return infos
|
|
275
|
-
|
|
276
|
-
def to_dict(self) -> Dict:
|
|
277
|
-
data = super().to_dict()
|
|
278
|
-
if isinstance(data.get('previous_flow_rate'), np.ndarray):
|
|
279
|
-
data['previous_flow_rate'] = data['previous_flow_rate'].tolist()
|
|
280
|
-
return data
|
|
442
|
+
self.size = flow_system.fit_to_model_coords(f'{prefix}|size', self.size, dims=['period', 'scenario'])
|
|
281
443
|
|
|
282
444
|
def _plausibility_checks(self) -> None:
|
|
283
445
|
# TODO: Incorporate into Variable? (Lower_bound can not be greater than upper bound
|
|
284
|
-
if
|
|
446
|
+
if (self.relative_minimum > self.relative_maximum).any():
|
|
285
447
|
raise PlausibilityError(self.label_full + ': Take care, that relative_minimum <= relative_maximum!')
|
|
286
448
|
|
|
287
449
|
if not isinstance(self.size, InvestParameters) and (
|
|
288
|
-
|
|
450
|
+
np.any(self.size == CONFIG.Modeling.big) and self.fixed_relative_profile is not None
|
|
289
451
|
): # Default Size --> Most likely by accident
|
|
290
452
|
logger.warning(
|
|
291
|
-
f'Flow "{self.
|
|
292
|
-
f'The default size is {CONFIG.
|
|
453
|
+
f'Flow "{self.label_full}" has no size assigned, but a "fixed_relative_profile". '
|
|
454
|
+
f'The default size is {CONFIG.Modeling.big}. As "flow_rate = size * fixed_relative_profile", '
|
|
293
455
|
f'the resulting flow_rate will be very high. To fix this, assign a size to the Flow {self}.'
|
|
294
456
|
)
|
|
295
457
|
|
|
296
458
|
if self.fixed_relative_profile is not None and self.on_off_parameters is not None:
|
|
297
459
|
logger.warning(
|
|
298
|
-
f'Flow {self.
|
|
460
|
+
f'Flow {self.label_full} has both a fixed_relative_profile and an on_off_parameters.'
|
|
299
461
|
f'This will allow the flow to be switched on and off, effectively differing from the fixed_flow_rate.'
|
|
300
462
|
)
|
|
301
463
|
|
|
302
|
-
if (self.relative_minimum > 0)
|
|
464
|
+
if np.any(self.relative_minimum > 0) and self.on_off_parameters is None:
|
|
303
465
|
logger.warning(
|
|
304
|
-
f'Flow {self.
|
|
466
|
+
f'Flow {self.label_full} has a relative_minimum of {self.relative_minimum} and no on_off_parameters. '
|
|
305
467
|
f'This prevents the flow_rate from switching off (flow_rate = 0). '
|
|
306
468
|
f'Consider using on_off_parameters to allow the flow to be switched on and off.'
|
|
307
469
|
)
|
|
308
470
|
|
|
471
|
+
if self.previous_flow_rate is not None:
|
|
472
|
+
if not any(
|
|
473
|
+
[
|
|
474
|
+
isinstance(self.previous_flow_rate, np.ndarray) and self.previous_flow_rate.ndim == 1,
|
|
475
|
+
isinstance(self.previous_flow_rate, (int, float, list)),
|
|
476
|
+
]
|
|
477
|
+
):
|
|
478
|
+
raise TypeError(
|
|
479
|
+
f'previous_flow_rate must be None, a scalar, a list of scalars or a 1D-numpy-array. Got {type(self.previous_flow_rate)}. '
|
|
480
|
+
f'Different values in different periods or scenarios are not yet supported.'
|
|
481
|
+
)
|
|
482
|
+
|
|
309
483
|
@property
|
|
310
484
|
def label_full(self) -> str:
|
|
311
485
|
return f'{self.component}({self.label})'
|
|
@@ -315,88 +489,129 @@ class Flow(Element):
|
|
|
315
489
|
# Wenn kein InvestParameters existiert --> True; Wenn Investparameter, den Wert davon nehmen
|
|
316
490
|
return False if (isinstance(self.size, InvestParameters) and self.size.fixed_size is None) else True
|
|
317
491
|
|
|
318
|
-
@property
|
|
319
|
-
def invest_is_optional(self) -> bool:
|
|
320
|
-
# Wenn kein InvestParameters existiert: # Investment ist nicht optional -> Keine Variable --> False
|
|
321
|
-
return False if (isinstance(self.size, InvestParameters) and not self.size.optional) else True
|
|
322
|
-
|
|
323
492
|
|
|
324
493
|
class FlowModel(ElementModel):
|
|
325
|
-
|
|
494
|
+
element: Flow # Type hint
|
|
495
|
+
|
|
496
|
+
def __init__(self, model: FlowSystemModel, element: Flow):
|
|
326
497
|
super().__init__(model, element)
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
self.
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
498
|
+
|
|
499
|
+
def _do_modeling(self):
|
|
500
|
+
super()._do_modeling()
|
|
501
|
+
# Main flow rate variable
|
|
502
|
+
self.add_variables(
|
|
503
|
+
lower=self.absolute_flow_rate_bounds[0],
|
|
504
|
+
upper=self.absolute_flow_rate_bounds[1],
|
|
505
|
+
coords=self._model.get_coords(),
|
|
506
|
+
short_name='flow_rate',
|
|
507
|
+
)
|
|
508
|
+
|
|
509
|
+
self._constraint_flow_rate()
|
|
510
|
+
|
|
511
|
+
# Total flow hours tracking
|
|
512
|
+
ModelingPrimitives.expression_tracking_variable(
|
|
513
|
+
model=self,
|
|
514
|
+
name=f'{self.label_full}|total_flow_hours',
|
|
515
|
+
tracked_expression=(self.flow_rate * self._model.hours_per_step).sum('time'),
|
|
516
|
+
bounds=(
|
|
517
|
+
self.element.flow_hours_total_min if self.element.flow_hours_total_min is not None else 0,
|
|
518
|
+
self.element.flow_hours_total_max if self.element.flow_hours_total_max is not None else None,
|
|
342
519
|
),
|
|
343
|
-
'
|
|
520
|
+
coords=['period', 'scenario'],
|
|
521
|
+
short_name='total_flow_hours',
|
|
344
522
|
)
|
|
345
523
|
|
|
346
|
-
#
|
|
347
|
-
|
|
348
|
-
self.on_off: OnOffModel = self.add(
|
|
349
|
-
OnOffModel(
|
|
350
|
-
model=self._model,
|
|
351
|
-
label_of_element=self.label_of_element,
|
|
352
|
-
on_off_parameters=self.element.on_off_parameters,
|
|
353
|
-
defining_variables=[self.flow_rate],
|
|
354
|
-
defining_bounds=[self.flow_rate_bounds_on],
|
|
355
|
-
previous_values=[self.element.previous_flow_rate],
|
|
356
|
-
),
|
|
357
|
-
'on_off',
|
|
358
|
-
)
|
|
359
|
-
self.on_off.do_modeling()
|
|
524
|
+
# Load factor constraints
|
|
525
|
+
self._create_bounds_for_load_factor()
|
|
360
526
|
|
|
361
|
-
#
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
)
|
|
375
|
-
self._investment.do_modeling()
|
|
376
|
-
|
|
377
|
-
self.total_flow_hours = self.add(
|
|
378
|
-
self._model.add_variables(
|
|
379
|
-
lower=extract_data(self.element.flow_hours_total_min, 0),
|
|
380
|
-
upper=extract_data(self.element.flow_hours_total_max, np.inf),
|
|
381
|
-
coords=self._model.get_coords(time_dim=False),
|
|
382
|
-
name=f'{self.label_full}|total_flow_hours',
|
|
527
|
+
# Effects
|
|
528
|
+
self._create_shares()
|
|
529
|
+
|
|
530
|
+
def _create_on_off_model(self):
|
|
531
|
+
on = self.add_variables(binary=True, short_name='on', coords=self._model.get_coords())
|
|
532
|
+
self.add_submodels(
|
|
533
|
+
OnOffModel(
|
|
534
|
+
model=self._model,
|
|
535
|
+
label_of_element=self.label_of_element,
|
|
536
|
+
parameters=self.element.on_off_parameters,
|
|
537
|
+
on_variable=on,
|
|
538
|
+
previous_states=self.previous_states,
|
|
539
|
+
label_of_model=self.label_of_element,
|
|
383
540
|
),
|
|
384
|
-
'
|
|
541
|
+
short_name='on_off',
|
|
385
542
|
)
|
|
386
543
|
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
544
|
+
def _create_investment_model(self):
|
|
545
|
+
self.add_submodels(
|
|
546
|
+
InvestmentModel(
|
|
547
|
+
model=self._model,
|
|
548
|
+
label_of_element=self.label_of_element,
|
|
549
|
+
parameters=self.element.size,
|
|
550
|
+
label_of_model=self.label_of_element,
|
|
391
551
|
),
|
|
392
|
-
'
|
|
552
|
+
'investment',
|
|
393
553
|
)
|
|
394
554
|
|
|
395
|
-
|
|
396
|
-
self.
|
|
555
|
+
def _constraint_flow_rate(self):
|
|
556
|
+
if not self.with_investment and not self.with_on_off:
|
|
557
|
+
# Most basic case. Already covered by direct variable bounds
|
|
558
|
+
pass
|
|
559
|
+
|
|
560
|
+
elif self.with_on_off and not self.with_investment:
|
|
561
|
+
# OnOff, but no Investment
|
|
562
|
+
self._create_on_off_model()
|
|
563
|
+
bounds = self.relative_flow_rate_bounds
|
|
564
|
+
BoundingPatterns.bounds_with_state(
|
|
565
|
+
self,
|
|
566
|
+
variable=self.flow_rate,
|
|
567
|
+
bounds=(bounds[0] * self.element.size, bounds[1] * self.element.size),
|
|
568
|
+
variable_state=self.on_off.on,
|
|
569
|
+
)
|
|
397
570
|
|
|
398
|
-
|
|
399
|
-
|
|
571
|
+
elif self.with_investment and not self.with_on_off:
|
|
572
|
+
# Investment, but no OnOff
|
|
573
|
+
self._create_investment_model()
|
|
574
|
+
BoundingPatterns.scaled_bounds(
|
|
575
|
+
self,
|
|
576
|
+
variable=self.flow_rate,
|
|
577
|
+
scaling_variable=self.investment.size,
|
|
578
|
+
relative_bounds=self.relative_flow_rate_bounds,
|
|
579
|
+
)
|
|
580
|
+
|
|
581
|
+
elif self.with_investment and self.with_on_off:
|
|
582
|
+
# Investment and OnOff
|
|
583
|
+
self._create_investment_model()
|
|
584
|
+
self._create_on_off_model()
|
|
585
|
+
|
|
586
|
+
BoundingPatterns.scaled_bounds_with_state(
|
|
587
|
+
model=self,
|
|
588
|
+
variable=self.flow_rate,
|
|
589
|
+
scaling_variable=self._investment.size,
|
|
590
|
+
relative_bounds=self.relative_flow_rate_bounds,
|
|
591
|
+
scaling_bounds=(self.element.size.minimum_or_fixed_size, self.element.size.maximum_or_fixed_size),
|
|
592
|
+
variable_state=self.on_off.on,
|
|
593
|
+
)
|
|
594
|
+
else:
|
|
595
|
+
raise Exception('Not valid')
|
|
596
|
+
|
|
597
|
+
@property
|
|
598
|
+
def with_on_off(self) -> bool:
|
|
599
|
+
return self.element.on_off_parameters is not None
|
|
600
|
+
|
|
601
|
+
@property
|
|
602
|
+
def with_investment(self) -> bool:
|
|
603
|
+
return isinstance(self.element.size, InvestParameters)
|
|
604
|
+
|
|
605
|
+
# Properties for clean access to variables
|
|
606
|
+
@property
|
|
607
|
+
def flow_rate(self) -> linopy.Variable:
|
|
608
|
+
"""Main flow rate variable"""
|
|
609
|
+
return self['flow_rate']
|
|
610
|
+
|
|
611
|
+
@property
|
|
612
|
+
def total_flow_hours(self) -> linopy.Variable:
|
|
613
|
+
"""Total flow hours variable"""
|
|
614
|
+
return self['total_flow_hours']
|
|
400
615
|
|
|
401
616
|
def results_structure(self):
|
|
402
617
|
return {
|
|
@@ -407,158 +622,161 @@ class FlowModel(ElementModel):
|
|
|
407
622
|
}
|
|
408
623
|
|
|
409
624
|
def _create_shares(self):
|
|
410
|
-
#
|
|
411
|
-
if self.element.effects_per_flow_hour
|
|
625
|
+
# Effects per flow hour
|
|
626
|
+
if self.element.effects_per_flow_hour:
|
|
412
627
|
self._model.effects.add_share_to_effects(
|
|
413
|
-
name=self.label_full,
|
|
628
|
+
name=self.label_full,
|
|
414
629
|
expressions={
|
|
415
|
-
effect: self.flow_rate * self._model.hours_per_step * factor
|
|
630
|
+
effect: self.flow_rate * self._model.hours_per_step * factor
|
|
416
631
|
for effect, factor in self.element.effects_per_flow_hour.items()
|
|
417
632
|
},
|
|
418
|
-
target='
|
|
633
|
+
target='temporal',
|
|
419
634
|
)
|
|
420
635
|
|
|
421
636
|
def _create_bounds_for_load_factor(self):
|
|
422
|
-
|
|
637
|
+
"""Create load factor constraints using current approach"""
|
|
638
|
+
# Get the size (either from element or investment)
|
|
639
|
+
size = self.investment.size if self.with_investment else self.element.size
|
|
423
640
|
|
|
424
|
-
#
|
|
641
|
+
# Maximum load factor constraint
|
|
425
642
|
if self.element.load_factor_max is not None:
|
|
426
|
-
name_short = 'load_factor_max'
|
|
427
643
|
flow_hours_per_size_max = self._model.hours_per_step.sum('time') * self.element.load_factor_max
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
self._model.add_constraints(
|
|
432
|
-
self.total_flow_hours <= size * flow_hours_per_size_max,
|
|
433
|
-
name=f'{self.label_full}|{name_short}',
|
|
434
|
-
),
|
|
435
|
-
name_short,
|
|
644
|
+
self.add_constraints(
|
|
645
|
+
self.total_flow_hours <= size * flow_hours_per_size_max,
|
|
646
|
+
short_name='load_factor_max',
|
|
436
647
|
)
|
|
437
648
|
|
|
438
|
-
#
|
|
649
|
+
# Minimum load factor constraint
|
|
439
650
|
if self.element.load_factor_min is not None:
|
|
440
|
-
name_short = 'load_factor_min'
|
|
441
651
|
flow_hours_per_size_min = self._model.hours_per_step.sum('time') * self.element.load_factor_min
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
self._model.add_constraints(
|
|
446
|
-
self.total_flow_hours >= size * flow_hours_per_size_min,
|
|
447
|
-
name=f'{self.label_full}|{name_short}',
|
|
448
|
-
),
|
|
449
|
-
name_short,
|
|
652
|
+
self.add_constraints(
|
|
653
|
+
self.total_flow_hours >= size * flow_hours_per_size_min,
|
|
654
|
+
short_name='load_factor_min',
|
|
450
655
|
)
|
|
451
656
|
|
|
452
657
|
@property
|
|
453
|
-
def
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
if not isinstance(size, InvestParameters):
|
|
458
|
-
return relative_minimum * extract_data(size), relative_maximum * extract_data(size)
|
|
459
|
-
if size.fixed_size is not None:
|
|
460
|
-
return (relative_minimum * extract_data(size.fixed_size),
|
|
461
|
-
relative_maximum * extract_data(size.fixed_size))
|
|
462
|
-
return (relative_minimum * extract_data(size.minimum_size),
|
|
463
|
-
relative_maximum * extract_data(size.maximum_size))
|
|
658
|
+
def relative_flow_rate_bounds(self) -> tuple[TemporalData, TemporalData]:
|
|
659
|
+
if self.element.fixed_relative_profile is not None:
|
|
660
|
+
return self.element.fixed_relative_profile, self.element.fixed_relative_profile
|
|
661
|
+
return self.element.relative_minimum, self.element.relative_maximum
|
|
464
662
|
|
|
465
663
|
@property
|
|
466
|
-
def
|
|
467
|
-
"""
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
664
|
+
def absolute_flow_rate_bounds(self) -> tuple[TemporalData, TemporalData]:
|
|
665
|
+
"""
|
|
666
|
+
Returns the absolute bounds the flow_rate can reach.
|
|
667
|
+
Further constraining might be needed
|
|
668
|
+
"""
|
|
669
|
+
lb_relative, ub_relative = self.relative_flow_rate_bounds
|
|
670
|
+
|
|
671
|
+
lb = 0
|
|
672
|
+
if not self.with_on_off:
|
|
673
|
+
if not self.with_investment:
|
|
674
|
+
# Basic case without investment and without OnOff
|
|
675
|
+
lb = lb_relative * self.element.size
|
|
676
|
+
elif self.with_investment and self.element.size.mandatory:
|
|
677
|
+
# With mandatory Investment
|
|
678
|
+
lb = lb_relative * self.element.size.minimum_or_fixed_size
|
|
679
|
+
|
|
680
|
+
if self.with_investment:
|
|
681
|
+
ub = ub_relative * self.element.size.maximum_or_fixed_size
|
|
682
|
+
else:
|
|
683
|
+
ub = ub_relative * self.element.size
|
|
684
|
+
|
|
685
|
+
return lb, ub
|
|
472
686
|
|
|
473
687
|
@property
|
|
474
|
-
def
|
|
475
|
-
"""
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
return extract_data(fixed_profile)
|
|
688
|
+
def on_off(self) -> OnOffModel | None:
|
|
689
|
+
"""OnOff feature"""
|
|
690
|
+
if 'on_off' not in self.submodels:
|
|
691
|
+
return None
|
|
692
|
+
return self.submodels['on_off']
|
|
480
693
|
|
|
481
694
|
@property
|
|
482
|
-
def
|
|
483
|
-
"""
|
|
484
|
-
|
|
485
|
-
Further constraining might be done in OnOffModel and InvestmentModel
|
|
486
|
-
"""
|
|
487
|
-
if self.element.on_off_parameters is not None:
|
|
488
|
-
return 0
|
|
489
|
-
if isinstance(self.element.size, InvestParameters):
|
|
490
|
-
if self.element.size.optional:
|
|
491
|
-
return 0
|
|
492
|
-
return self.flow_rate_lower_bound_relative * extract_data(self.element.size.minimum_size)
|
|
493
|
-
return self.flow_rate_lower_bound_relative * extract_data(self.element.size)
|
|
695
|
+
def _investment(self) -> InvestmentModel | None:
|
|
696
|
+
"""Deprecated alias for investment"""
|
|
697
|
+
return self.investment
|
|
494
698
|
|
|
495
699
|
@property
|
|
496
|
-
def
|
|
497
|
-
"""
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
700
|
+
def investment(self) -> InvestmentModel | None:
|
|
701
|
+
"""OnOff feature"""
|
|
702
|
+
if 'investment' not in self.submodels:
|
|
703
|
+
return None
|
|
704
|
+
return self.submodels['investment']
|
|
705
|
+
|
|
706
|
+
@property
|
|
707
|
+
def previous_states(self) -> TemporalData | None:
|
|
708
|
+
"""Previous states of the flow rate"""
|
|
709
|
+
# TODO: This would be nicer to handle in the Flow itself, and allow DataArrays as well.
|
|
710
|
+
previous_flow_rate = self.element.previous_flow_rate
|
|
711
|
+
if previous_flow_rate is None:
|
|
712
|
+
return None
|
|
713
|
+
|
|
714
|
+
return ModelingUtilitiesAbstract.to_binary(
|
|
715
|
+
values=xr.DataArray(
|
|
716
|
+
[previous_flow_rate] if np.isscalar(previous_flow_rate) else previous_flow_rate, dims='time'
|
|
717
|
+
),
|
|
718
|
+
epsilon=CONFIG.Modeling.epsilon,
|
|
719
|
+
dims='time',
|
|
720
|
+
)
|
|
504
721
|
|
|
505
722
|
|
|
506
723
|
class BusModel(ElementModel):
|
|
507
|
-
|
|
724
|
+
element: Bus # Type hint
|
|
725
|
+
|
|
726
|
+
def __init__(self, model: FlowSystemModel, element: Bus):
|
|
727
|
+
self.excess_input: linopy.Variable | None = None
|
|
728
|
+
self.excess_output: linopy.Variable | None = None
|
|
508
729
|
super().__init__(model, element)
|
|
509
|
-
self.element: Bus = element
|
|
510
|
-
self.excess_input: Optional[linopy.Variable] = None
|
|
511
|
-
self.excess_output: Optional[linopy.Variable] = None
|
|
512
730
|
|
|
513
|
-
def
|
|
731
|
+
def _do_modeling(self) -> None:
|
|
732
|
+
super()._do_modeling()
|
|
514
733
|
# inputs == outputs
|
|
515
734
|
for flow in self.element.inputs + self.element.outputs:
|
|
516
|
-
self.
|
|
517
|
-
inputs = sum([flow.
|
|
518
|
-
outputs = sum([flow.
|
|
519
|
-
eq_bus_balance = self.
|
|
735
|
+
self.register_variable(flow.submodel.flow_rate, flow.label_full)
|
|
736
|
+
inputs = sum([flow.submodel.flow_rate for flow in self.element.inputs])
|
|
737
|
+
outputs = sum([flow.submodel.flow_rate for flow in self.element.outputs])
|
|
738
|
+
eq_bus_balance = self.add_constraints(inputs == outputs, short_name='balance')
|
|
520
739
|
|
|
521
740
|
# Fehlerplus/-minus:
|
|
522
741
|
if self.element.with_excess:
|
|
523
|
-
excess_penalty = np.multiply(
|
|
524
|
-
|
|
525
|
-
)
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
),
|
|
530
|
-
'excess_input',
|
|
531
|
-
)
|
|
532
|
-
self.excess_output = self.add(
|
|
533
|
-
self._model.add_variables(
|
|
534
|
-
lower=0, coords=self._model.get_coords(), name=f'{self.label_full}|excess_output'
|
|
535
|
-
),
|
|
536
|
-
'excess_output',
|
|
742
|
+
excess_penalty = np.multiply(self._model.hours_per_step, self.element.excess_penalty_per_flow_hour)
|
|
743
|
+
|
|
744
|
+
self.excess_input = self.add_variables(lower=0, coords=self._model.get_coords(), short_name='excess_input')
|
|
745
|
+
|
|
746
|
+
self.excess_output = self.add_variables(
|
|
747
|
+
lower=0, coords=self._model.get_coords(), short_name='excess_output'
|
|
537
748
|
)
|
|
749
|
+
|
|
538
750
|
eq_bus_balance.lhs -= -self.excess_input + self.excess_output
|
|
539
751
|
|
|
540
752
|
self._model.effects.add_share_to_penalty(self.label_of_element, (self.excess_input * excess_penalty).sum())
|
|
541
753
|
self._model.effects.add_share_to_penalty(self.label_of_element, (self.excess_output * excess_penalty).sum())
|
|
542
754
|
|
|
543
755
|
def results_structure(self):
|
|
544
|
-
inputs = [flow.
|
|
545
|
-
outputs = [flow.
|
|
756
|
+
inputs = [flow.submodel.flow_rate.name for flow in self.element.inputs]
|
|
757
|
+
outputs = [flow.submodel.flow_rate.name for flow in self.element.outputs]
|
|
546
758
|
if self.excess_input is not None:
|
|
547
759
|
inputs.append(self.excess_input.name)
|
|
548
760
|
if self.excess_output is not None:
|
|
549
761
|
outputs.append(self.excess_output.name)
|
|
550
|
-
return {
|
|
551
|
-
|
|
762
|
+
return {
|
|
763
|
+
**super().results_structure(),
|
|
764
|
+
'inputs': inputs,
|
|
765
|
+
'outputs': outputs,
|
|
766
|
+
'flows': [flow.label_full for flow in self.element.inputs + self.element.outputs],
|
|
767
|
+
}
|
|
552
768
|
|
|
553
769
|
|
|
554
770
|
class ComponentModel(ElementModel):
|
|
555
|
-
|
|
771
|
+
element: Component # Type hint
|
|
772
|
+
|
|
773
|
+
def __init__(self, model: FlowSystemModel, element: Component):
|
|
774
|
+
self.on_off: OnOffModel | None = None
|
|
556
775
|
super().__init__(model, element)
|
|
557
|
-
self.element: Component = element
|
|
558
|
-
self.on_off: Optional[OnOffModel] = None
|
|
559
776
|
|
|
560
|
-
def
|
|
777
|
+
def _do_modeling(self):
|
|
561
778
|
"""Initiates all FlowModels"""
|
|
779
|
+
super()._do_modeling()
|
|
562
780
|
all_flows = self.element.inputs + self.element.outputs
|
|
563
781
|
if self.element.on_off_parameters:
|
|
564
782
|
for flow in all_flows:
|
|
@@ -571,35 +789,64 @@ class ComponentModel(ElementModel):
|
|
|
571
789
|
flow.on_off_parameters = OnOffParameters()
|
|
572
790
|
|
|
573
791
|
for flow in all_flows:
|
|
574
|
-
self.
|
|
575
|
-
|
|
576
|
-
for sub_model in self.sub_models:
|
|
577
|
-
sub_model.do_modeling()
|
|
792
|
+
self.add_submodels(flow.create_model(self._model), short_name=flow.label)
|
|
578
793
|
|
|
579
794
|
if self.element.on_off_parameters:
|
|
580
|
-
self.
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
795
|
+
on = self.add_variables(binary=True, short_name='on', coords=self._model.get_coords())
|
|
796
|
+
if len(all_flows) == 1:
|
|
797
|
+
self.add_constraints(on == all_flows[0].submodel.on_off.on, short_name='on')
|
|
798
|
+
else:
|
|
799
|
+
flow_ons = [flow.submodel.on_off.on for flow in all_flows]
|
|
800
|
+
# TODO: Is the EPSILON even necessary?
|
|
801
|
+
self.add_constraints(on <= sum(flow_ons) + CONFIG.Modeling.epsilon, short_name='on|ub')
|
|
802
|
+
self.add_constraints(
|
|
803
|
+
on >= sum(flow_ons) / (len(flow_ons) + CONFIG.Modeling.epsilon), short_name='on|lb'
|
|
588
804
|
)
|
|
589
|
-
)
|
|
590
805
|
|
|
591
|
-
self.on_off.
|
|
806
|
+
self.on_off = self.add_submodels(
|
|
807
|
+
OnOffModel(
|
|
808
|
+
model=self._model,
|
|
809
|
+
label_of_element=self.label_of_element,
|
|
810
|
+
parameters=self.element.on_off_parameters,
|
|
811
|
+
on_variable=on,
|
|
812
|
+
label_of_model=self.label_of_element,
|
|
813
|
+
previous_states=self.previous_states,
|
|
814
|
+
),
|
|
815
|
+
short_name='on_off',
|
|
816
|
+
)
|
|
592
817
|
|
|
593
818
|
if self.element.prevent_simultaneous_flows:
|
|
594
819
|
# Simultanious Useage --> Only One FLow is On at a time, but needs a Binary for every flow
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
820
|
+
ModelingPrimitives.mutual_exclusivity_constraint(
|
|
821
|
+
self,
|
|
822
|
+
binary_variables=[flow.submodel.on_off.on for flow in self.element.prevent_simultaneous_flows],
|
|
823
|
+
short_name='prevent_simultaneous_use',
|
|
824
|
+
)
|
|
598
825
|
|
|
599
826
|
def results_structure(self):
|
|
600
827
|
return {
|
|
601
828
|
**super().results_structure(),
|
|
602
|
-
'inputs': [flow.
|
|
603
|
-
'outputs': [flow.
|
|
829
|
+
'inputs': [flow.submodel.flow_rate.name for flow in self.element.inputs],
|
|
830
|
+
'outputs': [flow.submodel.flow_rate.name for flow in self.element.outputs],
|
|
604
831
|
'flows': [flow.label_full for flow in self.element.inputs + self.element.outputs],
|
|
605
832
|
}
|
|
833
|
+
|
|
834
|
+
@property
|
|
835
|
+
def previous_states(self) -> xr.DataArray | None:
|
|
836
|
+
"""Previous state of the component, derived from its flows"""
|
|
837
|
+
if self.element.on_off_parameters is None:
|
|
838
|
+
raise ValueError(f'OnOffModel not present in \n{self}\nCant access previous_states')
|
|
839
|
+
|
|
840
|
+
previous_states = [flow.submodel.on_off._previous_states for flow in self.element.inputs + self.element.outputs]
|
|
841
|
+
previous_states = [da for da in previous_states if da is not None]
|
|
842
|
+
|
|
843
|
+
if not previous_states: # Empty list
|
|
844
|
+
return None
|
|
845
|
+
|
|
846
|
+
max_len = max(da.sizes['time'] for da in previous_states)
|
|
847
|
+
|
|
848
|
+
padded_previous_states = [
|
|
849
|
+
da.assign_coords(time=range(-da.sizes['time'], 0)).reindex(time=range(-max_len, 0), fill_value=0)
|
|
850
|
+
for da in previous_states
|
|
851
|
+
]
|
|
852
|
+
return xr.concat(padded_previous_states, dim='flow').any(dim='flow').astype(int)
|