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