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.

Files changed (58) hide show
  1. flixopt/__init__.py +33 -4
  2. flixopt/aggregation.py +60 -80
  3. flixopt/calculation.py +395 -178
  4. flixopt/commons.py +1 -10
  5. flixopt/components.py +939 -448
  6. flixopt/config.py +553 -191
  7. flixopt/core.py +513 -846
  8. flixopt/effects.py +644 -178
  9. flixopt/elements.py +610 -355
  10. flixopt/features.py +394 -966
  11. flixopt/flow_system.py +736 -219
  12. flixopt/interface.py +1104 -302
  13. flixopt/io.py +103 -79
  14. flixopt/linear_converters.py +387 -95
  15. flixopt/modeling.py +759 -0
  16. flixopt/network_app.py +73 -39
  17. flixopt/plotting.py +294 -138
  18. flixopt/results.py +1253 -299
  19. flixopt/solvers.py +25 -21
  20. flixopt/structure.py +938 -396
  21. flixopt/utils.py +38 -12
  22. flixopt-3.0.0.dist-info/METADATA +209 -0
  23. flixopt-3.0.0.dist-info/RECORD +26 -0
  24. flixopt-3.0.0.dist-info/top_level.txt +1 -0
  25. docs/examples/00-Minimal Example.md +0 -5
  26. docs/examples/01-Basic Example.md +0 -5
  27. docs/examples/02-Complex Example.md +0 -10
  28. docs/examples/03-Calculation Modes.md +0 -5
  29. docs/examples/index.md +0 -5
  30. docs/faq/contribute.md +0 -61
  31. docs/faq/index.md +0 -3
  32. docs/images/architecture_flixOpt-pre2.0.0.png +0 -0
  33. docs/images/architecture_flixOpt.png +0 -0
  34. docs/images/flixopt-icon.svg +0 -1
  35. docs/javascripts/mathjax.js +0 -18
  36. docs/user-guide/Mathematical Notation/Bus.md +0 -33
  37. docs/user-guide/Mathematical Notation/Effects, Penalty & Objective.md +0 -132
  38. docs/user-guide/Mathematical Notation/Flow.md +0 -26
  39. docs/user-guide/Mathematical Notation/LinearConverter.md +0 -21
  40. docs/user-guide/Mathematical Notation/Piecewise.md +0 -49
  41. docs/user-guide/Mathematical Notation/Storage.md +0 -44
  42. docs/user-guide/Mathematical Notation/index.md +0 -22
  43. docs/user-guide/Mathematical Notation/others.md +0 -3
  44. docs/user-guide/index.md +0 -124
  45. flixopt/config.yaml +0 -10
  46. flixopt-2.2.0rc2.dist-info/METADATA +0 -167
  47. flixopt-2.2.0rc2.dist-info/RECORD +0 -54
  48. flixopt-2.2.0rc2.dist-info/top_level.txt +0 -5
  49. pics/architecture_flixOpt-pre2.0.0.png +0 -0
  50. pics/architecture_flixOpt.png +0 -0
  51. pics/flixOpt_plotting.jpg +0 -0
  52. pics/flixopt-icon.svg +0 -1
  53. pics/pics.pptx +0 -0
  54. scripts/extract_release_notes.py +0 -45
  55. scripts/gen_ref_pages.py +0 -54
  56. tests/ressources/Zeitreihen2020.csv +0 -35137
  57. {flixopt-2.2.0rc2.dist-info → flixopt-3.0.0.dist-info}/WHEEL +0 -0
  58. {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
- from typing import TYPE_CHECKING, Dict, Iterator, List, Optional, Union
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 .effects import EffectValuesUser, EffectValuesUserScalar
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
- def __init__(self, start: NumericData, end: NumericData):
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
- Args:
28
- start: The x-values of the piece.
29
- end: The end of the piece.
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: 'FlowSystem', name_prefix: str):
35
- self.start = flow_system.create_time_series(f'{name_prefix}|start', self.start)
36
- self.end = flow_system.create_time_series(f'{name_prefix}|end', self.end)
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
- def __init__(self, pieces: List[Piece]):
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
- Args:
46
- pieces: The pieces of the piecewise.
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: 'FlowSystem', name_prefix: str):
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 conversion relationships between multiple flows.
234
+ """Define coordinated piecewise linear relationships between multiple flows.
67
235
 
68
- This class models complex conversion processes where the relationship between
69
- input and output flows changes at different operating points, such as:
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
- - Variable efficiency equipment (heat pumps, engines, turbines)
72
- - Multi-stage chemical processes with different conversion rates
73
- - Equipment with discrete operating modes
74
- - Systems with capacity constraints and thresholds
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
- Args:
77
- piecewises: Dictionary mapping flow labels to their Piecewise conversion functions.
78
- Keys are flow names (e.g., 'electricity_in', 'heat_out', 'fuel_consumed').
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
- Note:
84
- Special modeling features:
85
-
86
- - **Gaps**: Express forbidden operating ranges by creating non-contiguous pieces.
87
- Example: `[(0,50), (100,200)]` - cannot operate between 50-100 units
88
- - **Points**: Express discrete operating points using pieces with identical start/end.
89
- Example: `[(50,50), (100,100)]` - can only operate at exactly 50 or 100 units
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 variable COP (Coefficient of Performance):
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 output
106
- Piece(35, 75), # High load COP=3.0: 35-75 kW heat output
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 input → 52.5 kW heat output (interpolated)
290
+ # At 15 kW electricity → 52.5 kW heat + 3.75 m³/h cooling water
112
291
  ```
113
292
 
114
- Engine with fuel consumption and emissions:
293
+ Combined cycle power plant with synchronized flows:
115
294
 
116
295
  ```python
117
- PiecewiseConversion(
296
+ power_plant_pc = PiecewiseConversion(
118
297
  {
119
- 'fuel_input': Piecewise(
298
+ 'natural_gas': Piecewise(
120
299
  [
121
- Piece(5, 15), # Part load: 5-15 L/h fuel
122
- Piece(15, 30), # Full load: 15-30 L/h fuel
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
- 'power_output': Piecewise(
304
+ 'electricity': Piecewise(
126
305
  [
127
- Piece(10, 25), # Part load: 10-25 kW output
128
- Piece(25, 45), # Full load: 25-45 kW output
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(12, 35), # Part load: 12-35 kg/h CO2
134
- Piece(35, 78), # Full load: 35-78 kg/h CO2
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
- Discrete operating modes (on/off equipment):
326
+ Chemical reactor with multiple products and waste:
142
327
 
143
328
  ```python
144
- PiecewiseConversion(
329
+ reactor_pc = PiecewiseConversion(
145
330
  {
146
- 'electricity_in': Piecewise(
331
+ 'feedstock': Piecewise(
147
332
  [
148
- Piece(0, 0), # Off mode: no consumption
149
- Piece(20, 20), # On mode: fixed 20 kW consumption
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
- 'cooling_out': Piecewise(
337
+ 'product_A': Piecewise(
153
338
  [
154
- Piece(0, 0), # Off mode: no cooling
155
- Piece(60, 60), # On mode: fixed 60 kW cooling
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 forbidden operating range:
359
+ Equipment with discrete operating modes:
163
360
 
164
361
  ```python
165
- PiecewiseConversion(
362
+ compressor_pc = PiecewiseConversion(
166
363
  {
167
- 'steam_input': Piecewise(
364
+ 'electricity': Piecewise(
168
365
  [
169
- Piece(0, 100), # Low pressure operation
170
- Piece(200, 500), # High pressure (gap: 100-200)
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
- 'power_output': Piecewise(
371
+ 'compressed_air': Piecewise(
174
372
  [
175
- Piece(0, 80), # Low efficiency at low pressure
176
- Piece(180, 400), # High efficiency at high pressure
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
- Multi-product chemical reactor:
382
+ Equipment with forbidden operating range:
184
383
 
185
384
  ```python
186
- fx.PiecewiseConversion(
385
+ steam_turbine_pc = PiecewiseConversion(
187
386
  {
188
- 'feedstock': fx.Piecewise(
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
- fx.Piece(7, 32), # Small batch yield: 70%
197
- fx.Piece(32, 140), # Large batch yield: 70%
389
+ Piece(0, 100), # Low pressure operation
390
+ Piece(200, 500), # High pressure (gap: 100-200 forbidden)
198
391
  ]
199
392
  ),
200
- 'product_B': fx.Piecewise(
393
+ 'electricity_out': Piecewise(
201
394
  [
202
- fx.Piece(2, 12), # Small batch: 20% to product B
203
- fx.Piece(12, 45), # Large batch: better selectivity
395
+ Piece(0, 30), # Low pressure: poor efficiency
396
+ Piece(80, 220), # High pressure: good efficiency
204
397
  ]
205
398
  ),
206
- 'waste': fx.Piecewise(
399
+ 'condensate_out': Piecewise(
207
400
  [
208
- fx.Piece(1, 6), # Small batch waste: 10%
209
- fx.Piece(6, 15), # Large batch waste: 7.5%
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
- - Heat pumps/chillers: COP varies with load and ambient conditions
218
- - Power plants: Heat rate curves showing fuel efficiency vs output
219
- - Chemical reactors: Conversion rates and selectivity vs throughput
220
- - Compressors/pumps: Power consumption vs flow rate
221
- - Multi-stage processes: Different conversion rates per stage
222
- - Equipment with minimum loads: Cannot operate below threshold
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: Dict[str, Piecewise]):
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: 'FlowSystem', name_prefix: str):
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
- def __init__(self, piecewise_origin: Piecewise, piecewise_shares: Dict[str, Piecewise]):
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
- def transform_data(self, flow_system: 'FlowSystem', name_prefix: str):
252
- raise NotImplementedError('PiecewiseEffects is not yet implemented for non scalar shares')
253
- # self.piecewise_origin.transform_data(flow_system, f'{name_prefix}|PiecewiseEffects|origin')
254
- # for name, piecewise in self.piecewise_shares.items():
255
- # piecewise.transform_data(flow_system, f'{name_prefix}|PiecewiseEffects|{name}')
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
- @register_class_for_io
259
- class PiecewiseEffectsPerFlowHour(Interface):
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
- This class models situations where the relationship between flow rate and effects changes at
264
- different flow rate levels, such as:
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
- piecewise_flow_rate: `Piecewise` defining the valid flow rate segments.
273
- Each Piece represents a linear segment with (min_flow, max_flow) bounds.
274
-
275
- piecewise_shares: Dictionary mapping effect names to their `Piecewise`.
276
- Keys are effect names (e.g., 'Costs', 'CO2', 'Maintenance').
277
- Values are `Piecewise` objects defining the absolute effect values (not rates/prices).
278
-
279
- ⚠️ IMPORTANT: Values represent total effect amounts, not unit rates.
280
- For a flow rate of X, the effect value is interpolated from the `Piecewise`.
281
- This is NOT flow_rate × unit_price (which would be non-linear).
282
-
283
- Behavior:
284
- - If the first piece doesn't start at zero, flow rate is automatically bounded
285
- by piecewise_flow_rate (when OnOffParameters are not used)
286
- - Each segment represents a linear relationship within that flow rate range
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
- # Tiered cost structure with increasing rates
293
- PiecewiseEffectsPerFlowHour(
294
- piecewise_flow_rate=Piecewise([
295
- Piece(0, 50), # Low flow segment: 0-50 units
296
- Piece(50, 200) # High flow segment: 50-200 units
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
- 'Costs': Piecewise([
300
- Piece(0, 500), # At flow=0: cost=0, at flow=50: cost=500
301
- Piece(500, 2000) # At flow=50: cost=500, at flow=200: cost=2000
302
- ]),
303
- 'CO2': Piecewise([
304
- Piece(0, 100), # At flow=0: CO2=0, at flow=50: CO2=100
305
- Piece(100, 800) # At flow=50: CO2=100, at flow=200: CO2=800
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
- # Decision between two suppliers with overlapping flow ranges
311
- PiecewiseEffectsPerFlowHour(
312
- piecewise_flow_rate=Piecewise([
313
- Piece(0, 100), # Supplier A: 0-100 units
314
- Piece(50, 150) # Supplier B: 50-150 units (overlaps with A)
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
- 'Costs': Piecewise([
318
- Piece(0, 800), # Supplier A: cheaper for low volumes
319
- Piece(400, 1200) # Supplier B: better rates for high volumes
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
- # Flow range 50-100: Optimizer chooses between suppliers based on cost
569
+ ```
324
570
 
325
- # Optional equipment with minimum activation threshold
326
- PiecewiseEffectsPerFlowHour(
327
- piecewise_flow_rate=Piecewise([
328
- Piece(0, 0), # Equipment off: no flow
329
- Piece(20, 100) # Equipment on: minimum 20 units required
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
- 'Costs': Piecewise([
333
- Piece(0, 0), # No cost when off
334
- Piece(200, 800) # Fixed startup cost + variable cost
335
- ]),
336
- 'CO2': Piecewise([
337
- Piece(0, 0), # No CO2 when off
338
- Piece(50, 300) # Decreasing CO2 per fuel burn with higher power
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
- # Decision: Either flow=0 (off) or flow≥20 (on with minimum threshold)
599
+ ```
600
+
601
+ Data center with capacity-dependent efficiency:
343
602
 
344
- # Equipment efficiency curve (although this might be better modeled as a Flow rather than an effect)
345
- PiecewiseEffectsPerFlowHour(
346
- piecewise_flow_rate=Piecewise([Piece(10, 100)]), # Min 10, max 100 units
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
- 'PowerConsumption': Piecewise([Piece(50, 800)]) # Non-linear efficiency
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, piecewise_flow_rate: Piecewise, piecewise_shares: Dict[str, Piecewise]):
355
- self.piecewise_flow_rate = piecewise_flow_rate
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: 'FlowSystem', name_prefix: str):
359
- self.piecewise_flow_rate.transform_data(flow_system, f'{name_prefix}|PiecewiseEffectsPerFlowHour|origin')
360
- for name, piecewise in self.piecewise_shares.items():
361
- piecewise.transform_data(flow_system, f'{name_prefix}|PiecewiseEffectsPerFlowHour|{name}')
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
- collects arguments for invest-stuff
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: Optional[Union[int, float]] = None,
373
- minimum_size: Optional[Union[int, float]] = None,
374
- maximum_size: Optional[Union[int, float]] = None,
375
- optional: bool = True, # Investition ist weglassbar
376
- fix_effects: Optional['EffectValuesUserScalar'] = None,
377
- specific_effects: Optional['EffectValuesUserScalar'] = None, # costs per Flow-Unit/Storage-Size/...
378
- piecewise_effects: Optional[PiecewiseEffects] = None,
379
- divest_effects: Optional['EffectValuesUserScalar'] = None,
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
- Args:
383
- fix_effects: Fixed investment costs if invested. (Attention: Annualize costs to chosen period!)
384
- divest_effects: Fixed divestment costs (if not invested, e.g., demolition costs or contractual penalty).
385
- fixed_size: Determines if the investment size is fixed.
386
- optional: If True, investment is not forced.
387
- specific_effects: Specific costs, e.g., in €/kW_nominal or €/m²_nominal.
388
- Example: {costs: 3, CO2: 0.3} with costs and CO2 representing an Object of class Effect
389
- (Attention: Annualize costs to chosen period!)
390
- piecewise_effects: Linear piecewise relation [invest_pieces, cost_pieces].
391
- Example 1:
392
- [ [5, 25, 25, 100], # size in kW
393
- {costs: [50,250,250,800], # €
394
- PE: [5, 25, 25, 100] # kWh_PrimaryEnergy
395
- }
396
- ]
397
- Example 2 (if only standard-effect):
398
- [ [5, 25, 25, 100], # kW # size in kW
399
- [50,250,250,800] # value for standart effect, typically €
400
- ] # €
401
- (Attention: Annualize costs to chosen period!)
402
- (Args 'specific_effects' and 'fix_effects' can be used in parallel to Investsizepieces)
403
- minimum_size: Min nominal value (only if: size_is_fixed = False). Defaults to CONFIG.modeling.EPSILON.
404
- maximum_size: Max nominal value (only if: size_is_fixed = False). Defaults to CONFIG.modeling.BIG.
405
- """
406
- self.fix_effects: EffectValuesUser = fix_effects or {}
407
- self.divest_effects: EffectValuesUser = divest_effects or {}
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.optional = optional
410
- self.specific_effects: EffectValuesUser = specific_effects or {}
411
- self.piecewise_effects = piecewise_effects
412
- self._minimum_size = minimum_size if minimum_size is not None else CONFIG.modeling.EPSILON
413
- self._maximum_size = maximum_size if maximum_size is not None else CONFIG.modeling.BIG # default maximum
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
- def transform_data(self, flow_system: 'FlowSystem'):
416
- self.fix_effects = flow_system.effects.create_effect_values_dict(self.fix_effects)
417
- self.divest_effects = flow_system.effects.create_effect_values_dict(self.divest_effects)
418
- self.specific_effects = flow_system.effects.create_effect_values_dict(self.specific_effects)
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 minimum_size(self):
422
- return self.fixed_size or self._minimum_size
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 maximum_size(self):
426
- return self.fixed_size or self._maximum_size
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: Optional['EffectValuesUser'] = None,
434
- effects_per_running_hour: Optional['EffectValuesUser'] = None,
435
- on_hours_total_min: Optional[int] = None,
436
- on_hours_total_max: Optional[int] = None,
437
- consecutive_on_hours_min: Optional[NumericData] = None,
438
- consecutive_on_hours_max: Optional[NumericData] = None,
439
- consecutive_off_hours_min: Optional[NumericData] = None,
440
- consecutive_off_hours_max: Optional[NumericData] = None,
441
- switch_on_total_max: Optional[int] = None,
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
- Bundles information about the on and off state of an Element.
446
- If no parameters are given, the default is to create a binary variable for the on state
447
- without further constraints or effects and a variable for the total on hours.
448
-
449
- Args:
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: NumericDataTS = consecutive_on_hours_min
469
- self.consecutive_on_hours_max: NumericDataTS = consecutive_on_hours_max
470
- self.consecutive_off_hours_min: NumericDataTS = consecutive_off_hours_min
471
- self.consecutive_off_hours_max: NumericDataTS = 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: 'FlowSystem', name_prefix: str):
476
- self.effects_per_switch_on = flow_system.create_effect_time_series(
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.create_effect_time_series(
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.create_time_series(
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.create_time_series(
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.create_time_series(
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.create_time_series(
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
- """Determines wether the OFF Variable is needed or not"""
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 wether a Variable for consecutive off hours is needed or not"""
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 wether a Variable for consecutive off hours is needed or not"""
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 wether a Variable for SWITCH-ON is needed or not"""
513
- return (
514
- any(
515
- param not in (None, {})
516
- for param in [
517
- self.effects_per_switch_on,
518
- self.switch_on_total_max,
519
- self.on_hours_total_min,
520
- self.on_hours_total_max,
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
  )