flixopt 2.2.0rc2__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 +33 -4
- flixopt/aggregation.py +60 -80
- flixopt/calculation.py +395 -178
- 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 +759 -0
- flixopt/network_app.py +73 -39
- flixopt/plotting.py +294 -138
- flixopt/results.py +1253 -299
- flixopt/solvers.py +25 -21
- flixopt/structure.py +938 -396
- flixopt/utils.py +38 -12
- flixopt-3.0.0.dist-info/METADATA +209 -0
- flixopt-3.0.0.dist-info/RECORD +26 -0
- 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 -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.0.dist-info}/WHEEL +0 -0
- {flixopt-2.2.0rc2.dist-info → flixopt-3.0.0.dist-info}/licenses/LICENSE +0 -0
flixopt/interface.py
CHANGED
|
@@ -3,15 +3,24 @@ This module contains classes to collect Parameters for the Investment and OnOff
|
|
|
3
3
|
These are tightly connected to features.py
|
|
4
4
|
"""
|
|
5
5
|
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
6
8
|
import logging
|
|
7
|
-
|
|
9
|
+
import warnings
|
|
10
|
+
from typing import TYPE_CHECKING, Literal, Optional
|
|
11
|
+
|
|
12
|
+
import numpy as np
|
|
13
|
+
import pandas as pd
|
|
14
|
+
import xarray as xr
|
|
8
15
|
|
|
9
16
|
from .config import CONFIG
|
|
10
|
-
from .core import NumericData, NumericDataTS, Scalar
|
|
11
17
|
from .structure import Interface, register_class_for_io
|
|
12
18
|
|
|
13
19
|
if TYPE_CHECKING: # for type checking and preventing circular imports
|
|
14
|
-
from .
|
|
20
|
+
from collections.abc import Iterator
|
|
21
|
+
|
|
22
|
+
from .core import PeriodicData, PeriodicDataUser, Scalar, TemporalDataUser
|
|
23
|
+
from .effects import PeriodicEffectsUser, TemporalEffectsUser
|
|
15
24
|
from .flow_system import FlowSystem
|
|
16
25
|
|
|
17
26
|
|
|
@@ -20,34 +29,193 @@ logger = logging.getLogger('flixopt')
|
|
|
20
29
|
|
|
21
30
|
@register_class_for_io
|
|
22
31
|
class Piece(Interface):
|
|
23
|
-
|
|
24
|
-
"""
|
|
25
|
-
Define a Piece, which is part of a Piecewise object.
|
|
32
|
+
"""Define a single linear segment with specified domain boundaries.
|
|
26
33
|
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
34
|
+
This class represents one linear segment that will be combined with other
|
|
35
|
+
pieces to form complete piecewise linear functions. Each piece defines
|
|
36
|
+
a domain interval [start, end] where a linear relationship applies.
|
|
37
|
+
|
|
38
|
+
Args:
|
|
39
|
+
start: Lower bound of the domain interval for this linear segment.
|
|
40
|
+
Can be scalar values or time series arrays for time-varying boundaries.
|
|
41
|
+
end: Upper bound of the domain interval for this linear segment.
|
|
42
|
+
Can be scalar values or time series arrays for time-varying boundaries.
|
|
43
|
+
|
|
44
|
+
Examples:
|
|
45
|
+
Basic piece for equipment efficiency curve:
|
|
46
|
+
|
|
47
|
+
```python
|
|
48
|
+
# Single segment from 40% to 80% load
|
|
49
|
+
efficiency_segment = Piece(start=40, end=80)
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
Piece with time-varying boundaries:
|
|
53
|
+
|
|
54
|
+
```python
|
|
55
|
+
# Capacity limits that change seasonally
|
|
56
|
+
seasonal_piece = Piece(
|
|
57
|
+
start=np.array([10, 20, 30, 25]), # Minimum capacity by season
|
|
58
|
+
end=np.array([80, 100, 90, 70]), # Maximum capacity by season
|
|
59
|
+
)
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
Fixed operating point (start equals end):
|
|
63
|
+
|
|
64
|
+
```python
|
|
65
|
+
# Equipment that operates at exactly 50 MW
|
|
66
|
+
fixed_output = Piece(start=50, end=50)
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
Note:
|
|
70
|
+
Individual pieces are building blocks that gain meaning when combined
|
|
71
|
+
into Piecewise functions. See the Piecewise class for information about
|
|
72
|
+
how pieces interact and relate to each other.
|
|
73
|
+
|
|
74
|
+
"""
|
|
75
|
+
|
|
76
|
+
def __init__(self, start: TemporalDataUser, end: TemporalDataUser):
|
|
31
77
|
self.start = start
|
|
32
78
|
self.end = end
|
|
79
|
+
self.has_time_dim = False
|
|
33
80
|
|
|
34
|
-
def transform_data(self, flow_system:
|
|
35
|
-
self.
|
|
36
|
-
self.
|
|
81
|
+
def transform_data(self, flow_system: FlowSystem, name_prefix: str = '') -> None:
|
|
82
|
+
dims = None if self.has_time_dim else ['period', 'scenario']
|
|
83
|
+
self.start = flow_system.fit_to_model_coords(f'{name_prefix}|start', self.start, dims=dims)
|
|
84
|
+
self.end = flow_system.fit_to_model_coords(f'{name_prefix}|end', self.end, dims=dims)
|
|
37
85
|
|
|
38
86
|
|
|
39
87
|
@register_class_for_io
|
|
40
88
|
class Piecewise(Interface):
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
Define a Piecewise, consisting of a list of Pieces.
|
|
89
|
+
"""
|
|
90
|
+
Define a Piecewise, consisting of a list of Pieces.
|
|
44
91
|
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
92
|
+
Args:
|
|
93
|
+
pieces: list of Piece objects defining the linear segments. The arrangement
|
|
94
|
+
and relationships between pieces determine the function behavior:
|
|
95
|
+
- Touching pieces (end of one = start of next) ensure continuity
|
|
96
|
+
- Gaps between pieces create forbidden regions
|
|
97
|
+
- Overlapping pieces provide an extra choice for the optimizer
|
|
98
|
+
|
|
99
|
+
Piece Relationship Patterns:
|
|
100
|
+
**Touching Pieces (Continuous Function)**:
|
|
101
|
+
Pieces that share boundary points create smooth, continuous functions
|
|
102
|
+
without gaps or overlaps.
|
|
103
|
+
|
|
104
|
+
**Gaps Between Pieces (Forbidden Regions)**:
|
|
105
|
+
Non-contiguous pieces with gaps represent forbidden regions.
|
|
106
|
+
For example minimum load requirements or safety zones.
|
|
107
|
+
|
|
108
|
+
**Overlapping Pieces (Flexible Operation)**:
|
|
109
|
+
Pieces with overlapping domains provide optimization flexibility,
|
|
110
|
+
allowing the solver to choose which segment to operate in.
|
|
111
|
+
|
|
112
|
+
Examples:
|
|
113
|
+
Continuous efficiency curve (touching pieces):
|
|
114
|
+
|
|
115
|
+
```python
|
|
116
|
+
efficiency_curve = Piecewise(
|
|
117
|
+
[
|
|
118
|
+
Piece(start=0, end=25), # Low load: 0-25 MW
|
|
119
|
+
Piece(start=25, end=75), # Medium load: 25-75 MW (touches at 25)
|
|
120
|
+
Piece(start=75, end=100), # High load: 75-100 MW (touches at 75)
|
|
121
|
+
]
|
|
122
|
+
)
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
Equipment with forbidden operating range (gap):
|
|
126
|
+
|
|
127
|
+
```python
|
|
128
|
+
turbine_operation = Piecewise(
|
|
129
|
+
[
|
|
130
|
+
Piece(start=0, end=0), # Off state (point operation)
|
|
131
|
+
Piece(start=40, end=100), # Operating range (gap: 0-40 forbidden)
|
|
132
|
+
]
|
|
133
|
+
)
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
Flexible operation with overlapping options:
|
|
137
|
+
|
|
138
|
+
```python
|
|
139
|
+
flexible_operation = Piecewise(
|
|
140
|
+
[
|
|
141
|
+
Piece(start=20, end=60), # Standard efficiency mode
|
|
142
|
+
Piece(start=50, end=90), # High efficiency mode (overlap: 50-60)
|
|
143
|
+
]
|
|
144
|
+
)
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
Tiered pricing structure:
|
|
148
|
+
|
|
149
|
+
```python
|
|
150
|
+
electricity_pricing = Piecewise(
|
|
151
|
+
[
|
|
152
|
+
Piece(start=0, end=100), # Tier 1: 0-100 kWh
|
|
153
|
+
Piece(start=100, end=500), # Tier 2: 100-500 kWh
|
|
154
|
+
Piece(start=500, end=1000), # Tier 3: 500-1000 kWh
|
|
155
|
+
]
|
|
156
|
+
)
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
Seasonal capacity variation:
|
|
160
|
+
|
|
161
|
+
```python
|
|
162
|
+
seasonal_capacity = Piecewise(
|
|
163
|
+
[
|
|
164
|
+
Piece(start=[10, 15, 20, 12], end=[80, 90, 85, 75]), # Varies by time
|
|
165
|
+
]
|
|
166
|
+
)
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
Container Operations:
|
|
170
|
+
The Piecewise class supports standard Python container operations:
|
|
171
|
+
|
|
172
|
+
```python
|
|
173
|
+
piecewise = Piecewise([piece1, piece2, piece3])
|
|
174
|
+
|
|
175
|
+
len(piecewise) # Returns number of pieces (3)
|
|
176
|
+
piecewise[0] # Access first piece
|
|
177
|
+
for piece in piecewise: # Iterate over all pieces
|
|
178
|
+
print(piece.start, piece.end)
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
Validation Considerations:
|
|
182
|
+
- Pieces are typically ordered by their start values
|
|
183
|
+
- Check for unintended gaps that might create infeasible regions
|
|
184
|
+
- Consider whether overlaps provide desired flexibility or create ambiguity
|
|
185
|
+
- Ensure time-varying pieces have consistent dimensions
|
|
186
|
+
|
|
187
|
+
Common Use Cases:
|
|
188
|
+
- Power plants: Heat rate curves, efficiency vs load, emissions profiles
|
|
189
|
+
- HVAC systems: COP vs temperature, capacity vs conditions
|
|
190
|
+
- Industrial processes: Conversion rates vs throughput, quality vs speed
|
|
191
|
+
- Financial modeling: Tiered rates, progressive taxes, bulk discounts
|
|
192
|
+
- Transportation: Fuel efficiency curves, capacity vs speed
|
|
193
|
+
- Storage systems: Efficiency vs state of charge, power vs energy
|
|
194
|
+
- Renewable energy: Output vs weather conditions, curtailment strategies
|
|
195
|
+
|
|
196
|
+
"""
|
|
197
|
+
|
|
198
|
+
def __init__(self, pieces: list[Piece]):
|
|
48
199
|
self.pieces = pieces
|
|
200
|
+
self._has_time_dim = False
|
|
201
|
+
|
|
202
|
+
@property
|
|
203
|
+
def has_time_dim(self):
|
|
204
|
+
return self._has_time_dim
|
|
205
|
+
|
|
206
|
+
@has_time_dim.setter
|
|
207
|
+
def has_time_dim(self, value):
|
|
208
|
+
self._has_time_dim = value
|
|
209
|
+
for piece in self.pieces:
|
|
210
|
+
piece.has_time_dim = value
|
|
49
211
|
|
|
50
212
|
def __len__(self):
|
|
213
|
+
"""
|
|
214
|
+
Return the number of Piece segments in this Piecewise container.
|
|
215
|
+
|
|
216
|
+
Returns:
|
|
217
|
+
int: Count of contained Piece objects.
|
|
218
|
+
"""
|
|
51
219
|
return len(self.pieces)
|
|
52
220
|
|
|
53
221
|
def __getitem__(self, index) -> Piece:
|
|
@@ -56,43 +224,48 @@ class Piecewise(Interface):
|
|
|
56
224
|
def __iter__(self) -> Iterator[Piece]:
|
|
57
225
|
return iter(self.pieces) # Enables iteration like for piece in piecewise: ...
|
|
58
226
|
|
|
59
|
-
def transform_data(self, flow_system:
|
|
227
|
+
def transform_data(self, flow_system: FlowSystem, name_prefix: str = '') -> None:
|
|
60
228
|
for i, piece in enumerate(self.pieces):
|
|
61
229
|
piece.transform_data(flow_system, f'{name_prefix}|Piece{i}')
|
|
62
230
|
|
|
63
231
|
|
|
64
232
|
@register_class_for_io
|
|
65
233
|
class PiecewiseConversion(Interface):
|
|
66
|
-
"""Define piecewise linear
|
|
234
|
+
"""Define coordinated piecewise linear relationships between multiple flows.
|
|
67
235
|
|
|
68
|
-
This class models
|
|
69
|
-
|
|
236
|
+
This class models conversion processes where multiple flows (inputs, outputs,
|
|
237
|
+
auxiliaries) have synchronized piecewise relationships. All flows change
|
|
238
|
+
together based on the same operating point, enabling accurate modeling of
|
|
239
|
+
complex equipment with variable performance characteristics.
|
|
70
240
|
|
|
71
|
-
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
241
|
+
Multi-Flow Coordination:
|
|
242
|
+
All piecewise functions must have matching piece structures (same number
|
|
243
|
+
of pieces with compatible domains) to ensure synchronized operation.
|
|
244
|
+
When the equipment operates at a given point, ALL flows scale proportionally
|
|
245
|
+
within their respective pieces.
|
|
75
246
|
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
Values are Piecewise objects defining conversion factors at different operating points.
|
|
80
|
-
All Piecewise objects must have the same number of pieces and compatible domains
|
|
81
|
-
to ensure consistent conversion relationships across operating ranges.
|
|
247
|
+
Mathematical Formulation:
|
|
248
|
+
See the complete mathematical model in the documentation:
|
|
249
|
+
[Piecewise](../user-guide/mathematical-notation/features/Piecewise.md)
|
|
82
250
|
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
251
|
+
Args:
|
|
252
|
+
piecewises: Dictionary mapping flow labels to their Piecewise functions.
|
|
253
|
+
Keys are flow identifiers (e.g., 'electricity_in', 'heat_out', 'fuel_consumed').
|
|
254
|
+
Values are Piecewise objects that define each flow's behavior.
|
|
255
|
+
**Critical Requirement**: All Piecewise objects must have the same
|
|
256
|
+
number of pieces with compatible domains to ensure consistent operation.
|
|
257
|
+
|
|
258
|
+
Operating Point Coordination:
|
|
259
|
+
When equipment operates at any point within a piece, all flows scale
|
|
260
|
+
proportionally within their corresponding pieces. This ensures realistic
|
|
261
|
+
equipment behavior where efficiency, consumption, and production rates
|
|
262
|
+
all change together.
|
|
90
263
|
|
|
91
264
|
Examples:
|
|
92
|
-
Heat pump with
|
|
265
|
+
Heat pump with coordinated efficiency changes:
|
|
93
266
|
|
|
94
267
|
```python
|
|
95
|
-
PiecewiseConversion(
|
|
268
|
+
heat_pump_pc = PiecewiseConversion(
|
|
96
269
|
{
|
|
97
270
|
'electricity_in': Piecewise(
|
|
98
271
|
[
|
|
@@ -102,423 +275,1052 @@ class PiecewiseConversion(Interface):
|
|
|
102
275
|
),
|
|
103
276
|
'heat_out': Piecewise(
|
|
104
277
|
[
|
|
105
|
-
Piece(0, 35), # Low load COP=3.5: 0-35 kW heat
|
|
106
|
-
Piece(35, 75), # High load COP=3.0: 35-75 kW heat
|
|
278
|
+
Piece(0, 35), # Low load COP=3.5: 0-35 kW heat
|
|
279
|
+
Piece(35, 75), # High load COP=3.0: 35-75 kW heat
|
|
280
|
+
]
|
|
281
|
+
),
|
|
282
|
+
'cooling_water': Piecewise(
|
|
283
|
+
[
|
|
284
|
+
Piece(0, 2.5), # Low load: 0-2.5 m³/h cooling
|
|
285
|
+
Piece(2.5, 6), # High load: 2.5-6 m³/h cooling
|
|
107
286
|
]
|
|
108
287
|
),
|
|
109
288
|
}
|
|
110
289
|
)
|
|
111
|
-
# At 15 kW electricity
|
|
290
|
+
# At 15 kW electricity → 52.5 kW heat + 3.75 m³/h cooling water
|
|
112
291
|
```
|
|
113
292
|
|
|
114
|
-
|
|
293
|
+
Combined cycle power plant with synchronized flows:
|
|
115
294
|
|
|
116
295
|
```python
|
|
117
|
-
PiecewiseConversion(
|
|
296
|
+
power_plant_pc = PiecewiseConversion(
|
|
118
297
|
{
|
|
119
|
-
'
|
|
298
|
+
'natural_gas': Piecewise(
|
|
120
299
|
[
|
|
121
|
-
Piece(
|
|
122
|
-
Piece(
|
|
300
|
+
Piece(150, 300), # Part load: 150-300 MW_th fuel
|
|
301
|
+
Piece(300, 500), # Full load: 300-500 MW_th fuel
|
|
123
302
|
]
|
|
124
303
|
),
|
|
125
|
-
'
|
|
304
|
+
'electricity': Piecewise(
|
|
126
305
|
[
|
|
127
|
-
Piece(
|
|
128
|
-
Piece(
|
|
306
|
+
Piece(60, 135), # Part load: 60-135 MW_e (45% efficiency)
|
|
307
|
+
Piece(135, 250), # Full load: 135-250 MW_e (50% efficiency)
|
|
308
|
+
]
|
|
309
|
+
),
|
|
310
|
+
'steam_export': Piecewise(
|
|
311
|
+
[
|
|
312
|
+
Piece(20, 35), # Part load: 20-35 MW_th steam
|
|
313
|
+
Piece(35, 50), # Full load: 35-50 MW_th steam
|
|
129
314
|
]
|
|
130
315
|
),
|
|
131
316
|
'co2_emissions': Piecewise(
|
|
132
317
|
[
|
|
133
|
-
Piece(
|
|
134
|
-
Piece(
|
|
318
|
+
Piece(30, 60), # Part load: 30-60 t/h CO2
|
|
319
|
+
Piece(60, 100), # Full load: 60-100 t/h CO2
|
|
135
320
|
]
|
|
136
321
|
),
|
|
137
322
|
}
|
|
138
323
|
)
|
|
139
324
|
```
|
|
140
325
|
|
|
141
|
-
|
|
326
|
+
Chemical reactor with multiple products and waste:
|
|
142
327
|
|
|
143
328
|
```python
|
|
144
|
-
PiecewiseConversion(
|
|
329
|
+
reactor_pc = PiecewiseConversion(
|
|
145
330
|
{
|
|
146
|
-
'
|
|
331
|
+
'feedstock': Piecewise(
|
|
147
332
|
[
|
|
148
|
-
Piece(
|
|
149
|
-
Piece(
|
|
333
|
+
Piece(10, 50), # Small batch: 10-50 kg/h
|
|
334
|
+
Piece(50, 200), # Large batch: 50-200 kg/h
|
|
150
335
|
]
|
|
151
336
|
),
|
|
152
|
-
'
|
|
337
|
+
'product_A': Piecewise(
|
|
153
338
|
[
|
|
154
|
-
Piece(
|
|
155
|
-
Piece(
|
|
339
|
+
Piece(7, 35), # Small batch: 70% yield
|
|
340
|
+
Piece(35, 140), # Large batch: 70% yield
|
|
341
|
+
]
|
|
342
|
+
),
|
|
343
|
+
'product_B': Piecewise(
|
|
344
|
+
[
|
|
345
|
+
Piece(2, 10), # Small batch: 20% yield
|
|
346
|
+
Piece(10, 45), # Large batch: 22.5% yield (improved)
|
|
347
|
+
]
|
|
348
|
+
),
|
|
349
|
+
'waste_stream': Piecewise(
|
|
350
|
+
[
|
|
351
|
+
Piece(1, 5), # Small batch: 10% waste
|
|
352
|
+
Piece(5, 15), # Large batch: 7.5% waste (efficiency)
|
|
156
353
|
]
|
|
157
354
|
),
|
|
158
355
|
}
|
|
159
356
|
)
|
|
160
357
|
```
|
|
161
358
|
|
|
162
|
-
Equipment with
|
|
359
|
+
Equipment with discrete operating modes:
|
|
163
360
|
|
|
164
361
|
```python
|
|
165
|
-
PiecewiseConversion(
|
|
362
|
+
compressor_pc = PiecewiseConversion(
|
|
166
363
|
{
|
|
167
|
-
'
|
|
364
|
+
'electricity': Piecewise(
|
|
168
365
|
[
|
|
169
|
-
Piece(0,
|
|
170
|
-
Piece(
|
|
366
|
+
Piece(0, 0), # Off mode: no consumption
|
|
367
|
+
Piece(45, 45), # Low mode: fixed 45 kW
|
|
368
|
+
Piece(85, 85), # High mode: fixed 85 kW
|
|
171
369
|
]
|
|
172
370
|
),
|
|
173
|
-
'
|
|
371
|
+
'compressed_air': Piecewise(
|
|
174
372
|
[
|
|
175
|
-
Piece(0,
|
|
176
|
-
Piece(
|
|
373
|
+
Piece(0, 0), # Off mode: no production
|
|
374
|
+
Piece(250, 250), # Low mode: 250 Nm³/h
|
|
375
|
+
Piece(500, 500), # High mode: 500 Nm³/h
|
|
177
376
|
]
|
|
178
377
|
),
|
|
179
378
|
}
|
|
180
379
|
)
|
|
181
380
|
```
|
|
182
381
|
|
|
183
|
-
|
|
382
|
+
Equipment with forbidden operating range:
|
|
184
383
|
|
|
185
384
|
```python
|
|
186
|
-
|
|
385
|
+
steam_turbine_pc = PiecewiseConversion(
|
|
187
386
|
{
|
|
188
|
-
'
|
|
189
|
-
[
|
|
190
|
-
fx.Piece(10, 50), # Small batch: 10-50 kg/h
|
|
191
|
-
fx.Piece(50, 200), # Large batch: 50-200 kg/h
|
|
192
|
-
]
|
|
193
|
-
),
|
|
194
|
-
'product_A': fx.Piecewise(
|
|
387
|
+
'steam_in': Piecewise(
|
|
195
388
|
[
|
|
196
|
-
|
|
197
|
-
|
|
389
|
+
Piece(0, 100), # Low pressure operation
|
|
390
|
+
Piece(200, 500), # High pressure (gap: 100-200 forbidden)
|
|
198
391
|
]
|
|
199
392
|
),
|
|
200
|
-
'
|
|
393
|
+
'electricity_out': Piecewise(
|
|
201
394
|
[
|
|
202
|
-
|
|
203
|
-
|
|
395
|
+
Piece(0, 30), # Low pressure: poor efficiency
|
|
396
|
+
Piece(80, 220), # High pressure: good efficiency
|
|
204
397
|
]
|
|
205
398
|
),
|
|
206
|
-
'
|
|
399
|
+
'condensate_out': Piecewise(
|
|
207
400
|
[
|
|
208
|
-
|
|
209
|
-
|
|
401
|
+
Piece(0, 100), # Low pressure condensate
|
|
402
|
+
Piece(200, 500), # High pressure condensate
|
|
210
403
|
]
|
|
211
404
|
),
|
|
212
405
|
}
|
|
213
406
|
)
|
|
214
407
|
```
|
|
215
408
|
|
|
409
|
+
Design Patterns:
|
|
410
|
+
**Forbidden Ranges**: Use gaps between pieces to model equipment that cannot
|
|
411
|
+
operate in certain ranges (e.g., minimum loads, unstable regions).
|
|
412
|
+
|
|
413
|
+
**Discrete Modes**: Use pieces with identical start/end values to model
|
|
414
|
+
equipment with fixed operating points (e.g., on/off, discrete speeds).
|
|
415
|
+
|
|
416
|
+
**Efficiency Changes**: Coordinate input and output pieces to reflect
|
|
417
|
+
changing conversion efficiency across operating ranges.
|
|
418
|
+
|
|
216
419
|
Common Use Cases:
|
|
217
|
-
-
|
|
218
|
-
-
|
|
219
|
-
-
|
|
220
|
-
-
|
|
221
|
-
- Multi-stage processes
|
|
222
|
-
-
|
|
223
|
-
- Batch processes: Discrete production campaigns
|
|
420
|
+
- Power generation: Multi-fuel plants, cogeneration systems, renewable hybrids
|
|
421
|
+
- HVAC systems: Heat pumps, chillers with variable COP and auxiliary loads
|
|
422
|
+
- Industrial processes: Multi-product reactors, separation units, heat exchangers
|
|
423
|
+
- Transportation: Multi-modal systems, hybrid vehicles, charging infrastructure
|
|
424
|
+
- Water treatment: Multi-stage processes with varying energy and chemical needs
|
|
425
|
+
- Energy storage: Systems with efficiency changes and auxiliary power requirements
|
|
224
426
|
|
|
225
427
|
"""
|
|
226
428
|
|
|
227
|
-
def __init__(self, piecewises:
|
|
429
|
+
def __init__(self, piecewises: dict[str, Piecewise]):
|
|
228
430
|
self.piecewises = piecewises
|
|
431
|
+
self._has_time_dim = True
|
|
432
|
+
self.has_time_dim = True # Initial propagation
|
|
433
|
+
|
|
434
|
+
@property
|
|
435
|
+
def has_time_dim(self):
|
|
436
|
+
return self._has_time_dim
|
|
437
|
+
|
|
438
|
+
@has_time_dim.setter
|
|
439
|
+
def has_time_dim(self, value):
|
|
440
|
+
self._has_time_dim = value
|
|
441
|
+
for piecewise in self.piecewises.values():
|
|
442
|
+
piecewise.has_time_dim = value
|
|
229
443
|
|
|
230
444
|
def items(self):
|
|
445
|
+
"""
|
|
446
|
+
Return an iterator over (flow_label, Piecewise) pairs stored in this PiecewiseConversion.
|
|
447
|
+
|
|
448
|
+
This is a thin convenience wrapper around the internal mapping and yields the same view
|
|
449
|
+
as dict.items(), where each key is a flow label (str) and each value is a Piecewise.
|
|
450
|
+
"""
|
|
231
451
|
return self.piecewises.items()
|
|
232
452
|
|
|
233
|
-
def transform_data(self, flow_system:
|
|
453
|
+
def transform_data(self, flow_system: FlowSystem, name_prefix: str = '') -> None:
|
|
234
454
|
for name, piecewise in self.piecewises.items():
|
|
235
455
|
piecewise.transform_data(flow_system, f'{name_prefix}|{name}')
|
|
236
456
|
|
|
237
457
|
|
|
238
458
|
@register_class_for_io
|
|
239
459
|
class PiecewiseEffects(Interface):
|
|
240
|
-
|
|
241
|
-
"""
|
|
242
|
-
Define piecewise effects related to a variable.
|
|
243
|
-
|
|
244
|
-
Args:
|
|
245
|
-
piecewise_origin: Piecewise of the related variable
|
|
246
|
-
piecewise_shares: Piecewise defining the shares to different Effects
|
|
247
|
-
"""
|
|
248
|
-
self.piecewise_origin = piecewise_origin
|
|
249
|
-
self.piecewise_shares = piecewise_shares
|
|
460
|
+
"""Define how a single decision variable contributes to system effects with piecewise rates.
|
|
250
461
|
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
462
|
+
This class models situations where a decision variable (the origin) generates
|
|
463
|
+
different types of system effects (costs, emissions, resource consumption) at
|
|
464
|
+
rates that change non-linearly with the variable's operating level. Unlike
|
|
465
|
+
PiecewiseConversion which coordinates multiple flows, PiecewiseEffects focuses
|
|
466
|
+
on how one variable impacts multiple system-wide effects.
|
|
256
467
|
|
|
468
|
+
Key Concept - Origin vs. Effects:
|
|
469
|
+
- **Origin**: The primary decision variable (e.g., production level, capacity, size)
|
|
470
|
+
- **Shares**: The amounts which this variable contributes to different system effects
|
|
257
471
|
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
Define piecewise linear relationships between flow rate and various effects (costs, emissions, etc.).
|
|
472
|
+
Relationship to PiecewiseConversion:
|
|
473
|
+
**PiecewiseConversion**: Models synchronized relationships between multiple
|
|
474
|
+
flow variables (e.g., fuel_in, electricity_out, emissions_out all coordinated).
|
|
262
475
|
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
- Pump efficiency curves across operating ranges
|
|
266
|
-
- Emission factors that vary with operating levels
|
|
267
|
-
- Capacity-dependent transportation costs
|
|
268
|
-
- Decision between different operating modes or suppliers
|
|
269
|
-
- Optional equipment activation with minimum flow requirements
|
|
476
|
+
**PiecewiseEffects**: Models how one variable contributes to system-wide
|
|
477
|
+
effects at variable rates (e.g., production_level → costs, emissions, resources).
|
|
270
478
|
|
|
271
479
|
Args:
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
-
|
|
285
|
-
|
|
286
|
-
-
|
|
287
|
-
- Effects are interpolated linearly within each piece
|
|
288
|
-
- All `Piece`s of the different `Piecewise`s at index i are active at the same time
|
|
289
|
-
- A decision whether to utilize the effect can be modeled by defining multiple Pieces for the same flow rate range
|
|
480
|
+
piecewise_origin: Piecewise function defining the behavior of the primary
|
|
481
|
+
decision variable. This establishes the operating domain and ranges.
|
|
482
|
+
piecewise_shares: Dictionary mapping effect names to their rate functions.
|
|
483
|
+
Keys are effect identifiers (e.g., 'cost_per_unit', 'CO2_intensity').
|
|
484
|
+
Values are Piecewise objects defining the contribution rate per unit
|
|
485
|
+
of the origin variable at different operating levels.
|
|
486
|
+
|
|
487
|
+
Mathematical Relationship:
|
|
488
|
+
For each effect: Total_Effect = Origin_Variable × Share_Rate(Origin_Level)
|
|
489
|
+
|
|
490
|
+
This enables modeling of:
|
|
491
|
+
- Economies of scale (decreasing unit costs with volume)
|
|
492
|
+
- Learning curves (improving efficiency with experience)
|
|
493
|
+
- Threshold effects (changing rates at different scales)
|
|
494
|
+
- Progressive pricing (increasing rates with consumption)
|
|
290
495
|
|
|
291
496
|
Examples:
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
497
|
+
Manufacturing with economies of scale:
|
|
498
|
+
|
|
499
|
+
```python
|
|
500
|
+
production_effects = PiecewiseEffects(
|
|
501
|
+
piecewise_origin=Piecewise(
|
|
502
|
+
[
|
|
503
|
+
Piece(0, 1000), # Small scale: 0-1000 units/month
|
|
504
|
+
Piece(1000, 5000), # Medium scale: 1000-5000 units/month
|
|
505
|
+
Piece(5000, 10000), # Large scale: 5000-10000 units/month
|
|
506
|
+
]
|
|
507
|
+
),
|
|
298
508
|
piecewise_shares={
|
|
299
|
-
'
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
509
|
+
'unit_cost': Piecewise(
|
|
510
|
+
[
|
|
511
|
+
Piece(50, 45), # €50-45/unit (scale benefits)
|
|
512
|
+
Piece(45, 35), # €45-35/unit (bulk materials)
|
|
513
|
+
Piece(35, 30), # €35-30/unit (automation benefits)
|
|
514
|
+
]
|
|
515
|
+
),
|
|
516
|
+
'labor_hours': Piecewise(
|
|
517
|
+
[
|
|
518
|
+
Piece(2.5, 2.0), # 2.5-2.0 hours/unit (learning curve)
|
|
519
|
+
Piece(2.0, 1.5), # 2.0-1.5 hours/unit (efficiency gains)
|
|
520
|
+
Piece(1.5, 1.2), # 1.5-1.2 hours/unit (specialization)
|
|
521
|
+
]
|
|
522
|
+
),
|
|
523
|
+
'CO2_intensity': Piecewise(
|
|
524
|
+
[
|
|
525
|
+
Piece(15, 12), # 15-12 kg CO2/unit (process optimization)
|
|
526
|
+
Piece(12, 9), # 12-9 kg CO2/unit (equipment efficiency)
|
|
527
|
+
Piece(9, 7), # 9-7 kg CO2/unit (renewable energy)
|
|
528
|
+
]
|
|
529
|
+
),
|
|
530
|
+
},
|
|
308
531
|
)
|
|
532
|
+
```
|
|
533
|
+
|
|
534
|
+
Power generation with load-dependent characteristics:
|
|
309
535
|
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
536
|
+
```python
|
|
537
|
+
generator_effects = PiecewiseEffects(
|
|
538
|
+
piecewise_origin=Piecewise(
|
|
539
|
+
[
|
|
540
|
+
Piece(50, 200), # Part load operation: 50-200 MW
|
|
541
|
+
Piece(200, 350), # Rated operation: 200-350 MW
|
|
542
|
+
Piece(350, 400), # Overload operation: 350-400 MW
|
|
543
|
+
]
|
|
544
|
+
),
|
|
316
545
|
piecewise_shares={
|
|
317
|
-
'
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
546
|
+
'fuel_rate': Piecewise(
|
|
547
|
+
[
|
|
548
|
+
Piece(12.0, 10.5), # Heat rate: 12.0-10.5 GJ/MWh (part load penalty)
|
|
549
|
+
Piece(10.5, 9.8), # Heat rate: 10.5-9.8 GJ/MWh (optimal efficiency)
|
|
550
|
+
Piece(9.8, 11.2), # Heat rate: 9.8-11.2 GJ/MWh (overload penalty)
|
|
551
|
+
]
|
|
552
|
+
),
|
|
553
|
+
'maintenance_factor': Piecewise(
|
|
554
|
+
[
|
|
555
|
+
Piece(0.8, 1.0), # Low stress operation
|
|
556
|
+
Piece(1.0, 1.0), # Design operation
|
|
557
|
+
Piece(1.0, 1.5), # High stress operation
|
|
558
|
+
]
|
|
559
|
+
),
|
|
560
|
+
'NOx_rate': Piecewise(
|
|
561
|
+
[
|
|
562
|
+
Piece(0.20, 0.15), # NOx: 0.20-0.15 kg/MWh
|
|
563
|
+
Piece(0.15, 0.12), # NOx: 0.15-0.12 kg/MWh (optimal combustion)
|
|
564
|
+
Piece(0.12, 0.25), # NOx: 0.12-0.25 kg/MWh (overload penalties)
|
|
565
|
+
]
|
|
566
|
+
),
|
|
567
|
+
},
|
|
322
568
|
)
|
|
323
|
-
|
|
569
|
+
```
|
|
324
570
|
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
571
|
+
Progressive utility pricing structure:
|
|
572
|
+
|
|
573
|
+
```python
|
|
574
|
+
electricity_billing = PiecewiseEffects(
|
|
575
|
+
piecewise_origin=Piecewise(
|
|
576
|
+
[
|
|
577
|
+
Piece(0, 200), # Basic usage: 0-200 kWh/month
|
|
578
|
+
Piece(200, 800), # Standard usage: 200-800 kWh/month
|
|
579
|
+
Piece(800, 2000), # High usage: 800-2000 kWh/month
|
|
580
|
+
]
|
|
581
|
+
),
|
|
331
582
|
piecewise_shares={
|
|
332
|
-
'
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
583
|
+
'energy_rate': Piecewise(
|
|
584
|
+
[
|
|
585
|
+
Piece(0.12, 0.12), # Basic rate: €0.12/kWh
|
|
586
|
+
Piece(0.18, 0.18), # Standard rate: €0.18/kWh
|
|
587
|
+
Piece(0.28, 0.28), # Premium rate: €0.28/kWh
|
|
588
|
+
]
|
|
589
|
+
),
|
|
590
|
+
'carbon_tax': Piecewise(
|
|
591
|
+
[
|
|
592
|
+
Piece(0.02, 0.02), # Low carbon tax: €0.02/kWh
|
|
593
|
+
Piece(0.03, 0.03), # Medium carbon tax: €0.03/kWh
|
|
594
|
+
Piece(0.05, 0.05), # High carbon tax: €0.05/kWh
|
|
595
|
+
]
|
|
596
|
+
),
|
|
597
|
+
},
|
|
341
598
|
)
|
|
342
|
-
|
|
599
|
+
```
|
|
600
|
+
|
|
601
|
+
Data center with capacity-dependent efficiency:
|
|
343
602
|
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
603
|
+
```python
|
|
604
|
+
datacenter_effects = PiecewiseEffects(
|
|
605
|
+
piecewise_origin=Piecewise(
|
|
606
|
+
[
|
|
607
|
+
Piece(100, 500), # Low utilization: 100-500 servers
|
|
608
|
+
Piece(500, 2000), # Medium utilization: 500-2000 servers
|
|
609
|
+
Piece(2000, 5000), # High utilization: 2000-5000 servers
|
|
610
|
+
]
|
|
611
|
+
),
|
|
347
612
|
piecewise_shares={
|
|
348
|
-
'
|
|
349
|
-
|
|
613
|
+
'power_per_server': Piecewise(
|
|
614
|
+
[
|
|
615
|
+
Piece(0.8, 0.6), # 0.8-0.6 kW/server (inefficient cooling)
|
|
616
|
+
Piece(0.6, 0.4), # 0.6-0.4 kW/server (optimal efficiency)
|
|
617
|
+
Piece(0.4, 0.5), # 0.4-0.5 kW/server (thermal limits)
|
|
618
|
+
]
|
|
619
|
+
),
|
|
620
|
+
'cooling_overhead': Piecewise(
|
|
621
|
+
[
|
|
622
|
+
Piece(0.4, 0.3), # 40%-30% cooling overhead
|
|
623
|
+
Piece(0.3, 0.2), # 30%-20% cooling overhead
|
|
624
|
+
Piece(0.2, 0.25), # 20%-25% cooling overhead
|
|
625
|
+
]
|
|
626
|
+
),
|
|
627
|
+
},
|
|
350
628
|
)
|
|
629
|
+
```
|
|
630
|
+
|
|
631
|
+
Design Patterns:
|
|
632
|
+
**Economies of Scale**: Decreasing unit costs/impacts with increased scale
|
|
633
|
+
**Learning Curves**: Improving efficiency rates with experience/volume
|
|
634
|
+
**Threshold Effects**: Step changes in rates at specific operating levels
|
|
635
|
+
**Progressive Pricing**: Increasing rates for higher consumption levels
|
|
636
|
+
**Capacity Utilization**: Optimal efficiency at design points, penalties at extremes
|
|
637
|
+
|
|
638
|
+
Common Use Cases:
|
|
639
|
+
- Manufacturing: Production scaling, learning effects, quality improvements
|
|
640
|
+
- Energy systems: Generator efficiency curves, renewable capacity factors
|
|
641
|
+
- Logistics: Transportation rates, warehouse utilization, delivery optimization
|
|
642
|
+
- Utilities: Progressive pricing, infrastructure cost allocation
|
|
643
|
+
- Financial services: Risk premiums, transaction fees, volume discounts
|
|
644
|
+
- Environmental modeling: Pollution intensity, resource consumption rates
|
|
351
645
|
|
|
352
646
|
"""
|
|
353
647
|
|
|
354
|
-
def __init__(self,
|
|
355
|
-
self.
|
|
648
|
+
def __init__(self, piecewise_origin: Piecewise, piecewise_shares: dict[str, Piecewise]):
|
|
649
|
+
self.piecewise_origin = piecewise_origin
|
|
356
650
|
self.piecewise_shares = piecewise_shares
|
|
651
|
+
self._has_time_dim = False
|
|
652
|
+
self.has_time_dim = False # Initial propagation
|
|
653
|
+
|
|
654
|
+
@property
|
|
655
|
+
def has_time_dim(self):
|
|
656
|
+
return self._has_time_dim
|
|
657
|
+
|
|
658
|
+
@has_time_dim.setter
|
|
659
|
+
def has_time_dim(self, value):
|
|
660
|
+
self._has_time_dim = value
|
|
661
|
+
self.piecewise_origin.has_time_dim = value
|
|
662
|
+
for piecewise in self.piecewise_shares.values():
|
|
663
|
+
piecewise.has_time_dim = value
|
|
357
664
|
|
|
358
|
-
def transform_data(self, flow_system:
|
|
359
|
-
self.
|
|
360
|
-
for
|
|
361
|
-
piecewise.transform_data(flow_system, f'{name_prefix}|
|
|
665
|
+
def transform_data(self, flow_system: FlowSystem, name_prefix: str = '') -> None:
|
|
666
|
+
self.piecewise_origin.transform_data(flow_system, f'{name_prefix}|PiecewiseEffects|origin')
|
|
667
|
+
for effect, piecewise in self.piecewise_shares.items():
|
|
668
|
+
piecewise.transform_data(flow_system, f'{name_prefix}|PiecewiseEffects|{effect}')
|
|
362
669
|
|
|
363
670
|
|
|
364
671
|
@register_class_for_io
|
|
365
672
|
class InvestParameters(Interface):
|
|
366
|
-
"""
|
|
367
|
-
|
|
673
|
+
"""Define investment decision parameters with flexible sizing and effect modeling.
|
|
674
|
+
|
|
675
|
+
This class models investment decisions in optimization problems, supporting
|
|
676
|
+
both binary (invest/don't invest) and continuous sizing choices with
|
|
677
|
+
comprehensive cost structures. It enables realistic representation of
|
|
678
|
+
investment economics including fixed costs, scale effects, and divestment penalties.
|
|
679
|
+
|
|
680
|
+
Investment Decision Types:
|
|
681
|
+
**Binary Investments**: Fixed size investments creating yes/no decisions
|
|
682
|
+
(e.g., install a specific generator, build a particular facility)
|
|
683
|
+
|
|
684
|
+
**Continuous Sizing**: Variable size investments with minimum/maximum bounds
|
|
685
|
+
(e.g., battery capacity from 10-1000 kWh, pipeline diameter optimization)
|
|
686
|
+
|
|
687
|
+
Cost Modeling Approaches:
|
|
688
|
+
- **Fixed Effects**: One-time costs independent of size (permits, connections)
|
|
689
|
+
- **Specific Effects**: Linear costs proportional to size (€/kW, €/m²)
|
|
690
|
+
- **Piecewise Effects**: Non-linear relationships (bulk discounts, learning curves)
|
|
691
|
+
- **Divestment Effects**: Penalties for not investing (demolition, opportunity costs)
|
|
692
|
+
|
|
693
|
+
Mathematical Formulation:
|
|
694
|
+
See the complete mathematical model in the documentation:
|
|
695
|
+
[InvestParameters](../user-guide/mathematical-notation/features/InvestParameters.md)
|
|
696
|
+
|
|
697
|
+
Args:
|
|
698
|
+
fixed_size: Creates binary decision at this exact size. None allows continuous sizing.
|
|
699
|
+
minimum_size: Lower bound for continuous sizing. Default: CONFIG.Modeling.epsilon.
|
|
700
|
+
Ignored if fixed_size is specified.
|
|
701
|
+
maximum_size: Upper bound for continuous sizing. Default: CONFIG.Modeling.big.
|
|
702
|
+
Ignored if fixed_size is specified.
|
|
703
|
+
mandatory: Controls whether investment is required. When True, forces investment
|
|
704
|
+
to occur (useful for mandatory upgrades or replacement decisions).
|
|
705
|
+
When False (default), optimization can choose not to invest.
|
|
706
|
+
With multiple periods, at least one period has to have an investment.
|
|
707
|
+
effects_of_investment: Fixed costs if investment is made, regardless of size.
|
|
708
|
+
Dict: {'effect_name': value} (e.g., {'cost': 10000}).
|
|
709
|
+
effects_of_investment_per_size: Variable costs proportional to size (per-unit costs).
|
|
710
|
+
Dict: {'effect_name': value/unit} (e.g., {'cost': 1200}).
|
|
711
|
+
piecewise_effects_of_investment: Non-linear costs using PiecewiseEffects.
|
|
712
|
+
Combinable with effects_of_investment and effects_of_investment_per_size.
|
|
713
|
+
effects_of_retirement: Costs incurred if NOT investing (demolition, penalties).
|
|
714
|
+
Dict: {'effect_name': value}.
|
|
715
|
+
|
|
716
|
+
Deprecated Args:
|
|
717
|
+
fix_effects: **Deprecated**. Use `effects_of_investment` instead.
|
|
718
|
+
Will be removed in version 4.0.
|
|
719
|
+
specific_effects: **Deprecated**. Use `effects_of_investment_per_size` instead.
|
|
720
|
+
Will be removed in version 4.0.
|
|
721
|
+
divest_effects: **Deprecated**. Use `effects_of_retirement` instead.
|
|
722
|
+
Will be removed in version 4.0.
|
|
723
|
+
piecewise_effects: **Deprecated**. Use `piecewise_effects_of_investment` instead.
|
|
724
|
+
Will be removed in version 4.0.
|
|
725
|
+
optional: DEPRECATED. Use `mandatory` instead. Opposite of `mandatory`.
|
|
726
|
+
Will be removed in version 4.0.
|
|
727
|
+
linked_periods: Describes which periods are linked. 1 means linked, 0 means size=0. None means no linked periods.
|
|
728
|
+
|
|
729
|
+
Cost Annualization Requirements:
|
|
730
|
+
All cost values must be properly weighted to match the optimization model's time horizon.
|
|
731
|
+
For long-term investments, the cost values should be annualized to the corresponding operation time (annuity).
|
|
732
|
+
|
|
733
|
+
- Use equivalent annual cost (capital cost / equipment lifetime)
|
|
734
|
+
- Apply appropriate discount rates for present value calculations
|
|
735
|
+
- Account for inflation, escalation, and financing costs
|
|
736
|
+
|
|
737
|
+
Example: €1M equipment with 20-year life → €50k/year fixed cost
|
|
738
|
+
|
|
739
|
+
Examples:
|
|
740
|
+
Simple binary investment (solar panels):
|
|
741
|
+
|
|
742
|
+
```python
|
|
743
|
+
solar_investment = InvestParameters(
|
|
744
|
+
fixed_size=100, # 100 kW system (binary decision)
|
|
745
|
+
mandatory=False, # Investment is optional
|
|
746
|
+
effects_of_investment={
|
|
747
|
+
'cost': 25000, # Installation and permitting costs
|
|
748
|
+
'CO2': -50000, # Avoided emissions over lifetime
|
|
749
|
+
},
|
|
750
|
+
effects_of_investment_per_size={
|
|
751
|
+
'cost': 1200, # €1200/kW for panels (annualized)
|
|
752
|
+
'CO2': -800, # kg CO2 avoided per kW annually
|
|
753
|
+
},
|
|
754
|
+
)
|
|
755
|
+
```
|
|
756
|
+
|
|
757
|
+
Flexible sizing with economies of scale:
|
|
758
|
+
|
|
759
|
+
```python
|
|
760
|
+
battery_investment = InvestParameters(
|
|
761
|
+
minimum_size=10, # Minimum viable system size (kWh)
|
|
762
|
+
maximum_size=1000, # Maximum installable capacity
|
|
763
|
+
mandatory=False, # Investment is optional
|
|
764
|
+
effects_of_investment={
|
|
765
|
+
'cost': 5000, # Grid connection and control system
|
|
766
|
+
'installation_time': 2, # Days for fixed components
|
|
767
|
+
},
|
|
768
|
+
piecewise_effects_of_investment=PiecewiseEffects(
|
|
769
|
+
piecewise_origin=Piecewise(
|
|
770
|
+
[
|
|
771
|
+
Piece(0, 100), # Small systems
|
|
772
|
+
Piece(100, 500), # Medium systems
|
|
773
|
+
Piece(500, 1000), # Large systems
|
|
774
|
+
]
|
|
775
|
+
),
|
|
776
|
+
piecewise_shares={
|
|
777
|
+
'cost': Piecewise(
|
|
778
|
+
[
|
|
779
|
+
Piece(800, 750), # High cost/kWh for small systems
|
|
780
|
+
Piece(750, 600), # Medium cost/kWh
|
|
781
|
+
Piece(600, 500), # Bulk discount for large systems
|
|
782
|
+
]
|
|
783
|
+
)
|
|
784
|
+
},
|
|
785
|
+
),
|
|
786
|
+
)
|
|
787
|
+
```
|
|
788
|
+
|
|
789
|
+
Mandatory replacement with retirement costs:
|
|
790
|
+
|
|
791
|
+
```python
|
|
792
|
+
boiler_replacement = InvestParameters(
|
|
793
|
+
minimum_size=50,
|
|
794
|
+
maximum_size=200,
|
|
795
|
+
mandatory=False, # Can choose not to replace
|
|
796
|
+
effects_of_investment={
|
|
797
|
+
'cost': 15000, # Installation costs
|
|
798
|
+
'disruption': 3, # Days of downtime
|
|
799
|
+
},
|
|
800
|
+
effects_of_investment_per_size={
|
|
801
|
+
'cost': 400, # €400/kW capacity
|
|
802
|
+
'maintenance': 25, # Annual maintenance per kW
|
|
803
|
+
},
|
|
804
|
+
effects_of_retirement={
|
|
805
|
+
'cost': 8000, # Demolition if not replaced
|
|
806
|
+
'environmental': 100, # Disposal fees
|
|
807
|
+
},
|
|
808
|
+
)
|
|
809
|
+
```
|
|
810
|
+
|
|
811
|
+
Multi-technology comparison:
|
|
812
|
+
|
|
813
|
+
```python
|
|
814
|
+
# Gas turbine option
|
|
815
|
+
gas_turbine = InvestParameters(
|
|
816
|
+
fixed_size=50, # MW
|
|
817
|
+
effects_of_investment={'cost': 2500000, 'CO2': 1250000},
|
|
818
|
+
effects_of_investment_per_size={'fuel_cost': 45, 'maintenance': 12},
|
|
819
|
+
)
|
|
820
|
+
|
|
821
|
+
# Wind farm option
|
|
822
|
+
wind_farm = InvestParameters(
|
|
823
|
+
minimum_size=20,
|
|
824
|
+
maximum_size=100,
|
|
825
|
+
effects_of_investment={'cost': 1000000, 'CO2': -5000000},
|
|
826
|
+
effects_of_investment_per_size={'cost': 1800000, 'land_use': 0.5},
|
|
827
|
+
)
|
|
828
|
+
```
|
|
829
|
+
|
|
830
|
+
Technology learning curve:
|
|
831
|
+
|
|
832
|
+
```python
|
|
833
|
+
hydrogen_electrolyzer = InvestParameters(
|
|
834
|
+
minimum_size=1,
|
|
835
|
+
maximum_size=50, # MW
|
|
836
|
+
piecewise_effects_of_investment=PiecewiseEffects(
|
|
837
|
+
piecewise_origin=Piecewise(
|
|
838
|
+
[
|
|
839
|
+
Piece(0, 5), # Small scale: early adoption
|
|
840
|
+
Piece(5, 20), # Medium scale: cost reduction
|
|
841
|
+
Piece(20, 50), # Large scale: mature technology
|
|
842
|
+
]
|
|
843
|
+
),
|
|
844
|
+
piecewise_shares={
|
|
845
|
+
'capex': Piecewise(
|
|
846
|
+
[
|
|
847
|
+
Piece(2000, 1800), # Learning reduces costs
|
|
848
|
+
Piece(1800, 1400), # Continued cost reduction
|
|
849
|
+
Piece(1400, 1200), # Technology maturity
|
|
850
|
+
]
|
|
851
|
+
),
|
|
852
|
+
'efficiency': Piecewise(
|
|
853
|
+
[
|
|
854
|
+
Piece(65, 68), # Improving efficiency
|
|
855
|
+
Piece(68, 72), # with scale and experience
|
|
856
|
+
Piece(72, 75), # Best efficiency at scale
|
|
857
|
+
]
|
|
858
|
+
),
|
|
859
|
+
},
|
|
860
|
+
),
|
|
861
|
+
)
|
|
862
|
+
```
|
|
863
|
+
|
|
864
|
+
Common Use Cases:
|
|
865
|
+
- Power generation: Plant sizing, technology selection, retrofit decisions
|
|
866
|
+
- Industrial equipment: Capacity expansion, efficiency upgrades, replacements
|
|
867
|
+
- Infrastructure: Network expansion, facility construction, system upgrades
|
|
868
|
+
- Energy storage: Battery sizing, pumped hydro, compressed air systems
|
|
869
|
+
- Transportation: Fleet expansion, charging infrastructure, modal shifts
|
|
870
|
+
- Buildings: HVAC systems, insulation upgrades, renewable integration
|
|
871
|
+
|
|
368
872
|
"""
|
|
369
873
|
|
|
370
874
|
def __init__(
|
|
371
875
|
self,
|
|
372
|
-
fixed_size:
|
|
373
|
-
minimum_size:
|
|
374
|
-
maximum_size:
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
876
|
+
fixed_size: PeriodicDataUser | None = None,
|
|
877
|
+
minimum_size: PeriodicDataUser | None = None,
|
|
878
|
+
maximum_size: PeriodicDataUser | None = None,
|
|
879
|
+
mandatory: bool = False,
|
|
880
|
+
effects_of_investment: PeriodicEffectsUser | None = None,
|
|
881
|
+
effects_of_investment_per_size: PeriodicEffectsUser | None = None,
|
|
882
|
+
effects_of_retirement: PeriodicEffectsUser | None = None,
|
|
883
|
+
piecewise_effects_of_investment: PiecewiseEffects | None = None,
|
|
884
|
+
linked_periods: PeriodicDataUser | tuple[int, int] | None = None,
|
|
885
|
+
**kwargs,
|
|
380
886
|
):
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
fix_effects
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
self.
|
|
407
|
-
|
|
887
|
+
# Handle deprecated parameters using centralized helper
|
|
888
|
+
effects_of_investment = self._handle_deprecated_kwarg(
|
|
889
|
+
kwargs, 'fix_effects', 'effects_of_investment', effects_of_investment
|
|
890
|
+
)
|
|
891
|
+
effects_of_investment_per_size = self._handle_deprecated_kwarg(
|
|
892
|
+
kwargs, 'specific_effects', 'effects_of_investment_per_size', effects_of_investment_per_size
|
|
893
|
+
)
|
|
894
|
+
effects_of_retirement = self._handle_deprecated_kwarg(
|
|
895
|
+
kwargs, 'divest_effects', 'effects_of_retirement', effects_of_retirement
|
|
896
|
+
)
|
|
897
|
+
piecewise_effects_of_investment = self._handle_deprecated_kwarg(
|
|
898
|
+
kwargs, 'piecewise_effects', 'piecewise_effects_of_investment', piecewise_effects_of_investment
|
|
899
|
+
)
|
|
900
|
+
# For mandatory parameter with non-None default, disable conflict checking
|
|
901
|
+
if 'optional' in kwargs:
|
|
902
|
+
warnings.warn(
|
|
903
|
+
'Deprecated parameter "optional" used. Check conflicts with new parameter "mandatory" manually!',
|
|
904
|
+
DeprecationWarning,
|
|
905
|
+
stacklevel=2,
|
|
906
|
+
)
|
|
907
|
+
mandatory = self._handle_deprecated_kwarg(
|
|
908
|
+
kwargs, 'optional', 'mandatory', mandatory, transform=lambda x: not x, check_conflict=False
|
|
909
|
+
)
|
|
910
|
+
|
|
911
|
+
# Validate any remaining unexpected kwargs
|
|
912
|
+
self._validate_kwargs(kwargs)
|
|
913
|
+
|
|
914
|
+
self.effects_of_investment: PeriodicEffectsUser = (
|
|
915
|
+
effects_of_investment if effects_of_investment is not None else {}
|
|
916
|
+
)
|
|
917
|
+
self.effects_of_retirement: PeriodicEffectsUser = (
|
|
918
|
+
effects_of_retirement if effects_of_retirement is not None else {}
|
|
919
|
+
)
|
|
408
920
|
self.fixed_size = fixed_size
|
|
409
|
-
self.
|
|
410
|
-
self.
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
self.
|
|
921
|
+
self.mandatory = mandatory
|
|
922
|
+
self.effects_of_investment_per_size: PeriodicEffectsUser = (
|
|
923
|
+
effects_of_investment_per_size if effects_of_investment_per_size is not None else {}
|
|
924
|
+
)
|
|
925
|
+
self.piecewise_effects_of_investment = piecewise_effects_of_investment
|
|
926
|
+
self.minimum_size = minimum_size if minimum_size is not None else CONFIG.Modeling.epsilon
|
|
927
|
+
self.maximum_size = maximum_size if maximum_size is not None else CONFIG.Modeling.big # default maximum
|
|
928
|
+
self.linked_periods = linked_periods
|
|
929
|
+
|
|
930
|
+
def transform_data(self, flow_system: FlowSystem, name_prefix: str = '') -> None:
|
|
931
|
+
self.effects_of_investment = flow_system.fit_effects_to_model_coords(
|
|
932
|
+
label_prefix=name_prefix,
|
|
933
|
+
effect_values=self.effects_of_investment,
|
|
934
|
+
label_suffix='effects_of_investment',
|
|
935
|
+
dims=['period', 'scenario'],
|
|
936
|
+
)
|
|
937
|
+
self.effects_of_retirement = flow_system.fit_effects_to_model_coords(
|
|
938
|
+
label_prefix=name_prefix,
|
|
939
|
+
effect_values=self.effects_of_retirement,
|
|
940
|
+
label_suffix='effects_of_retirement',
|
|
941
|
+
dims=['period', 'scenario'],
|
|
942
|
+
)
|
|
943
|
+
self.effects_of_investment_per_size = flow_system.fit_effects_to_model_coords(
|
|
944
|
+
label_prefix=name_prefix,
|
|
945
|
+
effect_values=self.effects_of_investment_per_size,
|
|
946
|
+
label_suffix='effects_of_investment_per_size',
|
|
947
|
+
dims=['period', 'scenario'],
|
|
948
|
+
)
|
|
949
|
+
|
|
950
|
+
if self.piecewise_effects_of_investment is not None:
|
|
951
|
+
self.piecewise_effects_of_investment.has_time_dim = False
|
|
952
|
+
self.piecewise_effects_of_investment.transform_data(flow_system, f'{name_prefix}|PiecewiseEffects')
|
|
953
|
+
|
|
954
|
+
self.minimum_size = flow_system.fit_to_model_coords(
|
|
955
|
+
f'{name_prefix}|minimum_size', self.minimum_size, dims=['period', 'scenario']
|
|
956
|
+
)
|
|
957
|
+
self.maximum_size = flow_system.fit_to_model_coords(
|
|
958
|
+
f'{name_prefix}|maximum_size', self.maximum_size, dims=['period', 'scenario']
|
|
959
|
+
)
|
|
960
|
+
# Convert tuple (first_period, last_period) to DataArray if needed
|
|
961
|
+
if isinstance(self.linked_periods, (tuple, list)):
|
|
962
|
+
if len(self.linked_periods) != 2:
|
|
963
|
+
raise TypeError(
|
|
964
|
+
f'If you provide a tuple to "linked_periods", it needs to be len=2. Got {len(self.linked_periods)=}'
|
|
965
|
+
)
|
|
966
|
+
logger.debug(f'Computing linked_periods from {self.linked_periods}')
|
|
967
|
+
start, end = self.linked_periods
|
|
968
|
+
if start not in flow_system.periods.values:
|
|
969
|
+
logger.warning(
|
|
970
|
+
f'Start of linked periods ({start} not found in periods directly: {flow_system.periods.values}'
|
|
971
|
+
)
|
|
972
|
+
if end not in flow_system.periods.values:
|
|
973
|
+
logger.warning(
|
|
974
|
+
f'End of linked periods ({end} not found in periods directly: {flow_system.periods.values}'
|
|
975
|
+
)
|
|
976
|
+
self.linked_periods = self.compute_linked_periods(start, end, flow_system.periods)
|
|
977
|
+
logger.debug(f'Computed {self.linked_periods=}')
|
|
978
|
+
|
|
979
|
+
self.linked_periods = flow_system.fit_to_model_coords(
|
|
980
|
+
f'{name_prefix}|linked_periods', self.linked_periods, dims=['period', 'scenario']
|
|
981
|
+
)
|
|
982
|
+
self.fixed_size = flow_system.fit_to_model_coords(
|
|
983
|
+
f'{name_prefix}|fixed_size', self.fixed_size, dims=['period', 'scenario']
|
|
984
|
+
)
|
|
985
|
+
|
|
986
|
+
@property
|
|
987
|
+
def optional(self) -> bool:
|
|
988
|
+
"""DEPRECATED: Use 'mandatory' property instead. Returns the opposite of 'mandatory'."""
|
|
989
|
+
import warnings
|
|
990
|
+
|
|
991
|
+
warnings.warn("Property 'optional' is deprecated. Use 'mandatory' instead.", DeprecationWarning, stacklevel=2)
|
|
992
|
+
return not self.mandatory
|
|
414
993
|
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
994
|
+
@optional.setter
|
|
995
|
+
def optional(self, value: bool):
|
|
996
|
+
"""DEPRECATED: Use 'mandatory' property instead. Sets the opposite of the given value to 'mandatory'."""
|
|
997
|
+
warnings.warn("Property 'optional' is deprecated. Use 'mandatory' instead.", DeprecationWarning, stacklevel=2)
|
|
998
|
+
self.mandatory = not value
|
|
419
999
|
|
|
420
1000
|
@property
|
|
421
|
-
def
|
|
422
|
-
|
|
1001
|
+
def fix_effects(self) -> PeriodicEffectsUser:
|
|
1002
|
+
"""Deprecated property. Use effects_of_investment instead."""
|
|
1003
|
+
warnings.warn(
|
|
1004
|
+
'The fix_effects property is deprecated. Use effects_of_investment instead.',
|
|
1005
|
+
DeprecationWarning,
|
|
1006
|
+
stacklevel=2,
|
|
1007
|
+
)
|
|
1008
|
+
return self.effects_of_investment
|
|
1009
|
+
|
|
1010
|
+
@property
|
|
1011
|
+
def specific_effects(self) -> PeriodicEffectsUser:
|
|
1012
|
+
"""Deprecated property. Use effects_of_investment_per_size instead."""
|
|
1013
|
+
warnings.warn(
|
|
1014
|
+
'The specific_effects property is deprecated. Use effects_of_investment_per_size instead.',
|
|
1015
|
+
DeprecationWarning,
|
|
1016
|
+
stacklevel=2,
|
|
1017
|
+
)
|
|
1018
|
+
return self.effects_of_investment_per_size
|
|
1019
|
+
|
|
1020
|
+
@property
|
|
1021
|
+
def divest_effects(self) -> PeriodicEffectsUser:
|
|
1022
|
+
"""Deprecated property. Use effects_of_retirement instead."""
|
|
1023
|
+
warnings.warn(
|
|
1024
|
+
'The divest_effects property is deprecated. Use effects_of_retirement instead.',
|
|
1025
|
+
DeprecationWarning,
|
|
1026
|
+
stacklevel=2,
|
|
1027
|
+
)
|
|
1028
|
+
return self.effects_of_retirement
|
|
423
1029
|
|
|
424
1030
|
@property
|
|
425
|
-
def
|
|
426
|
-
|
|
1031
|
+
def piecewise_effects(self) -> PiecewiseEffects | None:
|
|
1032
|
+
"""Deprecated property. Use piecewise_effects_of_investment instead."""
|
|
1033
|
+
warnings.warn(
|
|
1034
|
+
'The piecewise_effects property is deprecated. Use piecewise_effects_of_investment instead.',
|
|
1035
|
+
DeprecationWarning,
|
|
1036
|
+
stacklevel=2,
|
|
1037
|
+
)
|
|
1038
|
+
return self.piecewise_effects_of_investment
|
|
1039
|
+
|
|
1040
|
+
@property
|
|
1041
|
+
def minimum_or_fixed_size(self) -> PeriodicData:
|
|
1042
|
+
return self.fixed_size if self.fixed_size is not None else self.minimum_size
|
|
1043
|
+
|
|
1044
|
+
@property
|
|
1045
|
+
def maximum_or_fixed_size(self) -> PeriodicData:
|
|
1046
|
+
return self.fixed_size if self.fixed_size is not None else self.maximum_size
|
|
1047
|
+
|
|
1048
|
+
@staticmethod
|
|
1049
|
+
def compute_linked_periods(first_period: int, last_period: int, periods: pd.Index | list[int]) -> xr.DataArray:
|
|
1050
|
+
return xr.DataArray(
|
|
1051
|
+
xr.where(
|
|
1052
|
+
(first_period <= np.array(periods)) & (np.array(periods) <= last_period),
|
|
1053
|
+
1,
|
|
1054
|
+
0,
|
|
1055
|
+
),
|
|
1056
|
+
coords=(pd.Index(periods, name='period'),),
|
|
1057
|
+
).rename('linked_periods')
|
|
427
1058
|
|
|
428
1059
|
|
|
429
1060
|
@register_class_for_io
|
|
430
1061
|
class OnOffParameters(Interface):
|
|
1062
|
+
"""Define operational constraints and effects for binary on/off equipment behavior.
|
|
1063
|
+
|
|
1064
|
+
This class models equipment that operates in discrete states (on/off) rather than
|
|
1065
|
+
continuous operation, capturing realistic operational constraints and associated
|
|
1066
|
+
costs. It handles complex equipment behavior including startup costs, minimum
|
|
1067
|
+
run times, cycling limitations, and maintenance scheduling requirements.
|
|
1068
|
+
|
|
1069
|
+
Key Modeling Capabilities:
|
|
1070
|
+
**Switching Costs**: One-time costs for starting equipment (fuel, wear, labor)
|
|
1071
|
+
**Runtime Constraints**: Minimum and maximum continuous operation periods
|
|
1072
|
+
**Cycling Limits**: Maximum number of starts to prevent excessive wear
|
|
1073
|
+
**Operating Hours**: Total runtime limits and requirements over time horizon
|
|
1074
|
+
|
|
1075
|
+
Typical Equipment Applications:
|
|
1076
|
+
- **Power Plants**: Combined cycle units, steam turbines with startup costs
|
|
1077
|
+
- **Industrial Processes**: Batch reactors, furnaces with thermal cycling
|
|
1078
|
+
- **HVAC Systems**: Chillers, boilers with minimum run times
|
|
1079
|
+
- **Backup Equipment**: Emergency generators, standby systems
|
|
1080
|
+
- **Process Equipment**: Compressors, pumps with operational constraints
|
|
1081
|
+
|
|
1082
|
+
Mathematical Formulation:
|
|
1083
|
+
See the complete mathematical model in the documentation:
|
|
1084
|
+
[OnOffParameters](../user-guide/mathematical-notation/features/OnOffParameters.md)
|
|
1085
|
+
|
|
1086
|
+
Args:
|
|
1087
|
+
effects_per_switch_on: Costs or impacts incurred for each transition from
|
|
1088
|
+
off state (var_on=0) to on state (var_on=1). Represents startup costs,
|
|
1089
|
+
wear and tear, or other switching impacts. Dictionary mapping effect
|
|
1090
|
+
names to values (e.g., {'cost': 500, 'maintenance_hours': 2}).
|
|
1091
|
+
effects_per_running_hour: Ongoing costs or impacts while equipment operates
|
|
1092
|
+
in the on state. Includes fuel costs, labor, consumables, or emissions.
|
|
1093
|
+
Dictionary mapping effect names to hourly values (e.g., {'fuel_cost': 45}).
|
|
1094
|
+
on_hours_total_min: Minimum total operating hours across the entire time horizon.
|
|
1095
|
+
Ensures equipment meets minimum utilization requirements or contractual
|
|
1096
|
+
obligations (e.g., power purchase agreements, maintenance schedules).
|
|
1097
|
+
on_hours_total_max: Maximum total operating hours across the entire time horizon.
|
|
1098
|
+
Limits equipment usage due to maintenance schedules, fuel availability,
|
|
1099
|
+
environmental permits, or equipment lifetime constraints.
|
|
1100
|
+
consecutive_on_hours_min: Minimum continuous operating duration once started.
|
|
1101
|
+
Models minimum run times due to thermal constraints, process stability,
|
|
1102
|
+
or efficiency considerations. Can be time-varying to reflect different
|
|
1103
|
+
constraints across the planning horizon.
|
|
1104
|
+
consecutive_on_hours_max: Maximum continuous operating duration in one campaign.
|
|
1105
|
+
Models mandatory maintenance intervals, process batch sizes, or
|
|
1106
|
+
equipment thermal limits requiring periodic shutdowns.
|
|
1107
|
+
consecutive_off_hours_min: Minimum continuous shutdown duration between operations.
|
|
1108
|
+
Models cooling periods, maintenance requirements, or process constraints
|
|
1109
|
+
that prevent immediate restart after shutdown.
|
|
1110
|
+
consecutive_off_hours_max: Maximum continuous shutdown duration before mandatory
|
|
1111
|
+
restart. Models equipment preservation, process stability, or contractual
|
|
1112
|
+
requirements for minimum activity levels.
|
|
1113
|
+
switch_on_total_max: Maximum number of startup operations across the time horizon.
|
|
1114
|
+
Limits equipment cycling to reduce wear, maintenance costs, or comply
|
|
1115
|
+
with operational constraints (e.g., grid stability requirements).
|
|
1116
|
+
force_switch_on: When True, creates switch-on variables even without explicit
|
|
1117
|
+
switch_on_total_max constraint. Useful for tracking or reporting startup
|
|
1118
|
+
events without enforcing limits.
|
|
1119
|
+
|
|
1120
|
+
Note:
|
|
1121
|
+
**Time Series Boundary Handling**: The final time period constraints for
|
|
1122
|
+
consecutive_on_hours_min/max and consecutive_off_hours_min/max are not
|
|
1123
|
+
enforced, allowing the optimization to end with ongoing campaigns that
|
|
1124
|
+
may be shorter than the specified minimums or longer than maximums.
|
|
1125
|
+
|
|
1126
|
+
Examples:
|
|
1127
|
+
Combined cycle power plant with startup costs and minimum run time:
|
|
1128
|
+
|
|
1129
|
+
```python
|
|
1130
|
+
power_plant_operation = OnOffParameters(
|
|
1131
|
+
effects_per_switch_on={
|
|
1132
|
+
'startup_cost': 25000, # €25,000 per startup
|
|
1133
|
+
'startup_fuel': 150, # GJ natural gas for startup
|
|
1134
|
+
'startup_time': 4, # Hours to reach full output
|
|
1135
|
+
'maintenance_impact': 0.1, # Fractional life consumption
|
|
1136
|
+
},
|
|
1137
|
+
effects_per_running_hour={
|
|
1138
|
+
'fixed_om': 125, # Fixed O&M costs while running
|
|
1139
|
+
'auxiliary_power': 2.5, # MW parasitic loads
|
|
1140
|
+
},
|
|
1141
|
+
consecutive_on_hours_min=8, # Minimum 8-hour run once started
|
|
1142
|
+
consecutive_off_hours_min=4, # Minimum 4-hour cooling period
|
|
1143
|
+
on_hours_total_max=6000, # Annual operating limit
|
|
1144
|
+
)
|
|
1145
|
+
```
|
|
1146
|
+
|
|
1147
|
+
Industrial batch process with cycling limits:
|
|
1148
|
+
|
|
1149
|
+
```python
|
|
1150
|
+
batch_reactor = OnOffParameters(
|
|
1151
|
+
effects_per_switch_on={
|
|
1152
|
+
'setup_cost': 1500, # Labor and materials for startup
|
|
1153
|
+
'catalyst_consumption': 5, # kg catalyst per batch
|
|
1154
|
+
'cleaning_chemicals': 200, # L cleaning solution
|
|
1155
|
+
},
|
|
1156
|
+
effects_per_running_hour={
|
|
1157
|
+
'steam': 2.5, # t/h process steam
|
|
1158
|
+
'electricity': 150, # kWh electrical load
|
|
1159
|
+
'cooling_water': 50, # m³/h cooling water
|
|
1160
|
+
},
|
|
1161
|
+
consecutive_on_hours_min=12, # Minimum batch size (12 hours)
|
|
1162
|
+
consecutive_on_hours_max=24, # Maximum batch size (24 hours)
|
|
1163
|
+
consecutive_off_hours_min=6, # Cleaning and setup time
|
|
1164
|
+
switch_on_total_max=200, # Maximum 200 batches per period
|
|
1165
|
+
on_hours_total_max=4000, # Maximum production time
|
|
1166
|
+
)
|
|
1167
|
+
```
|
|
1168
|
+
|
|
1169
|
+
HVAC system with thermostat control and maintenance:
|
|
1170
|
+
|
|
1171
|
+
```python
|
|
1172
|
+
hvac_operation = OnOffParameters(
|
|
1173
|
+
effects_per_switch_on={
|
|
1174
|
+
'compressor_wear': 0.5, # Hours of compressor life per start
|
|
1175
|
+
'inrush_current': 15, # kW peak demand on startup
|
|
1176
|
+
},
|
|
1177
|
+
effects_per_running_hour={
|
|
1178
|
+
'electricity': 25, # kW electrical consumption
|
|
1179
|
+
'maintenance': 0.12, # €/hour maintenance reserve
|
|
1180
|
+
},
|
|
1181
|
+
consecutive_on_hours_min=1, # Minimum 1-hour run to avoid cycling
|
|
1182
|
+
consecutive_off_hours_min=0.5, # 30-minute minimum off time
|
|
1183
|
+
switch_on_total_max=2000, # Limit cycling for compressor life
|
|
1184
|
+
on_hours_total_min=2000, # Minimum operation for humidity control
|
|
1185
|
+
on_hours_total_max=5000, # Maximum operation for energy budget
|
|
1186
|
+
)
|
|
1187
|
+
```
|
|
1188
|
+
|
|
1189
|
+
Backup generator with testing and maintenance requirements:
|
|
1190
|
+
|
|
1191
|
+
```python
|
|
1192
|
+
backup_generator = OnOffParameters(
|
|
1193
|
+
effects_per_switch_on={
|
|
1194
|
+
'fuel_priming': 50, # L diesel for system priming
|
|
1195
|
+
'wear_factor': 1.0, # Start cycles impact on maintenance
|
|
1196
|
+
'testing_labor': 2, # Hours technician time per test
|
|
1197
|
+
},
|
|
1198
|
+
effects_per_running_hour={
|
|
1199
|
+
'fuel_consumption': 180, # L/h diesel consumption
|
|
1200
|
+
'emissions_permit': 15, # € emissions allowance cost
|
|
1201
|
+
'noise_penalty': 25, # € noise compliance cost
|
|
1202
|
+
},
|
|
1203
|
+
consecutive_on_hours_min=0.5, # Minimum test duration (30 min)
|
|
1204
|
+
consecutive_off_hours_max=720, # Maximum 30 days between tests
|
|
1205
|
+
switch_on_total_max=52, # Weekly testing limit
|
|
1206
|
+
on_hours_total_min=26, # Minimum annual testing (0.5h × 52)
|
|
1207
|
+
on_hours_total_max=200, # Maximum runtime (emergencies + tests)
|
|
1208
|
+
)
|
|
1209
|
+
```
|
|
1210
|
+
|
|
1211
|
+
Peak shaving battery with cycling degradation:
|
|
1212
|
+
|
|
1213
|
+
```python
|
|
1214
|
+
battery_cycling = OnOffParameters(
|
|
1215
|
+
effects_per_switch_on={
|
|
1216
|
+
'cycle_degradation': 0.01, # % capacity loss per cycle
|
|
1217
|
+
'inverter_startup': 0.5, # kWh losses during startup
|
|
1218
|
+
},
|
|
1219
|
+
effects_per_running_hour={
|
|
1220
|
+
'standby_losses': 2, # kW standby consumption
|
|
1221
|
+
'cooling': 5, # kW thermal management
|
|
1222
|
+
'inverter_losses': 8, # kW conversion losses
|
|
1223
|
+
},
|
|
1224
|
+
consecutive_on_hours_min=1, # Minimum discharge duration
|
|
1225
|
+
consecutive_on_hours_max=4, # Maximum continuous discharge
|
|
1226
|
+
consecutive_off_hours_min=1, # Minimum rest between cycles
|
|
1227
|
+
switch_on_total_max=365, # Daily cycling limit
|
|
1228
|
+
force_switch_on=True, # Track all cycling events
|
|
1229
|
+
)
|
|
1230
|
+
```
|
|
1231
|
+
|
|
1232
|
+
Common Use Cases:
|
|
1233
|
+
- Power generation: Thermal plant cycling, renewable curtailment, grid services
|
|
1234
|
+
- Industrial processes: Batch production, maintenance scheduling, equipment rotation
|
|
1235
|
+
- Buildings: HVAC control, lighting systems, elevator operations
|
|
1236
|
+
- Transportation: Fleet management, charging infrastructure, maintenance windows
|
|
1237
|
+
- Storage systems: Battery cycling, pumped hydro, compressed air systems
|
|
1238
|
+
- Emergency equipment: Backup generators, safety systems, emergency lighting
|
|
1239
|
+
|
|
1240
|
+
"""
|
|
1241
|
+
|
|
431
1242
|
def __init__(
|
|
432
1243
|
self,
|
|
433
|
-
effects_per_switch_on:
|
|
434
|
-
effects_per_running_hour:
|
|
435
|
-
on_hours_total_min:
|
|
436
|
-
on_hours_total_max:
|
|
437
|
-
consecutive_on_hours_min:
|
|
438
|
-
consecutive_on_hours_max:
|
|
439
|
-
consecutive_off_hours_min:
|
|
440
|
-
consecutive_off_hours_max:
|
|
441
|
-
switch_on_total_max:
|
|
1244
|
+
effects_per_switch_on: TemporalEffectsUser | None = None,
|
|
1245
|
+
effects_per_running_hour: TemporalEffectsUser | None = None,
|
|
1246
|
+
on_hours_total_min: int | None = None,
|
|
1247
|
+
on_hours_total_max: int | None = None,
|
|
1248
|
+
consecutive_on_hours_min: TemporalDataUser | None = None,
|
|
1249
|
+
consecutive_on_hours_max: TemporalDataUser | None = None,
|
|
1250
|
+
consecutive_off_hours_min: TemporalDataUser | None = None,
|
|
1251
|
+
consecutive_off_hours_max: TemporalDataUser | None = None,
|
|
1252
|
+
switch_on_total_max: int | None = None,
|
|
442
1253
|
force_switch_on: bool = False,
|
|
443
1254
|
):
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
effects_per_switch_on: cost of one switch from off (var_on=0) to on (var_on=1),
|
|
451
|
-
unit i.g. in Euro
|
|
452
|
-
effects_per_running_hour: costs for operating, i.g. in € per hour
|
|
453
|
-
on_hours_total_min: min. overall sum of operating hours.
|
|
454
|
-
on_hours_total_max: max. overall sum of operating hours.
|
|
455
|
-
consecutive_on_hours_min: min sum of operating hours in one piece
|
|
456
|
-
(last on-time period of timeseries is not checked and can be shorter)
|
|
457
|
-
consecutive_on_hours_max: max sum of operating hours in one piece
|
|
458
|
-
consecutive_off_hours_min: min sum of non-operating hours in one piece
|
|
459
|
-
(last off-time period of timeseries is not checked and can be shorter)
|
|
460
|
-
consecutive_off_hours_max: max sum of non-operating hours in one piece
|
|
461
|
-
switch_on_total_max: max nr of switchOn operations
|
|
462
|
-
force_switch_on: force creation of switch on variable, even if there is no switch_on_total_max
|
|
463
|
-
"""
|
|
464
|
-
self.effects_per_switch_on: EffectValuesUser = effects_per_switch_on or {}
|
|
465
|
-
self.effects_per_running_hour: EffectValuesUser = effects_per_running_hour or {}
|
|
1255
|
+
self.effects_per_switch_on: TemporalEffectsUser = (
|
|
1256
|
+
effects_per_switch_on if effects_per_switch_on is not None else {}
|
|
1257
|
+
)
|
|
1258
|
+
self.effects_per_running_hour: TemporalEffectsUser = (
|
|
1259
|
+
effects_per_running_hour if effects_per_running_hour is not None else {}
|
|
1260
|
+
)
|
|
466
1261
|
self.on_hours_total_min: Scalar = on_hours_total_min
|
|
467
1262
|
self.on_hours_total_max: Scalar = on_hours_total_max
|
|
468
|
-
self.consecutive_on_hours_min:
|
|
469
|
-
self.consecutive_on_hours_max:
|
|
470
|
-
self.consecutive_off_hours_min:
|
|
471
|
-
self.consecutive_off_hours_max:
|
|
1263
|
+
self.consecutive_on_hours_min: TemporalDataUser = consecutive_on_hours_min
|
|
1264
|
+
self.consecutive_on_hours_max: TemporalDataUser = consecutive_on_hours_max
|
|
1265
|
+
self.consecutive_off_hours_min: TemporalDataUser = consecutive_off_hours_min
|
|
1266
|
+
self.consecutive_off_hours_max: TemporalDataUser = consecutive_off_hours_max
|
|
472
1267
|
self.switch_on_total_max: Scalar = switch_on_total_max
|
|
473
1268
|
self.force_switch_on: bool = force_switch_on
|
|
474
1269
|
|
|
475
|
-
def transform_data(self, flow_system:
|
|
476
|
-
self.effects_per_switch_on = flow_system.
|
|
1270
|
+
def transform_data(self, flow_system: FlowSystem, name_prefix: str = '') -> None:
|
|
1271
|
+
self.effects_per_switch_on = flow_system.fit_effects_to_model_coords(
|
|
477
1272
|
name_prefix, self.effects_per_switch_on, 'per_switch_on'
|
|
478
1273
|
)
|
|
479
|
-
self.effects_per_running_hour = flow_system.
|
|
1274
|
+
self.effects_per_running_hour = flow_system.fit_effects_to_model_coords(
|
|
480
1275
|
name_prefix, self.effects_per_running_hour, 'per_running_hour'
|
|
481
1276
|
)
|
|
482
|
-
self.consecutive_on_hours_min = flow_system.
|
|
1277
|
+
self.consecutive_on_hours_min = flow_system.fit_to_model_coords(
|
|
483
1278
|
f'{name_prefix}|consecutive_on_hours_min', self.consecutive_on_hours_min
|
|
484
1279
|
)
|
|
485
|
-
self.consecutive_on_hours_max = flow_system.
|
|
1280
|
+
self.consecutive_on_hours_max = flow_system.fit_to_model_coords(
|
|
486
1281
|
f'{name_prefix}|consecutive_on_hours_max', self.consecutive_on_hours_max
|
|
487
1282
|
)
|
|
488
|
-
self.consecutive_off_hours_min = flow_system.
|
|
1283
|
+
self.consecutive_off_hours_min = flow_system.fit_to_model_coords(
|
|
489
1284
|
f'{name_prefix}|consecutive_off_hours_min', self.consecutive_off_hours_min
|
|
490
1285
|
)
|
|
491
|
-
self.consecutive_off_hours_max = flow_system.
|
|
1286
|
+
self.consecutive_off_hours_max = flow_system.fit_to_model_coords(
|
|
492
1287
|
f'{name_prefix}|consecutive_off_hours_max', self.consecutive_off_hours_max
|
|
493
1288
|
)
|
|
1289
|
+
self.on_hours_total_max = flow_system.fit_to_model_coords(
|
|
1290
|
+
f'{name_prefix}|on_hours_total_max', self.on_hours_total_max, dims=['period', 'scenario']
|
|
1291
|
+
)
|
|
1292
|
+
self.on_hours_total_min = flow_system.fit_to_model_coords(
|
|
1293
|
+
f'{name_prefix}|on_hours_total_min', self.on_hours_total_min, dims=['period', 'scenario']
|
|
1294
|
+
)
|
|
1295
|
+
self.switch_on_total_max = flow_system.fit_to_model_coords(
|
|
1296
|
+
f'{name_prefix}|switch_on_total_max', self.switch_on_total_max, dims=['period', 'scenario']
|
|
1297
|
+
)
|
|
494
1298
|
|
|
495
1299
|
@property
|
|
496
1300
|
def use_off(self) -> bool:
|
|
497
|
-
"""
|
|
1301
|
+
"""Proxy: whether OFF variable is required"""
|
|
498
1302
|
return self.use_consecutive_off_hours
|
|
499
1303
|
|
|
500
1304
|
@property
|
|
501
1305
|
def use_consecutive_on_hours(self) -> bool:
|
|
502
|
-
"""Determines
|
|
1306
|
+
"""Determines whether a Variable for consecutive on hours is needed or not"""
|
|
503
1307
|
return any(param is not None for param in [self.consecutive_on_hours_min, self.consecutive_on_hours_max])
|
|
504
1308
|
|
|
505
1309
|
@property
|
|
506
1310
|
def use_consecutive_off_hours(self) -> bool:
|
|
507
|
-
"""Determines
|
|
1311
|
+
"""Determines whether a Variable for consecutive off hours is needed or not"""
|
|
508
1312
|
return any(param is not None for param in [self.consecutive_off_hours_min, self.consecutive_off_hours_max])
|
|
509
1313
|
|
|
510
1314
|
@property
|
|
511
1315
|
def use_switch_on(self) -> bool:
|
|
512
|
-
"""Determines
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
)
|
|
523
|
-
or self.force_switch_on
|
|
1316
|
+
"""Determines whether a variable for switch_on is needed or not"""
|
|
1317
|
+
if self.force_switch_on:
|
|
1318
|
+
return True
|
|
1319
|
+
|
|
1320
|
+
return any(
|
|
1321
|
+
param is not None and param != {}
|
|
1322
|
+
for param in [
|
|
1323
|
+
self.effects_per_switch_on,
|
|
1324
|
+
self.switch_on_total_max,
|
|
1325
|
+
]
|
|
524
1326
|
)
|