flixopt 2.2.0b0__py3-none-any.whl → 3.0.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of flixopt might be problematic. Click here for more details.
- flixopt/__init__.py +35 -1
- flixopt/aggregation.py +60 -81
- flixopt/calculation.py +381 -196
- flixopt/components.py +1022 -359
- flixopt/config.py +553 -191
- flixopt/core.py +475 -1315
- flixopt/effects.py +477 -214
- flixopt/elements.py +591 -344
- flixopt/features.py +403 -957
- flixopt/flow_system.py +781 -293
- flixopt/interface.py +1159 -189
- flixopt/io.py +50 -55
- flixopt/linear_converters.py +384 -92
- flixopt/modeling.py +759 -0
- flixopt/network_app.py +789 -0
- flixopt/plotting.py +273 -135
- flixopt/results.py +639 -383
- flixopt/solvers.py +25 -21
- flixopt/structure.py +928 -442
- flixopt/utils.py +34 -5
- flixopt-3.0.0.dist-info/METADATA +209 -0
- flixopt-3.0.0.dist-info/RECORD +26 -0
- {flixopt-2.2.0b0.dist-info → flixopt-3.0.0.dist-info}/WHEEL +1 -1
- flixopt-3.0.0.dist-info/top_level.txt +1 -0
- docs/examples/00-Minimal Example.md +0 -5
- docs/examples/01-Basic Example.md +0 -5
- docs/examples/02-Complex Example.md +0 -10
- docs/examples/03-Calculation Modes.md +0 -5
- docs/examples/index.md +0 -5
- docs/faq/contribute.md +0 -49
- docs/faq/index.md +0 -3
- docs/images/architecture_flixOpt-pre2.0.0.png +0 -0
- docs/images/architecture_flixOpt.png +0 -0
- docs/images/flixopt-icon.svg +0 -1
- docs/javascripts/mathjax.js +0 -18
- docs/release-notes/_template.txt +0 -32
- docs/release-notes/index.md +0 -7
- docs/release-notes/v2.0.0.md +0 -93
- docs/release-notes/v2.0.1.md +0 -12
- docs/release-notes/v2.1.0.md +0 -31
- docs/release-notes/v2.2.0.md +0 -55
- docs/user-guide/Mathematical Notation/Bus.md +0 -33
- docs/user-guide/Mathematical Notation/Effects, Penalty & Objective.md +0 -132
- docs/user-guide/Mathematical Notation/Flow.md +0 -26
- docs/user-guide/Mathematical Notation/Investment.md +0 -115
- docs/user-guide/Mathematical Notation/LinearConverter.md +0 -21
- docs/user-guide/Mathematical Notation/Piecewise.md +0 -49
- docs/user-guide/Mathematical Notation/Storage.md +0 -44
- docs/user-guide/Mathematical Notation/index.md +0 -22
- docs/user-guide/Mathematical Notation/others.md +0 -3
- docs/user-guide/index.md +0 -124
- flixopt/config.yaml +0 -10
- flixopt-2.2.0b0.dist-info/METADATA +0 -146
- flixopt-2.2.0b0.dist-info/RECORD +0 -59
- flixopt-2.2.0b0.dist-info/top_level.txt +0 -5
- pics/architecture_flixOpt-pre2.0.0.png +0 -0
- pics/architecture_flixOpt.png +0 -0
- pics/flixOpt_plotting.jpg +0 -0
- pics/flixopt-icon.svg +0 -1
- pics/pics.pptx +0 -0
- scripts/gen_ref_pages.py +0 -54
- tests/ressources/Zeitreihen2020.csv +0 -35137
- {flixopt-2.2.0b0.dist-info → flixopt-3.0.0.dist-info}/licenses/LICENSE +0 -0
flixopt/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 NumericDataTS, Scalar, ScenarioData, TimestepData
|
|
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,36 +29,173 @@ 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
|
|
33
79
|
self.has_time_dim = False
|
|
34
80
|
|
|
35
|
-
def transform_data(self, flow_system:
|
|
36
|
-
self.
|
|
37
|
-
|
|
38
|
-
)
|
|
39
|
-
self.end = flow_system.create_time_series(
|
|
40
|
-
name=f'{name_prefix}|end', data=self.end, has_time_dim=self.has_time_dim, has_scenario_dim=True
|
|
41
|
-
)
|
|
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)
|
|
42
85
|
|
|
43
86
|
|
|
44
87
|
@register_class_for_io
|
|
45
88
|
class Piecewise(Interface):
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
Define a Piecewise, consisting of a list of Pieces.
|
|
89
|
+
"""
|
|
90
|
+
Define a Piecewise, consisting of a list of Pieces.
|
|
49
91
|
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
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]):
|
|
53
199
|
self.pieces = pieces
|
|
54
200
|
self._has_time_dim = False
|
|
55
201
|
|
|
@@ -64,6 +210,12 @@ class Piecewise(Interface):
|
|
|
64
210
|
piece.has_time_dim = value
|
|
65
211
|
|
|
66
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
|
+
"""
|
|
67
219
|
return len(self.pieces)
|
|
68
220
|
|
|
69
221
|
def __getitem__(self, index) -> Piece:
|
|
@@ -72,25 +224,212 @@ class Piecewise(Interface):
|
|
|
72
224
|
def __iter__(self) -> Iterator[Piece]:
|
|
73
225
|
return iter(self.pieces) # Enables iteration like for piece in piecewise: ...
|
|
74
226
|
|
|
75
|
-
def transform_data(self, flow_system:
|
|
227
|
+
def transform_data(self, flow_system: FlowSystem, name_prefix: str = '') -> None:
|
|
76
228
|
for i, piece in enumerate(self.pieces):
|
|
77
229
|
piece.transform_data(flow_system, f'{name_prefix}|Piece{i}')
|
|
78
230
|
|
|
79
231
|
|
|
80
232
|
@register_class_for_io
|
|
81
233
|
class PiecewiseConversion(Interface):
|
|
82
|
-
|
|
83
|
-
"""
|
|
84
|
-
Define a piecewise conversion between multiple Flows.
|
|
85
|
-
--> "gaps" can be expressed by a piece not starting at the end of the prior piece: [(1,3), (4,5)]
|
|
86
|
-
--> "points" can expressed as piece with same begin and end: [(3,3), (4,4)]
|
|
234
|
+
"""Define coordinated piecewise linear relationships between multiple flows.
|
|
87
235
|
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
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.
|
|
240
|
+
|
|
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.
|
|
246
|
+
|
|
247
|
+
Mathematical Formulation:
|
|
248
|
+
See the complete mathematical model in the documentation:
|
|
249
|
+
[Piecewise](../user-guide/mathematical-notation/features/Piecewise.md)
|
|
250
|
+
|
|
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.
|
|
263
|
+
|
|
264
|
+
Examples:
|
|
265
|
+
Heat pump with coordinated efficiency changes:
|
|
266
|
+
|
|
267
|
+
```python
|
|
268
|
+
heat_pump_pc = PiecewiseConversion(
|
|
269
|
+
{
|
|
270
|
+
'electricity_in': Piecewise(
|
|
271
|
+
[
|
|
272
|
+
Piece(0, 10), # Low load: 0-10 kW electricity
|
|
273
|
+
Piece(10, 25), # High load: 10-25 kW electricity
|
|
274
|
+
]
|
|
275
|
+
),
|
|
276
|
+
'heat_out': Piecewise(
|
|
277
|
+
[
|
|
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
|
|
286
|
+
]
|
|
287
|
+
),
|
|
288
|
+
}
|
|
289
|
+
)
|
|
290
|
+
# At 15 kW electricity → 52.5 kW heat + 3.75 m³/h cooling water
|
|
291
|
+
```
|
|
292
|
+
|
|
293
|
+
Combined cycle power plant with synchronized flows:
|
|
294
|
+
|
|
295
|
+
```python
|
|
296
|
+
power_plant_pc = PiecewiseConversion(
|
|
297
|
+
{
|
|
298
|
+
'natural_gas': Piecewise(
|
|
299
|
+
[
|
|
300
|
+
Piece(150, 300), # Part load: 150-300 MW_th fuel
|
|
301
|
+
Piece(300, 500), # Full load: 300-500 MW_th fuel
|
|
302
|
+
]
|
|
303
|
+
),
|
|
304
|
+
'electricity': Piecewise(
|
|
305
|
+
[
|
|
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
|
|
314
|
+
]
|
|
315
|
+
),
|
|
316
|
+
'co2_emissions': Piecewise(
|
|
317
|
+
[
|
|
318
|
+
Piece(30, 60), # Part load: 30-60 t/h CO2
|
|
319
|
+
Piece(60, 100), # Full load: 60-100 t/h CO2
|
|
320
|
+
]
|
|
321
|
+
),
|
|
322
|
+
}
|
|
323
|
+
)
|
|
324
|
+
```
|
|
325
|
+
|
|
326
|
+
Chemical reactor with multiple products and waste:
|
|
327
|
+
|
|
328
|
+
```python
|
|
329
|
+
reactor_pc = PiecewiseConversion(
|
|
330
|
+
{
|
|
331
|
+
'feedstock': Piecewise(
|
|
332
|
+
[
|
|
333
|
+
Piece(10, 50), # Small batch: 10-50 kg/h
|
|
334
|
+
Piece(50, 200), # Large batch: 50-200 kg/h
|
|
335
|
+
]
|
|
336
|
+
),
|
|
337
|
+
'product_A': Piecewise(
|
|
338
|
+
[
|
|
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)
|
|
353
|
+
]
|
|
354
|
+
),
|
|
355
|
+
}
|
|
356
|
+
)
|
|
357
|
+
```
|
|
358
|
+
|
|
359
|
+
Equipment with discrete operating modes:
|
|
360
|
+
|
|
361
|
+
```python
|
|
362
|
+
compressor_pc = PiecewiseConversion(
|
|
363
|
+
{
|
|
364
|
+
'electricity': Piecewise(
|
|
365
|
+
[
|
|
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
|
|
369
|
+
]
|
|
370
|
+
),
|
|
371
|
+
'compressed_air': Piecewise(
|
|
372
|
+
[
|
|
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
|
|
376
|
+
]
|
|
377
|
+
),
|
|
378
|
+
}
|
|
379
|
+
)
|
|
380
|
+
```
|
|
381
|
+
|
|
382
|
+
Equipment with forbidden operating range:
|
|
383
|
+
|
|
384
|
+
```python
|
|
385
|
+
steam_turbine_pc = PiecewiseConversion(
|
|
386
|
+
{
|
|
387
|
+
'steam_in': Piecewise(
|
|
388
|
+
[
|
|
389
|
+
Piece(0, 100), # Low pressure operation
|
|
390
|
+
Piece(200, 500), # High pressure (gap: 100-200 forbidden)
|
|
391
|
+
]
|
|
392
|
+
),
|
|
393
|
+
'electricity_out': Piecewise(
|
|
394
|
+
[
|
|
395
|
+
Piece(0, 30), # Low pressure: poor efficiency
|
|
396
|
+
Piece(80, 220), # High pressure: good efficiency
|
|
397
|
+
]
|
|
398
|
+
),
|
|
399
|
+
'condensate_out': Piecewise(
|
|
400
|
+
[
|
|
401
|
+
Piece(0, 100), # Low pressure condensate
|
|
402
|
+
Piece(200, 500), # High pressure condensate
|
|
403
|
+
]
|
|
404
|
+
),
|
|
405
|
+
}
|
|
406
|
+
)
|
|
407
|
+
```
|
|
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
|
+
|
|
419
|
+
Common Use Cases:
|
|
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
|
|
426
|
+
|
|
427
|
+
"""
|
|
428
|
+
|
|
429
|
+
def __init__(self, piecewises: dict[str, Piecewise]):
|
|
91
430
|
self.piecewises = piecewises
|
|
92
431
|
self._has_time_dim = True
|
|
93
|
-
self.has_time_dim = True #
|
|
432
|
+
self.has_time_dim = True # Initial propagation
|
|
94
433
|
|
|
95
434
|
@property
|
|
96
435
|
def has_time_dim(self):
|
|
@@ -103,27 +442,214 @@ class PiecewiseConversion(Interface):
|
|
|
103
442
|
piecewise.has_time_dim = value
|
|
104
443
|
|
|
105
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
|
+
"""
|
|
106
451
|
return self.piecewises.items()
|
|
107
452
|
|
|
108
|
-
def transform_data(self, flow_system:
|
|
453
|
+
def transform_data(self, flow_system: FlowSystem, name_prefix: str = '') -> None:
|
|
109
454
|
for name, piecewise in self.piecewises.items():
|
|
110
455
|
piecewise.transform_data(flow_system, f'{name_prefix}|{name}')
|
|
111
456
|
|
|
112
457
|
|
|
113
458
|
@register_class_for_io
|
|
114
459
|
class PiecewiseEffects(Interface):
|
|
115
|
-
|
|
116
|
-
"""
|
|
117
|
-
Define piecewise effects related to a variable.
|
|
460
|
+
"""Define how a single decision variable contributes to system effects with piecewise rates.
|
|
118
461
|
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
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.
|
|
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
|
|
471
|
+
|
|
472
|
+
Relationship to PiecewiseConversion:
|
|
473
|
+
**PiecewiseConversion**: Models synchronized relationships between multiple
|
|
474
|
+
flow variables (e.g., fuel_in, electricity_out, emissions_out all coordinated).
|
|
475
|
+
|
|
476
|
+
**PiecewiseEffects**: Models how one variable contributes to system-wide
|
|
477
|
+
effects at variable rates (e.g., production_level → costs, emissions, resources).
|
|
478
|
+
|
|
479
|
+
Args:
|
|
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)
|
|
495
|
+
|
|
496
|
+
Examples:
|
|
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
|
+
),
|
|
508
|
+
piecewise_shares={
|
|
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
|
+
},
|
|
531
|
+
)
|
|
532
|
+
```
|
|
533
|
+
|
|
534
|
+
Power generation with load-dependent characteristics:
|
|
535
|
+
|
|
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
|
+
),
|
|
545
|
+
piecewise_shares={
|
|
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
|
+
},
|
|
568
|
+
)
|
|
569
|
+
```
|
|
570
|
+
|
|
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
|
+
),
|
|
582
|
+
piecewise_shares={
|
|
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
|
+
},
|
|
598
|
+
)
|
|
599
|
+
```
|
|
600
|
+
|
|
601
|
+
Data center with capacity-dependent efficiency:
|
|
602
|
+
|
|
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
|
+
),
|
|
612
|
+
piecewise_shares={
|
|
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
|
+
},
|
|
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
|
|
645
|
+
|
|
646
|
+
"""
|
|
647
|
+
|
|
648
|
+
def __init__(self, piecewise_origin: Piecewise, piecewise_shares: dict[str, Piecewise]):
|
|
123
649
|
self.piecewise_origin = piecewise_origin
|
|
124
650
|
self.piecewise_shares = piecewise_shares
|
|
125
651
|
self._has_time_dim = False
|
|
126
|
-
self.has_time_dim = False #
|
|
652
|
+
self.has_time_dim = False # Initial propagation
|
|
127
653
|
|
|
128
654
|
@property
|
|
129
655
|
def has_time_dim(self):
|
|
@@ -136,7 +662,7 @@ class PiecewiseEffects(Interface):
|
|
|
136
662
|
for piecewise in self.piecewise_shares.values():
|
|
137
663
|
piecewise.has_time_dim = value
|
|
138
664
|
|
|
139
|
-
def transform_data(self, flow_system:
|
|
665
|
+
def transform_data(self, flow_system: FlowSystem, name_prefix: str = '') -> None:
|
|
140
666
|
self.piecewise_origin.transform_data(flow_system, f'{name_prefix}|PiecewiseEffects|origin')
|
|
141
667
|
for effect, piecewise in self.piecewise_shares.items():
|
|
142
668
|
piecewise.transform_data(flow_system, f'{name_prefix}|PiecewiseEffects|{effect}')
|
|
@@ -144,213 +670,657 @@ class PiecewiseEffects(Interface):
|
|
|
144
670
|
|
|
145
671
|
@register_class_for_io
|
|
146
672
|
class InvestParameters(Interface):
|
|
147
|
-
"""
|
|
148
|
-
|
|
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
|
+
|
|
149
872
|
"""
|
|
150
873
|
|
|
151
874
|
def __init__(
|
|
152
875
|
self,
|
|
153
|
-
fixed_size:
|
|
154
|
-
minimum_size:
|
|
155
|
-
maximum_size:
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
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,
|
|
162
886
|
):
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
fix_effects
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
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
|
+
)
|
|
182
920
|
self.fixed_size = fixed_size
|
|
183
|
-
self.
|
|
184
|
-
self.
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
self.
|
|
188
|
-
self.
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
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(
|
|
193
932
|
label_prefix=name_prefix,
|
|
194
|
-
effect_values=self.
|
|
195
|
-
label_suffix='
|
|
196
|
-
|
|
197
|
-
has_scenario_dim=True,
|
|
933
|
+
effect_values=self.effects_of_investment,
|
|
934
|
+
label_suffix='effects_of_investment',
|
|
935
|
+
dims=['period', 'scenario'],
|
|
198
936
|
)
|
|
199
|
-
self.
|
|
937
|
+
self.effects_of_retirement = flow_system.fit_effects_to_model_coords(
|
|
200
938
|
label_prefix=name_prefix,
|
|
201
|
-
effect_values=self.
|
|
202
|
-
label_suffix='
|
|
203
|
-
|
|
204
|
-
has_scenario_dim=True,
|
|
939
|
+
effect_values=self.effects_of_retirement,
|
|
940
|
+
label_suffix='effects_of_retirement',
|
|
941
|
+
dims=['period', 'scenario'],
|
|
205
942
|
)
|
|
206
|
-
self.
|
|
943
|
+
self.effects_of_investment_per_size = flow_system.fit_effects_to_model_coords(
|
|
207
944
|
label_prefix=name_prefix,
|
|
208
|
-
effect_values=self.
|
|
209
|
-
label_suffix='
|
|
210
|
-
|
|
211
|
-
has_scenario_dim=True,
|
|
945
|
+
effect_values=self.effects_of_investment_per_size,
|
|
946
|
+
label_suffix='effects_of_investment_per_size',
|
|
947
|
+
dims=['period', 'scenario'],
|
|
212
948
|
)
|
|
213
|
-
if self.piecewise_effects is not None:
|
|
214
|
-
self.piecewise_effects.has_time_dim = False
|
|
215
|
-
self.piecewise_effects.transform_data(flow_system, f'{name_prefix}|PiecewiseEffects')
|
|
216
949
|
|
|
217
|
-
self.
|
|
218
|
-
|
|
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']
|
|
219
956
|
)
|
|
220
|
-
self.
|
|
221
|
-
f'{name_prefix}|maximum_size', self.maximum_size,
|
|
957
|
+
self.maximum_size = flow_system.fit_to_model_coords(
|
|
958
|
+
f'{name_prefix}|maximum_size', self.maximum_size, dims=['period', 'scenario']
|
|
222
959
|
)
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
def _plausibility_checks(self, flow_system):
|
|
229
|
-
if isinstance(self.investment_scenarios, list):
|
|
230
|
-
if not set(self.investment_scenarios).issubset(flow_system.time_series_collection.scenarios):
|
|
231
|
-
raise ValueError(
|
|
232
|
-
f'Some scenarios in investment_scenarios are not present in the time_series_collection: '
|
|
233
|
-
f'{set(self.investment_scenarios) - set(flow_system.time_series_collection.scenarios)}'
|
|
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)=}'
|
|
234
965
|
)
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
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
|
|
993
|
+
|
|
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
|
|
242
999
|
|
|
243
1000
|
@property
|
|
244
|
-
def
|
|
245
|
-
|
|
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
|
|
246
1009
|
|
|
247
1010
|
@property
|
|
248
|
-
def
|
|
249
|
-
|
|
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
|
|
1029
|
+
|
|
1030
|
+
@property
|
|
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')
|
|
250
1058
|
|
|
251
1059
|
|
|
252
1060
|
@register_class_for_io
|
|
253
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
|
+
|
|
254
1242
|
def __init__(
|
|
255
1243
|
self,
|
|
256
|
-
effects_per_switch_on:
|
|
257
|
-
effects_per_running_hour:
|
|
258
|
-
on_hours_total_min:
|
|
259
|
-
on_hours_total_max:
|
|
260
|
-
consecutive_on_hours_min:
|
|
261
|
-
consecutive_on_hours_max:
|
|
262
|
-
consecutive_off_hours_min:
|
|
263
|
-
consecutive_off_hours_max:
|
|
264
|
-
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,
|
|
265
1253
|
force_switch_on: bool = False,
|
|
266
1254
|
):
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
effects_per_switch_on: cost of one switch from off (var_on=0) to on (var_on=1),
|
|
274
|
-
unit i.g. in Euro
|
|
275
|
-
effects_per_running_hour: costs for operating, i.g. in € per hour
|
|
276
|
-
on_hours_total_min: min. overall sum of operating hours.
|
|
277
|
-
on_hours_total_max: max. overall sum of operating hours.
|
|
278
|
-
consecutive_on_hours_min: min sum of operating hours in one piece
|
|
279
|
-
(last on-time period of timeseries is not checked and can be shorter)
|
|
280
|
-
consecutive_on_hours_max: max sum of operating hours in one piece
|
|
281
|
-
consecutive_off_hours_min: min sum of non-operating hours in one piece
|
|
282
|
-
(last off-time period of timeseries is not checked and can be shorter)
|
|
283
|
-
consecutive_off_hours_max: max sum of non-operating hours in one piece
|
|
284
|
-
switch_on_total_max: max nr of switchOn operations
|
|
285
|
-
force_switch_on: force creation of switch on variable, even if there is no switch_on_total_max
|
|
286
|
-
"""
|
|
287
|
-
self.effects_per_switch_on: EffectValuesUserTimestep = effects_per_switch_on or {}
|
|
288
|
-
self.effects_per_running_hour: EffectValuesUserTimestep = 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
|
+
)
|
|
289
1261
|
self.on_hours_total_min: Scalar = on_hours_total_min
|
|
290
1262
|
self.on_hours_total_max: Scalar = on_hours_total_max
|
|
291
|
-
self.consecutive_on_hours_min:
|
|
292
|
-
self.consecutive_on_hours_max:
|
|
293
|
-
self.consecutive_off_hours_min:
|
|
294
|
-
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
|
|
295
1267
|
self.switch_on_total_max: Scalar = switch_on_total_max
|
|
296
1268
|
self.force_switch_on: bool = force_switch_on
|
|
297
1269
|
|
|
298
|
-
def transform_data(self, flow_system:
|
|
299
|
-
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(
|
|
300
1272
|
name_prefix, self.effects_per_switch_on, 'per_switch_on'
|
|
301
1273
|
)
|
|
302
|
-
self.effects_per_running_hour = flow_system.
|
|
1274
|
+
self.effects_per_running_hour = flow_system.fit_effects_to_model_coords(
|
|
303
1275
|
name_prefix, self.effects_per_running_hour, 'per_running_hour'
|
|
304
1276
|
)
|
|
305
|
-
self.consecutive_on_hours_min = flow_system.
|
|
1277
|
+
self.consecutive_on_hours_min = flow_system.fit_to_model_coords(
|
|
306
1278
|
f'{name_prefix}|consecutive_on_hours_min', self.consecutive_on_hours_min
|
|
307
1279
|
)
|
|
308
|
-
self.consecutive_on_hours_max = flow_system.
|
|
1280
|
+
self.consecutive_on_hours_max = flow_system.fit_to_model_coords(
|
|
309
1281
|
f'{name_prefix}|consecutive_on_hours_max', self.consecutive_on_hours_max
|
|
310
1282
|
)
|
|
311
|
-
self.consecutive_off_hours_min = flow_system.
|
|
1283
|
+
self.consecutive_off_hours_min = flow_system.fit_to_model_coords(
|
|
312
1284
|
f'{name_prefix}|consecutive_off_hours_min', self.consecutive_off_hours_min
|
|
313
1285
|
)
|
|
314
|
-
self.consecutive_off_hours_max = flow_system.
|
|
1286
|
+
self.consecutive_off_hours_max = flow_system.fit_to_model_coords(
|
|
315
1287
|
f'{name_prefix}|consecutive_off_hours_max', self.consecutive_off_hours_max
|
|
316
1288
|
)
|
|
317
|
-
self.on_hours_total_max = flow_system.
|
|
318
|
-
f'{name_prefix}|on_hours_total_max', self.on_hours_total_max,
|
|
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']
|
|
319
1291
|
)
|
|
320
|
-
self.on_hours_total_min = flow_system.
|
|
321
|
-
f'{name_prefix}|on_hours_total_min', self.on_hours_total_min,
|
|
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']
|
|
322
1294
|
)
|
|
323
|
-
self.switch_on_total_max = flow_system.
|
|
324
|
-
f'{name_prefix}|switch_on_total_max', self.switch_on_total_max,
|
|
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']
|
|
325
1297
|
)
|
|
326
1298
|
|
|
327
1299
|
@property
|
|
328
1300
|
def use_off(self) -> bool:
|
|
329
|
-
"""
|
|
1301
|
+
"""Proxy: whether OFF variable is required"""
|
|
330
1302
|
return self.use_consecutive_off_hours
|
|
331
1303
|
|
|
332
1304
|
@property
|
|
333
1305
|
def use_consecutive_on_hours(self) -> bool:
|
|
334
|
-
"""Determines
|
|
1306
|
+
"""Determines whether a Variable for consecutive on hours is needed or not"""
|
|
335
1307
|
return any(param is not None for param in [self.consecutive_on_hours_min, self.consecutive_on_hours_max])
|
|
336
1308
|
|
|
337
1309
|
@property
|
|
338
1310
|
def use_consecutive_off_hours(self) -> bool:
|
|
339
|
-
"""Determines
|
|
1311
|
+
"""Determines whether a Variable for consecutive off hours is needed or not"""
|
|
340
1312
|
return any(param is not None for param in [self.consecutive_off_hours_min, self.consecutive_off_hours_max])
|
|
341
1313
|
|
|
342
1314
|
@property
|
|
343
1315
|
def use_switch_on(self) -> bool:
|
|
344
|
-
"""Determines
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
)
|
|
355
|
-
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
|
+
]
|
|
356
1326
|
)
|