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/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
|
|
14
|
-
from .
|
|
15
|
-
from .
|
|
16
|
-
from .
|
|
17
|
-
from .structure import Element, ElementModel,
|
|
15
|
+
from .core import PlausibilityError, Scalar, TemporalData, TemporalDataUser
|
|
16
|
+
from .features import InvestmentModel, OnOffModel
|
|
17
|
+
from .interface import InvestParameters, OnOffParameters
|
|
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,62 +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
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
meta_data: Optional[Dict] = None,
|
|
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,
|
|
170
377
|
):
|
|
171
|
-
r"""
|
|
172
|
-
Args:
|
|
173
|
-
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.
|
|
174
|
-
bus: blabel of the bus the flow is connected to.
|
|
175
|
-
size: size of the flow. If InvestmentParameters is used, size is optimized.
|
|
176
|
-
If size is None, a default value is used.
|
|
177
|
-
relative_minimum: min value is relative_minimum multiplied by size
|
|
178
|
-
relative_maximum: max value is relative_maximum multiplied by size. If size = max then relative_maximum=1
|
|
179
|
-
load_factor_min: minimal load factor general: avg Flow per nominalVal/investSize
|
|
180
|
-
(e.g. boiler, kW/kWh=h; solarthermal: kW/m²;
|
|
181
|
-
def: :math:`load\_factor:= sumFlowHours/ (nominal\_val \cdot \Delta t_{tot})`
|
|
182
|
-
load_factor_max: maximal load factor (see minimal load factor)
|
|
183
|
-
effects_per_flow_hour: operational costs, costs per flow-"work"
|
|
184
|
-
piecewise_effects_per_flow_hour: piecewise relation between flow hours and effects
|
|
185
|
-
on_off_parameters: If present, flow can be "off", i.e. be zero (only relevant if relative_minimum > 0)
|
|
186
|
-
Therefore a binary var "on" is used. Further, several other restrictions and effects can be modeled
|
|
187
|
-
through this On/Off State (See OnOffParameters)
|
|
188
|
-
flow_hours_total_max: maximum flow-hours ("flow-work")
|
|
189
|
-
(if size is not const, maybe load_factor_max is the better choice!)
|
|
190
|
-
flow_hours_total_min: minimum flow-hours ("flow-work")
|
|
191
|
-
(if size is not predefined, maybe load_factor_min is the better choice!)
|
|
192
|
-
fixed_relative_profile: fixed relative values for flow (if given).
|
|
193
|
-
flow_rate(t) := fixed_relative_profile(t) * size(t)
|
|
194
|
-
With this value, the flow_rate is no optimization-variable anymore.
|
|
195
|
-
(relative_minimum and relative_maximum are ignored)
|
|
196
|
-
used for fixed load or supply profiles, i.g. heat demand, wind-power, solarthermal
|
|
197
|
-
If the load-profile is just an upper limit, use relative_maximum instead.
|
|
198
|
-
previous_flow_rate: previous flow rate of the flow. Used to determine if and how long the
|
|
199
|
-
flow is already on / off. If None, the flow is considered to be off for one timestep.
|
|
200
|
-
meta_data: used to store more information about the Element. Is not used internally, but saved in the results. Only use python native types.
|
|
201
|
-
"""
|
|
202
378
|
super().__init__(label, meta_data=meta_data)
|
|
203
|
-
self.size =
|
|
379
|
+
self.size = CONFIG.Modeling.big if size is None else size
|
|
204
380
|
self.relative_minimum = relative_minimum
|
|
205
381
|
self.relative_maximum = relative_maximum
|
|
206
382
|
self.fixed_relative_profile = fixed_relative_profile
|
|
@@ -209,17 +385,14 @@ class Flow(Element):
|
|
|
209
385
|
self.load_factor_max = load_factor_max
|
|
210
386
|
# self.positive_gradient = TimeSeries('positive_gradient', positive_gradient, self)
|
|
211
387
|
self.effects_per_flow_hour = effects_per_flow_hour if effects_per_flow_hour is not None else {}
|
|
212
|
-
self.piecewise_effects_per_flow_hour = piecewise_effects_per_flow_hour
|
|
213
388
|
self.flow_hours_total_max = flow_hours_total_max
|
|
214
389
|
self.flow_hours_total_min = flow_hours_total_min
|
|
215
390
|
self.on_off_parameters = on_off_parameters
|
|
216
391
|
|
|
217
|
-
self.previous_flow_rate =
|
|
218
|
-
previous_flow_rate if not isinstance(previous_flow_rate, list) else np.array(previous_flow_rate)
|
|
219
|
-
)
|
|
392
|
+
self.previous_flow_rate = previous_flow_rate
|
|
220
393
|
|
|
221
394
|
self.component: str = 'UnknownComponent'
|
|
222
|
-
self.is_input_in_component:
|
|
395
|
+
self.is_input_in_component: bool | None = None
|
|
223
396
|
if isinstance(bus, Bus):
|
|
224
397
|
self.bus = bus.label_full
|
|
225
398
|
warnings.warn(
|
|
@@ -233,70 +406,80 @@ class Flow(Element):
|
|
|
233
406
|
self.bus = bus
|
|
234
407
|
self._bus_object = None
|
|
235
408
|
|
|
236
|
-
def create_model(self, model:
|
|
409
|
+
def create_model(self, model: FlowSystemModel) -> FlowModel:
|
|
237
410
|
self._plausibility_checks()
|
|
238
|
-
self.
|
|
239
|
-
return self.
|
|
240
|
-
|
|
241
|
-
def transform_data(self, flow_system:
|
|
242
|
-
|
|
243
|
-
|
|
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
|
|
420
|
+
)
|
|
421
|
+
self.effects_per_flow_hour = flow_system.fit_effects_to_model_coords(
|
|
422
|
+
prefix, self.effects_per_flow_hour, 'per_flow_hour'
|
|
244
423
|
)
|
|
245
|
-
self.
|
|
246
|
-
f'{
|
|
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']
|
|
247
426
|
)
|
|
248
|
-
self.
|
|
249
|
-
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']
|
|
250
429
|
)
|
|
251
|
-
self.
|
|
252
|
-
|
|
430
|
+
self.load_factor_max = flow_system.fit_to_model_coords(
|
|
431
|
+
f'{prefix}|load_factor_max', self.load_factor_max, dims=['period', 'scenario']
|
|
253
432
|
)
|
|
254
|
-
|
|
255
|
-
self.
|
|
433
|
+
self.load_factor_min = flow_system.fit_to_model_coords(
|
|
434
|
+
f'{prefix}|load_factor_min', self.load_factor_min, dims=['period', 'scenario']
|
|
435
|
+
)
|
|
436
|
+
|
|
256
437
|
if self.on_off_parameters is not None:
|
|
257
|
-
self.on_off_parameters.transform_data(flow_system,
|
|
438
|
+
self.on_off_parameters.transform_data(flow_system, prefix)
|
|
258
439
|
if isinstance(self.size, InvestParameters):
|
|
259
|
-
self.size.transform_data(flow_system)
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
infos = super().infos(use_numpy, use_element_label)
|
|
263
|
-
infos['is_input_in_component'] = self.is_input_in_component
|
|
264
|
-
return infos
|
|
265
|
-
|
|
266
|
-
def to_dict(self) -> Dict:
|
|
267
|
-
data = super().to_dict()
|
|
268
|
-
if isinstance(data.get('previous_flow_rate'), np.ndarray):
|
|
269
|
-
data['previous_flow_rate'] = data['previous_flow_rate'].tolist()
|
|
270
|
-
return data
|
|
440
|
+
self.size.transform_data(flow_system, prefix)
|
|
441
|
+
else:
|
|
442
|
+
self.size = flow_system.fit_to_model_coords(f'{prefix}|size', self.size, dims=['period', 'scenario'])
|
|
271
443
|
|
|
272
444
|
def _plausibility_checks(self) -> None:
|
|
273
445
|
# TODO: Incorporate into Variable? (Lower_bound can not be greater than upper bound
|
|
274
|
-
if
|
|
446
|
+
if (self.relative_minimum > self.relative_maximum).any():
|
|
275
447
|
raise PlausibilityError(self.label_full + ': Take care, that relative_minimum <= relative_maximum!')
|
|
276
448
|
|
|
277
|
-
if (
|
|
278
|
-
self.size == CONFIG.
|
|
449
|
+
if not isinstance(self.size, InvestParameters) and (
|
|
450
|
+
np.any(self.size == CONFIG.Modeling.big) and self.fixed_relative_profile is not None
|
|
279
451
|
): # Default Size --> Most likely by accident
|
|
280
452
|
logger.warning(
|
|
281
|
-
f'Flow "{self.
|
|
282
|
-
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", '
|
|
283
455
|
f'the resulting flow_rate will be very high. To fix this, assign a size to the Flow {self}.'
|
|
284
456
|
)
|
|
285
457
|
|
|
286
458
|
if self.fixed_relative_profile is not None and self.on_off_parameters is not None:
|
|
287
|
-
|
|
288
|
-
f'Flow {self.
|
|
289
|
-
f'
|
|
290
|
-
f'if you want to allow flows to be switched on and off.'
|
|
459
|
+
logger.warning(
|
|
460
|
+
f'Flow {self.label_full} has both a fixed_relative_profile and an on_off_parameters.'
|
|
461
|
+
f'This will allow the flow to be switched on and off, effectively differing from the fixed_flow_rate.'
|
|
291
462
|
)
|
|
292
463
|
|
|
293
|
-
if (self.relative_minimum > 0)
|
|
464
|
+
if np.any(self.relative_minimum > 0) and self.on_off_parameters is None:
|
|
294
465
|
logger.warning(
|
|
295
|
-
f'Flow {self.
|
|
466
|
+
f'Flow {self.label_full} has a relative_minimum of {self.relative_minimum} and no on_off_parameters. '
|
|
296
467
|
f'This prevents the flow_rate from switching off (flow_rate = 0). '
|
|
297
468
|
f'Consider using on_off_parameters to allow the flow to be switched on and off.'
|
|
298
469
|
)
|
|
299
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
|
+
|
|
300
483
|
@property
|
|
301
484
|
def label_full(self) -> str:
|
|
302
485
|
return f'{self.component}({self.label})'
|
|
@@ -306,252 +489,294 @@ class Flow(Element):
|
|
|
306
489
|
# Wenn kein InvestParameters existiert --> True; Wenn Investparameter, den Wert davon nehmen
|
|
307
490
|
return False if (isinstance(self.size, InvestParameters) and self.size.fixed_size is None) else True
|
|
308
491
|
|
|
309
|
-
@property
|
|
310
|
-
def invest_is_optional(self) -> bool:
|
|
311
|
-
# Wenn kein InvestParameters existiert: # Investment ist nicht optional -> Keine Variable --> False
|
|
312
|
-
return False if (isinstance(self.size, InvestParameters) and not self.size.optional) else True
|
|
313
|
-
|
|
314
492
|
|
|
315
493
|
class FlowModel(ElementModel):
|
|
316
|
-
|
|
494
|
+
element: Flow # Type hint
|
|
495
|
+
|
|
496
|
+
def __init__(self, model: FlowSystemModel, element: Flow):
|
|
317
497
|
super().__init__(model, element)
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
self.
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
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,
|
|
333
519
|
),
|
|
334
|
-
'
|
|
520
|
+
coords=['period', 'scenario'],
|
|
521
|
+
short_name='total_flow_hours',
|
|
335
522
|
)
|
|
336
523
|
|
|
337
|
-
#
|
|
338
|
-
|
|
339
|
-
self.on_off: OnOffModel = self.add(
|
|
340
|
-
OnOffModel(
|
|
341
|
-
model=self._model,
|
|
342
|
-
label_of_element=self.label_of_element,
|
|
343
|
-
on_off_parameters=self.element.on_off_parameters,
|
|
344
|
-
defining_variables=[self.flow_rate],
|
|
345
|
-
defining_bounds=[self.flow_rate_bounds_on],
|
|
346
|
-
previous_values=[self.element.previous_flow_rate],
|
|
347
|
-
),
|
|
348
|
-
'on_off',
|
|
349
|
-
)
|
|
350
|
-
self.on_off.do_modeling()
|
|
524
|
+
# Load factor constraints
|
|
525
|
+
self._create_bounds_for_load_factor()
|
|
351
526
|
|
|
352
|
-
#
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
),
|
|
366
|
-
'investment',
|
|
367
|
-
)
|
|
368
|
-
self._investment.do_modeling()
|
|
369
|
-
|
|
370
|
-
self.total_flow_hours = self.add(
|
|
371
|
-
self._model.add_variables(
|
|
372
|
-
lower=self.element.flow_hours_total_min if self.element.flow_hours_total_min is not None else 0,
|
|
373
|
-
upper=self.element.flow_hours_total_max if self.element.flow_hours_total_max is not None else np.inf,
|
|
374
|
-
coords=None,
|
|
375
|
-
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,
|
|
376
540
|
),
|
|
377
|
-
'
|
|
541
|
+
short_name='on_off',
|
|
378
542
|
)
|
|
379
543
|
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
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,
|
|
384
551
|
),
|
|
385
|
-
'
|
|
552
|
+
'investment',
|
|
386
553
|
)
|
|
387
554
|
|
|
388
|
-
|
|
389
|
-
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
|
+
)
|
|
390
570
|
|
|
391
|
-
|
|
392
|
-
|
|
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']
|
|
615
|
+
|
|
616
|
+
def results_structure(self):
|
|
617
|
+
return {
|
|
618
|
+
**super().results_structure(),
|
|
619
|
+
'start': self.element.bus if self.element.is_input_in_component else self.element.component,
|
|
620
|
+
'end': self.element.component if self.element.is_input_in_component else self.element.bus,
|
|
621
|
+
'component': self.element.component,
|
|
622
|
+
}
|
|
393
623
|
|
|
394
624
|
def _create_shares(self):
|
|
395
|
-
#
|
|
396
|
-
if self.element.effects_per_flow_hour
|
|
625
|
+
# Effects per flow hour
|
|
626
|
+
if self.element.effects_per_flow_hour:
|
|
397
627
|
self._model.effects.add_share_to_effects(
|
|
398
|
-
name=self.label_full,
|
|
628
|
+
name=self.label_full,
|
|
399
629
|
expressions={
|
|
400
|
-
effect: self.flow_rate * self._model.hours_per_step * factor
|
|
630
|
+
effect: self.flow_rate * self._model.hours_per_step * factor
|
|
401
631
|
for effect, factor in self.element.effects_per_flow_hour.items()
|
|
402
632
|
},
|
|
403
|
-
target='
|
|
404
|
-
)
|
|
405
|
-
|
|
406
|
-
if self.element.piecewise_effects_per_flow_hour is not None:
|
|
407
|
-
self.piecewise_effects = self.add(
|
|
408
|
-
PiecewiseEffectsPerFlowHourModel(
|
|
409
|
-
model=self._model,
|
|
410
|
-
label_of_element=self.label_of_element,
|
|
411
|
-
piecewise_origin=(
|
|
412
|
-
self.flow_rate.name,
|
|
413
|
-
self.element.piecewise_effects_per_flow_hour.piecewise_flow_rate,
|
|
414
|
-
),
|
|
415
|
-
piecewise_shares=self.element.piecewise_effects_per_flow_hour.piecewise_shares,
|
|
416
|
-
zero_point=self.on_off.on if self.on_off is not None else False,
|
|
417
|
-
),
|
|
633
|
+
target='temporal',
|
|
418
634
|
)
|
|
419
|
-
self.piecewise_effects.do_modeling()
|
|
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
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
self.add(
|
|
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,
|
|
643
|
+
flow_hours_per_size_max = self._model.hours_per_step.sum('time') * self.element.load_factor_max
|
|
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
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
self.add(
|
|
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,
|
|
651
|
+
flow_hours_per_size_min = self._model.hours_per_step.sum('time') * self.element.load_factor_min
|
|
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 * size, relative_maximum * size
|
|
459
|
-
if size.fixed_size is not None:
|
|
460
|
-
return relative_minimum * size.fixed_size, relative_maximum * size.fixed_size
|
|
461
|
-
return relative_minimum * size.minimum_size, relative_maximum * 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
|
|
462
662
|
|
|
463
663
|
@property
|
|
464
|
-
def
|
|
465
|
-
"""
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
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
|
|
470
686
|
|
|
471
687
|
@property
|
|
472
|
-
def
|
|
473
|
-
"""
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
return fixed_profile.active_data
|
|
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']
|
|
478
693
|
|
|
479
694
|
@property
|
|
480
|
-
def
|
|
481
|
-
"""
|
|
482
|
-
|
|
483
|
-
Further constraining might be done in OnOffModel and InvestmentModel
|
|
484
|
-
"""
|
|
485
|
-
if self.element.on_off_parameters is not None:
|
|
486
|
-
return 0
|
|
487
|
-
if isinstance(self.element.size, InvestParameters):
|
|
488
|
-
if self.element.size.optional:
|
|
489
|
-
return 0
|
|
490
|
-
return self.flow_rate_lower_bound_relative * self.element.size.minimum_size
|
|
491
|
-
return self.flow_rate_lower_bound_relative * self.element.size
|
|
695
|
+
def _investment(self) -> InvestmentModel | None:
|
|
696
|
+
"""Deprecated alias for investment"""
|
|
697
|
+
return self.investment
|
|
492
698
|
|
|
493
699
|
@property
|
|
494
|
-
def
|
|
495
|
-
"""
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
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
|
+
)
|
|
502
721
|
|
|
503
722
|
|
|
504
723
|
class BusModel(ElementModel):
|
|
505
|
-
|
|
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
|
|
506
729
|
super().__init__(model, element)
|
|
507
|
-
self.element: Bus = element
|
|
508
|
-
self.excess_input: Optional[linopy.Variable] = None
|
|
509
|
-
self.excess_output: Optional[linopy.Variable] = None
|
|
510
730
|
|
|
511
|
-
def
|
|
731
|
+
def _do_modeling(self) -> None:
|
|
732
|
+
super()._do_modeling()
|
|
512
733
|
# inputs == outputs
|
|
513
734
|
for flow in self.element.inputs + self.element.outputs:
|
|
514
|
-
self.
|
|
515
|
-
inputs = sum([flow.
|
|
516
|
-
outputs = sum([flow.
|
|
517
|
-
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')
|
|
518
739
|
|
|
519
740
|
# Fehlerplus/-minus:
|
|
520
741
|
if self.element.with_excess:
|
|
521
|
-
excess_penalty = np.multiply(
|
|
522
|
-
|
|
523
|
-
)
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
'
|
|
527
|
-
)
|
|
528
|
-
self.excess_output = self.add(
|
|
529
|
-
self._model.add_variables(lower=0, coords=self._model.coords, name=f'{self.label_full}|excess_output'),
|
|
530
|
-
'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'
|
|
531
748
|
)
|
|
749
|
+
|
|
532
750
|
eq_bus_balance.lhs -= -self.excess_input + self.excess_output
|
|
533
751
|
|
|
534
752
|
self._model.effects.add_share_to_penalty(self.label_of_element, (self.excess_input * excess_penalty).sum())
|
|
535
753
|
self._model.effects.add_share_to_penalty(self.label_of_element, (self.excess_output * excess_penalty).sum())
|
|
536
754
|
|
|
537
755
|
def results_structure(self):
|
|
538
|
-
inputs = [flow.
|
|
539
|
-
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]
|
|
540
758
|
if self.excess_input is not None:
|
|
541
759
|
inputs.append(self.excess_input.name)
|
|
542
760
|
if self.excess_output is not None:
|
|
543
761
|
outputs.append(self.excess_output.name)
|
|
544
|
-
return {
|
|
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
|
+
}
|
|
545
768
|
|
|
546
769
|
|
|
547
770
|
class ComponentModel(ElementModel):
|
|
548
|
-
|
|
771
|
+
element: Component # Type hint
|
|
772
|
+
|
|
773
|
+
def __init__(self, model: FlowSystemModel, element: Component):
|
|
774
|
+
self.on_off: OnOffModel | None = None
|
|
549
775
|
super().__init__(model, element)
|
|
550
|
-
self.element: Component = element
|
|
551
|
-
self.on_off: Optional[OnOffModel] = None
|
|
552
776
|
|
|
553
|
-
def
|
|
777
|
+
def _do_modeling(self):
|
|
554
778
|
"""Initiates all FlowModels"""
|
|
779
|
+
super()._do_modeling()
|
|
555
780
|
all_flows = self.element.inputs + self.element.outputs
|
|
556
781
|
if self.element.on_off_parameters:
|
|
557
782
|
for flow in all_flows:
|
|
@@ -564,34 +789,64 @@ class ComponentModel(ElementModel):
|
|
|
564
789
|
flow.on_off_parameters = OnOffParameters()
|
|
565
790
|
|
|
566
791
|
for flow in all_flows:
|
|
567
|
-
self.
|
|
568
|
-
|
|
569
|
-
for sub_model in self.sub_models:
|
|
570
|
-
sub_model.do_modeling()
|
|
792
|
+
self.add_submodels(flow.create_model(self._model), short_name=flow.label)
|
|
571
793
|
|
|
572
794
|
if self.element.on_off_parameters:
|
|
573
|
-
self.
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
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'
|
|
581
804
|
)
|
|
582
|
-
)
|
|
583
805
|
|
|
584
|
-
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
|
+
)
|
|
585
817
|
|
|
586
818
|
if self.element.prevent_simultaneous_flows:
|
|
587
819
|
# Simultanious Useage --> Only One FLow is On at a time, but needs a Binary for every flow
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
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
|
+
)
|
|
591
825
|
|
|
592
826
|
def results_structure(self):
|
|
593
827
|
return {
|
|
594
828
|
**super().results_structure(),
|
|
595
|
-
'inputs': [flow.
|
|
596
|
-
'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],
|
|
831
|
+
'flows': [flow.label_full for flow in self.element.inputs + self.element.outputs],
|
|
597
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)
|