flixopt 3.0.1__py3-none-any.whl → 6.0.0rc7__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- flixopt/__init__.py +57 -49
- flixopt/carrier.py +159 -0
- flixopt/clustering/__init__.py +51 -0
- flixopt/clustering/base.py +1746 -0
- flixopt/clustering/intercluster_helpers.py +201 -0
- flixopt/color_processing.py +372 -0
- flixopt/comparison.py +819 -0
- flixopt/components.py +848 -270
- flixopt/config.py +853 -496
- flixopt/core.py +111 -98
- flixopt/effects.py +294 -284
- flixopt/elements.py +484 -223
- flixopt/features.py +220 -118
- flixopt/flow_system.py +2026 -389
- flixopt/interface.py +504 -286
- flixopt/io.py +1718 -55
- flixopt/linear_converters.py +291 -230
- flixopt/modeling.py +304 -181
- flixopt/network_app.py +2 -1
- flixopt/optimization.py +788 -0
- flixopt/optimize_accessor.py +373 -0
- flixopt/plot_result.py +143 -0
- flixopt/plotting.py +1177 -1034
- flixopt/results.py +1331 -372
- flixopt/solvers.py +12 -4
- flixopt/statistics_accessor.py +2412 -0
- flixopt/stats_accessor.py +75 -0
- flixopt/structure.py +954 -120
- flixopt/topology_accessor.py +676 -0
- flixopt/transform_accessor.py +2277 -0
- flixopt/types.py +120 -0
- flixopt-6.0.0rc7.dist-info/METADATA +290 -0
- flixopt-6.0.0rc7.dist-info/RECORD +36 -0
- {flixopt-3.0.1.dist-info → flixopt-6.0.0rc7.dist-info}/WHEEL +1 -1
- flixopt/aggregation.py +0 -382
- flixopt/calculation.py +0 -672
- flixopt/commons.py +0 -51
- flixopt/utils.py +0 -86
- flixopt-3.0.1.dist-info/METADATA +0 -209
- flixopt-3.0.1.dist-info/RECORD +0 -26
- {flixopt-3.0.1.dist-info → flixopt-6.0.0rc7.dist-info}/licenses/LICENSE +0 -0
- {flixopt-3.0.1.dist-info → flixopt-6.0.0rc7.dist-info}/top_level.txt +0 -0
flixopt/interface.py
CHANGED
|
@@ -1,28 +1,26 @@
|
|
|
1
1
|
"""
|
|
2
|
-
This module contains classes to collect Parameters for the Investment and
|
|
2
|
+
This module contains classes to collect Parameters for the Investment and Status decisions.
|
|
3
3
|
These are tightly connected to features.py
|
|
4
4
|
"""
|
|
5
5
|
|
|
6
6
|
from __future__ import annotations
|
|
7
7
|
|
|
8
8
|
import logging
|
|
9
|
-
import
|
|
10
|
-
from typing import TYPE_CHECKING, Literal, Optional
|
|
9
|
+
from typing import TYPE_CHECKING, Any, Literal
|
|
11
10
|
|
|
12
11
|
import numpy as np
|
|
13
12
|
import pandas as pd
|
|
13
|
+
import plotly.express as px
|
|
14
14
|
import xarray as xr
|
|
15
15
|
|
|
16
16
|
from .config import CONFIG
|
|
17
|
+
from .plot_result import PlotResult
|
|
17
18
|
from .structure import Interface, register_class_for_io
|
|
18
19
|
|
|
19
20
|
if TYPE_CHECKING: # for type checking and preventing circular imports
|
|
20
21
|
from collections.abc import Iterator
|
|
21
22
|
|
|
22
|
-
from .
|
|
23
|
-
from .effects import PeriodicEffectsUser, TemporalEffectsUser
|
|
24
|
-
from .flow_system import FlowSystem
|
|
25
|
-
|
|
23
|
+
from .types import Effect_PS, Effect_TPS, Numeric_PS, Numeric_TPS
|
|
26
24
|
|
|
27
25
|
logger = logging.getLogger('flixopt')
|
|
28
26
|
|
|
@@ -73,21 +71,27 @@ class Piece(Interface):
|
|
|
73
71
|
|
|
74
72
|
"""
|
|
75
73
|
|
|
76
|
-
def __init__(self, start:
|
|
74
|
+
def __init__(self, start: Numeric_TPS, end: Numeric_TPS):
|
|
77
75
|
self.start = start
|
|
78
76
|
self.end = end
|
|
79
77
|
self.has_time_dim = False
|
|
80
78
|
|
|
81
|
-
def transform_data(self
|
|
79
|
+
def transform_data(self) -> None:
|
|
82
80
|
dims = None if self.has_time_dim else ['period', 'scenario']
|
|
83
|
-
self.start =
|
|
84
|
-
self.end =
|
|
81
|
+
self.start = self._fit_coords(f'{self.prefix}|start', self.start, dims=dims)
|
|
82
|
+
self.end = self._fit_coords(f'{self.prefix}|end', self.end, dims=dims)
|
|
85
83
|
|
|
86
84
|
|
|
87
85
|
@register_class_for_io
|
|
88
86
|
class Piecewise(Interface):
|
|
89
|
-
"""
|
|
90
|
-
|
|
87
|
+
"""Define piecewise linear approximations for modeling non-linear relationships.
|
|
88
|
+
|
|
89
|
+
Enables modeling of non-linear relationships through piecewise linear segments
|
|
90
|
+
while maintaining problem linearity. Consists of a collection of Pieces that
|
|
91
|
+
define valid ranges for variables.
|
|
92
|
+
|
|
93
|
+
Mathematical Formulation:
|
|
94
|
+
See <https://flixopt.github.io/flixopt/latest/user-guide/mathematical-notation/features/Piecewise/>
|
|
91
95
|
|
|
92
96
|
Args:
|
|
93
97
|
pieces: list of Piece objects defining the linear segments. The arrangement
|
|
@@ -224,9 +228,15 @@ class Piecewise(Interface):
|
|
|
224
228
|
def __iter__(self) -> Iterator[Piece]:
|
|
225
229
|
return iter(self.pieces) # Enables iteration like for piece in piecewise: ...
|
|
226
230
|
|
|
227
|
-
def
|
|
231
|
+
def link_to_flow_system(self, flow_system, prefix: str = '') -> None:
|
|
232
|
+
"""Propagate flow_system reference to nested Piece objects."""
|
|
233
|
+
super().link_to_flow_system(flow_system, prefix)
|
|
228
234
|
for i, piece in enumerate(self.pieces):
|
|
229
|
-
piece.
|
|
235
|
+
piece.link_to_flow_system(flow_system, self._sub_prefix(f'Piece{i}'))
|
|
236
|
+
|
|
237
|
+
def transform_data(self) -> None:
|
|
238
|
+
for piece in self.pieces:
|
|
239
|
+
piece.transform_data()
|
|
230
240
|
|
|
231
241
|
|
|
232
242
|
@register_class_for_io
|
|
@@ -411,7 +421,7 @@ class PiecewiseConversion(Interface):
|
|
|
411
421
|
operate in certain ranges (e.g., minimum loads, unstable regions).
|
|
412
422
|
|
|
413
423
|
**Discrete Modes**: Use pieces with identical start/end values to model
|
|
414
|
-
equipment with fixed operating points (e.g., on/
|
|
424
|
+
equipment with fixed operating points (e.g., on/inactive, discrete speeds).
|
|
415
425
|
|
|
416
426
|
**Efficiency Changes**: Coordinate input and output pieces to reflect
|
|
417
427
|
changing conversion efficiency across operating ranges.
|
|
@@ -450,9 +460,151 @@ class PiecewiseConversion(Interface):
|
|
|
450
460
|
"""
|
|
451
461
|
return self.piecewises.items()
|
|
452
462
|
|
|
453
|
-
def
|
|
463
|
+
def link_to_flow_system(self, flow_system, prefix: str = '') -> None:
|
|
464
|
+
"""Propagate flow_system reference to nested Piecewise objects."""
|
|
465
|
+
super().link_to_flow_system(flow_system, prefix)
|
|
454
466
|
for name, piecewise in self.piecewises.items():
|
|
455
|
-
piecewise.
|
|
467
|
+
piecewise.link_to_flow_system(flow_system, self._sub_prefix(name))
|
|
468
|
+
|
|
469
|
+
def transform_data(self) -> None:
|
|
470
|
+
for piecewise in self.piecewises.values():
|
|
471
|
+
piecewise.transform_data()
|
|
472
|
+
|
|
473
|
+
def plot(
|
|
474
|
+
self,
|
|
475
|
+
x_flow: str | None = None,
|
|
476
|
+
title: str = '',
|
|
477
|
+
select: dict[str, Any] | None = None,
|
|
478
|
+
colorscale: str | None = None,
|
|
479
|
+
show: bool | None = None,
|
|
480
|
+
) -> PlotResult:
|
|
481
|
+
"""Plot multi-flow piecewise conversion with time variation visualization.
|
|
482
|
+
|
|
483
|
+
Visualizes the piecewise linear relationships between flows. Each flow
|
|
484
|
+
is shown in a separate subplot (faceted by flow). Pieces are distinguished
|
|
485
|
+
by line dash style. If boundaries vary over time, color shows time progression.
|
|
486
|
+
|
|
487
|
+
Note:
|
|
488
|
+
Requires FlowSystem to be connected and transformed (call
|
|
489
|
+
flow_system.connect_and_transform() first).
|
|
490
|
+
|
|
491
|
+
Args:
|
|
492
|
+
x_flow: Flow label to use for X-axis. Defaults to first flow in dict.
|
|
493
|
+
title: Plot title.
|
|
494
|
+
select: xarray-style selection dict to filter data,
|
|
495
|
+
e.g. {'time': slice('2024-01-01', '2024-01-02')}.
|
|
496
|
+
colorscale: Colorscale name for time coloring (e.g., 'RdYlBu_r', 'viridis').
|
|
497
|
+
Defaults to CONFIG.Plotting.default_sequential_colorscale.
|
|
498
|
+
show: Whether to display the figure.
|
|
499
|
+
Defaults to CONFIG.Plotting.default_show.
|
|
500
|
+
|
|
501
|
+
Returns:
|
|
502
|
+
PlotResult containing the figure and underlying piecewise data.
|
|
503
|
+
|
|
504
|
+
Examples:
|
|
505
|
+
>>> flow_system.connect_and_transform()
|
|
506
|
+
>>> chp.piecewise_conversion.plot(x_flow='Gas', title='CHP Curves')
|
|
507
|
+
>>> # Select specific time range
|
|
508
|
+
>>> chp.piecewise_conversion.plot(select={'time': slice(0, 12)})
|
|
509
|
+
"""
|
|
510
|
+
if not self.flow_system.connected_and_transformed:
|
|
511
|
+
logger.debug('Connecting flow_system for plotting PiecewiseConversion')
|
|
512
|
+
self.flow_system.connect_and_transform()
|
|
513
|
+
|
|
514
|
+
colorscale = colorscale or CONFIG.Plotting.default_sequential_colorscale
|
|
515
|
+
|
|
516
|
+
flow_labels = list(self.piecewises.keys())
|
|
517
|
+
x_label = x_flow if x_flow is not None else flow_labels[0]
|
|
518
|
+
if x_label not in flow_labels:
|
|
519
|
+
raise ValueError(f"x_flow '{x_label}' not found. Available: {flow_labels}")
|
|
520
|
+
|
|
521
|
+
y_flows = [label for label in flow_labels if label != x_label]
|
|
522
|
+
if not y_flows:
|
|
523
|
+
raise ValueError('Need at least two flows to plot')
|
|
524
|
+
|
|
525
|
+
x_piecewise = self.piecewises[x_label]
|
|
526
|
+
|
|
527
|
+
# Build Dataset with all piece data
|
|
528
|
+
datasets = []
|
|
529
|
+
for y_label in y_flows:
|
|
530
|
+
y_piecewise = self.piecewises[y_label]
|
|
531
|
+
for i, (x_piece, y_piece) in enumerate(zip(x_piecewise, y_piecewise, strict=False)):
|
|
532
|
+
ds = xr.Dataset(
|
|
533
|
+
{
|
|
534
|
+
x_label: xr.concat([x_piece.start, x_piece.end], dim='point'),
|
|
535
|
+
'output': xr.concat([y_piece.start, y_piece.end], dim='point'),
|
|
536
|
+
}
|
|
537
|
+
)
|
|
538
|
+
ds = ds.assign_coords(point=['start', 'end'])
|
|
539
|
+
ds['flow'] = y_label
|
|
540
|
+
ds['piece'] = f'Piece {i}'
|
|
541
|
+
datasets.append(ds)
|
|
542
|
+
|
|
543
|
+
combined = xr.concat(datasets, dim='trace')
|
|
544
|
+
|
|
545
|
+
# Apply selection if provided
|
|
546
|
+
if select:
|
|
547
|
+
valid_select = {k: v for k, v in select.items() if k in combined.dims or k in combined.coords}
|
|
548
|
+
if valid_select:
|
|
549
|
+
combined = combined.sel(valid_select)
|
|
550
|
+
|
|
551
|
+
df = combined.to_dataframe().reset_index()
|
|
552
|
+
|
|
553
|
+
# Check if values vary over time
|
|
554
|
+
has_time = 'time' in df.columns
|
|
555
|
+
varies_over_time = False
|
|
556
|
+
if has_time:
|
|
557
|
+
varies_over_time = df.groupby(['trace', 'point'])[[x_label, 'output']].nunique().max().max() > 1
|
|
558
|
+
|
|
559
|
+
if varies_over_time:
|
|
560
|
+
# Time-varying: color by time, dash by piece
|
|
561
|
+
df['time_idx'] = df.groupby('time').ngroup()
|
|
562
|
+
df['line_id'] = df['trace'].astype(str) + '_' + df['time_idx'].astype(str)
|
|
563
|
+
n_times = df['time_idx'].nunique()
|
|
564
|
+
colors = px.colors.sample_colorscale(colorscale, n_times)
|
|
565
|
+
|
|
566
|
+
fig = px.line(
|
|
567
|
+
df,
|
|
568
|
+
x=x_label,
|
|
569
|
+
y='output',
|
|
570
|
+
color='time_idx',
|
|
571
|
+
line_dash='piece',
|
|
572
|
+
line_group='line_id',
|
|
573
|
+
facet_col='flow' if len(y_flows) > 1 else None,
|
|
574
|
+
title=title or 'Piecewise Conversion',
|
|
575
|
+
markers=True,
|
|
576
|
+
color_discrete_sequence=colors,
|
|
577
|
+
)
|
|
578
|
+
else:
|
|
579
|
+
# Static: dash by piece
|
|
580
|
+
if has_time:
|
|
581
|
+
df = df.groupby(['trace', 'point', 'flow', 'piece']).first().reset_index()
|
|
582
|
+
df['line_id'] = df['trace'].astype(str)
|
|
583
|
+
|
|
584
|
+
fig = px.line(
|
|
585
|
+
df,
|
|
586
|
+
x=x_label,
|
|
587
|
+
y='output',
|
|
588
|
+
line_dash='piece',
|
|
589
|
+
line_group='line_id',
|
|
590
|
+
facet_col='flow' if len(y_flows) > 1 else None,
|
|
591
|
+
title=title or 'Piecewise Conversion',
|
|
592
|
+
markers=True,
|
|
593
|
+
)
|
|
594
|
+
|
|
595
|
+
# Clean up facet titles and axis labels
|
|
596
|
+
fig.for_each_annotation(lambda a: a.update(text=a.text.replace('flow=', '')))
|
|
597
|
+
fig.update_yaxes(title_text='')
|
|
598
|
+
fig.update_xaxes(title_text=x_label)
|
|
599
|
+
|
|
600
|
+
result = PlotResult(data=combined, figure=fig)
|
|
601
|
+
|
|
602
|
+
if show is None:
|
|
603
|
+
show = CONFIG.Plotting.default_show
|
|
604
|
+
if show:
|
|
605
|
+
result.show()
|
|
606
|
+
|
|
607
|
+
return result
|
|
456
608
|
|
|
457
609
|
|
|
458
610
|
@register_class_for_io
|
|
@@ -662,10 +814,142 @@ class PiecewiseEffects(Interface):
|
|
|
662
814
|
for piecewise in self.piecewise_shares.values():
|
|
663
815
|
piecewise.has_time_dim = value
|
|
664
816
|
|
|
665
|
-
def
|
|
666
|
-
|
|
817
|
+
def link_to_flow_system(self, flow_system, prefix: str = '') -> None:
|
|
818
|
+
"""Propagate flow_system reference to nested Piecewise objects."""
|
|
819
|
+
super().link_to_flow_system(flow_system, prefix)
|
|
820
|
+
self.piecewise_origin.link_to_flow_system(flow_system, self._sub_prefix('origin'))
|
|
667
821
|
for effect, piecewise in self.piecewise_shares.items():
|
|
668
|
-
piecewise.
|
|
822
|
+
piecewise.link_to_flow_system(flow_system, self._sub_prefix(effect))
|
|
823
|
+
|
|
824
|
+
def transform_data(self) -> None:
|
|
825
|
+
self.piecewise_origin.transform_data()
|
|
826
|
+
for piecewise in self.piecewise_shares.values():
|
|
827
|
+
piecewise.transform_data()
|
|
828
|
+
|
|
829
|
+
def plot(
|
|
830
|
+
self,
|
|
831
|
+
title: str = '',
|
|
832
|
+
select: dict[str, Any] | None = None,
|
|
833
|
+
colorscale: str | None = None,
|
|
834
|
+
show: bool | None = None,
|
|
835
|
+
) -> PlotResult:
|
|
836
|
+
"""Plot origin vs effect shares with time variation visualization.
|
|
837
|
+
|
|
838
|
+
Visualizes the piecewise linear relationships between the origin variable
|
|
839
|
+
and its effect shares. Each effect is shown in a separate subplot (faceted
|
|
840
|
+
by effect). Pieces are distinguished by line dash style.
|
|
841
|
+
|
|
842
|
+
Note:
|
|
843
|
+
Requires FlowSystem to be connected and transformed (call
|
|
844
|
+
flow_system.connect_and_transform() first).
|
|
845
|
+
|
|
846
|
+
Args:
|
|
847
|
+
title: Plot title.
|
|
848
|
+
select: xarray-style selection dict to filter data,
|
|
849
|
+
e.g. {'time': slice('2024-01-01', '2024-01-02')}.
|
|
850
|
+
colorscale: Colorscale name for time coloring (e.g., 'RdYlBu_r', 'viridis').
|
|
851
|
+
Defaults to CONFIG.Plotting.default_sequential_colorscale.
|
|
852
|
+
show: Whether to display the figure.
|
|
853
|
+
Defaults to CONFIG.Plotting.default_show.
|
|
854
|
+
|
|
855
|
+
Returns:
|
|
856
|
+
PlotResult containing the figure and underlying piecewise data.
|
|
857
|
+
|
|
858
|
+
Examples:
|
|
859
|
+
>>> flow_system.connect_and_transform()
|
|
860
|
+
>>> invest_params.piecewise_effects_of_investment.plot(title='Investment Effects')
|
|
861
|
+
"""
|
|
862
|
+
if not self.flow_system.connected_and_transformed:
|
|
863
|
+
logger.debug('Connecting flow_system for plotting PiecewiseEffects')
|
|
864
|
+
self.flow_system.connect_and_transform()
|
|
865
|
+
|
|
866
|
+
colorscale = colorscale or CONFIG.Plotting.default_sequential_colorscale
|
|
867
|
+
|
|
868
|
+
effect_labels = list(self.piecewise_shares.keys())
|
|
869
|
+
if not effect_labels:
|
|
870
|
+
raise ValueError('Need at least one effect share to plot')
|
|
871
|
+
|
|
872
|
+
# Build Dataset with all piece data
|
|
873
|
+
datasets = []
|
|
874
|
+
for effect_label in effect_labels:
|
|
875
|
+
y_piecewise = self.piecewise_shares[effect_label]
|
|
876
|
+
for i, (x_piece, y_piece) in enumerate(zip(self.piecewise_origin, y_piecewise, strict=False)):
|
|
877
|
+
ds = xr.Dataset(
|
|
878
|
+
{
|
|
879
|
+
'origin': xr.concat([x_piece.start, x_piece.end], dim='point'),
|
|
880
|
+
'share': xr.concat([y_piece.start, y_piece.end], dim='point'),
|
|
881
|
+
}
|
|
882
|
+
)
|
|
883
|
+
ds = ds.assign_coords(point=['start', 'end'])
|
|
884
|
+
ds['effect'] = effect_label
|
|
885
|
+
ds['piece'] = f'Piece {i}'
|
|
886
|
+
datasets.append(ds)
|
|
887
|
+
|
|
888
|
+
combined = xr.concat(datasets, dim='trace')
|
|
889
|
+
|
|
890
|
+
# Apply selection if provided
|
|
891
|
+
if select:
|
|
892
|
+
valid_select = {k: v for k, v in select.items() if k in combined.dims or k in combined.coords}
|
|
893
|
+
if valid_select:
|
|
894
|
+
combined = combined.sel(valid_select)
|
|
895
|
+
|
|
896
|
+
df = combined.to_dataframe().reset_index()
|
|
897
|
+
|
|
898
|
+
# Check if values vary over time
|
|
899
|
+
has_time = 'time' in df.columns
|
|
900
|
+
varies_over_time = False
|
|
901
|
+
if has_time:
|
|
902
|
+
varies_over_time = df.groupby(['trace', 'point'])[['origin', 'share']].nunique().max().max() > 1
|
|
903
|
+
|
|
904
|
+
if varies_over_time:
|
|
905
|
+
# Time-varying: color by time, dash by piece
|
|
906
|
+
df['time_idx'] = df.groupby('time').ngroup()
|
|
907
|
+
df['line_id'] = df['trace'].astype(str) + '_' + df['time_idx'].astype(str)
|
|
908
|
+
n_times = df['time_idx'].nunique()
|
|
909
|
+
colors = px.colors.sample_colorscale(colorscale, n_times)
|
|
910
|
+
|
|
911
|
+
fig = px.line(
|
|
912
|
+
df,
|
|
913
|
+
x='origin',
|
|
914
|
+
y='share',
|
|
915
|
+
color='time_idx',
|
|
916
|
+
line_dash='piece',
|
|
917
|
+
line_group='line_id',
|
|
918
|
+
facet_col='effect' if len(effect_labels) > 1 else None,
|
|
919
|
+
title=title or 'Piecewise Effects',
|
|
920
|
+
markers=True,
|
|
921
|
+
color_discrete_sequence=colors,
|
|
922
|
+
)
|
|
923
|
+
else:
|
|
924
|
+
# Static: dash by piece
|
|
925
|
+
if has_time:
|
|
926
|
+
df = df.groupby(['trace', 'point', 'effect', 'piece']).first().reset_index()
|
|
927
|
+
df['line_id'] = df['trace'].astype(str)
|
|
928
|
+
|
|
929
|
+
fig = px.line(
|
|
930
|
+
df,
|
|
931
|
+
x='origin',
|
|
932
|
+
y='share',
|
|
933
|
+
line_dash='piece',
|
|
934
|
+
line_group='line_id',
|
|
935
|
+
facet_col='effect' if len(effect_labels) > 1 else None,
|
|
936
|
+
title=title or 'Piecewise Effects',
|
|
937
|
+
markers=True,
|
|
938
|
+
)
|
|
939
|
+
|
|
940
|
+
# Clean up facet titles and axis labels
|
|
941
|
+
fig.for_each_annotation(lambda a: a.update(text=a.text.replace('effect=', '')))
|
|
942
|
+
fig.update_yaxes(title_text='')
|
|
943
|
+
fig.update_xaxes(title_text='Origin')
|
|
944
|
+
|
|
945
|
+
result = PlotResult(data=combined, figure=fig)
|
|
946
|
+
|
|
947
|
+
if show is None:
|
|
948
|
+
show = CONFIG.Plotting.default_show
|
|
949
|
+
if show:
|
|
950
|
+
result.show()
|
|
951
|
+
|
|
952
|
+
return result
|
|
669
953
|
|
|
670
954
|
|
|
671
955
|
@register_class_for_io
|
|
@@ -691,14 +975,13 @@ class InvestParameters(Interface):
|
|
|
691
975
|
- **Divestment Effects**: Penalties for not investing (demolition, opportunity costs)
|
|
692
976
|
|
|
693
977
|
Mathematical Formulation:
|
|
694
|
-
See
|
|
695
|
-
[InvestParameters](../user-guide/mathematical-notation/features/InvestParameters.md)
|
|
978
|
+
See <https://flixopt.github.io/flixopt/latest/user-guide/mathematical-notation/features/InvestParameters/>
|
|
696
979
|
|
|
697
980
|
Args:
|
|
698
981
|
fixed_size: Creates binary decision at this exact size. None allows continuous sizing.
|
|
699
982
|
minimum_size: Lower bound for continuous sizing. Default: CONFIG.Modeling.epsilon.
|
|
700
983
|
Ignored if fixed_size is specified.
|
|
701
|
-
maximum_size: Upper bound for continuous sizing.
|
|
984
|
+
maximum_size: Upper bound for continuous sizing. Required if fixed_size is not set.
|
|
702
985
|
Ignored if fixed_size is specified.
|
|
703
986
|
mandatory: Controls whether investment is required. When True, forces investment
|
|
704
987
|
to occur (useful for mandatory upgrades or replacement decisions).
|
|
@@ -712,26 +995,15 @@ class InvestParameters(Interface):
|
|
|
712
995
|
Combinable with effects_of_investment and effects_of_investment_per_size.
|
|
713
996
|
effects_of_retirement: Costs incurred if NOT investing (demolition, penalties).
|
|
714
997
|
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
998
|
linked_periods: Describes which periods are linked. 1 means linked, 0 means size=0. None means no linked periods.
|
|
999
|
+
For convenience, pass a tuple containing the first and last period (2025, 2039), linking them and those in between
|
|
728
1000
|
|
|
729
1001
|
Cost Annualization Requirements:
|
|
730
1002
|
All cost values must be properly weighted to match the optimization model's time horizon.
|
|
731
1003
|
For long-term investments, the cost values should be annualized to the corresponding operation time (annuity).
|
|
732
1004
|
|
|
733
1005
|
- Use equivalent annual cost (capital cost / equipment lifetime)
|
|
734
|
-
- Apply appropriate discount rates for present value
|
|
1006
|
+
- Apply appropriate discount rates for present value optimizations
|
|
735
1007
|
- Account for inflation, escalation, and financing costs
|
|
736
1008
|
|
|
737
1009
|
Example: €1M equipment with 20-year life → €50k/year fixed cost
|
|
@@ -873,89 +1145,69 @@ class InvestParameters(Interface):
|
|
|
873
1145
|
|
|
874
1146
|
def __init__(
|
|
875
1147
|
self,
|
|
876
|
-
fixed_size:
|
|
877
|
-
minimum_size:
|
|
878
|
-
maximum_size:
|
|
1148
|
+
fixed_size: Numeric_PS | None = None,
|
|
1149
|
+
minimum_size: Numeric_PS | None = None,
|
|
1150
|
+
maximum_size: Numeric_PS | None = None,
|
|
879
1151
|
mandatory: bool = False,
|
|
880
|
-
effects_of_investment:
|
|
881
|
-
effects_of_investment_per_size:
|
|
882
|
-
effects_of_retirement:
|
|
1152
|
+
effects_of_investment: Effect_PS | Numeric_PS | None = None,
|
|
1153
|
+
effects_of_investment_per_size: Effect_PS | Numeric_PS | None = None,
|
|
1154
|
+
effects_of_retirement: Effect_PS | Numeric_PS | None = None,
|
|
883
1155
|
piecewise_effects_of_investment: PiecewiseEffects | None = None,
|
|
884
|
-
linked_periods:
|
|
885
|
-
**kwargs,
|
|
1156
|
+
linked_periods: Numeric_PS | tuple[int, int] | None = None,
|
|
886
1157
|
):
|
|
887
|
-
|
|
888
|
-
|
|
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
|
-
)
|
|
1158
|
+
self.effects_of_investment = effects_of_investment if effects_of_investment is not None else {}
|
|
1159
|
+
self.effects_of_retirement = effects_of_retirement if effects_of_retirement is not None else {}
|
|
920
1160
|
self.fixed_size = fixed_size
|
|
921
1161
|
self.mandatory = mandatory
|
|
922
|
-
self.effects_of_investment_per_size
|
|
1162
|
+
self.effects_of_investment_per_size = (
|
|
923
1163
|
effects_of_investment_per_size if effects_of_investment_per_size is not None else {}
|
|
924
1164
|
)
|
|
925
1165
|
self.piecewise_effects_of_investment = piecewise_effects_of_investment
|
|
926
1166
|
self.minimum_size = minimum_size if minimum_size is not None else CONFIG.Modeling.epsilon
|
|
927
|
-
self.maximum_size = maximum_size
|
|
1167
|
+
self.maximum_size = maximum_size
|
|
928
1168
|
self.linked_periods = linked_periods
|
|
929
1169
|
|
|
930
|
-
def
|
|
931
|
-
|
|
932
|
-
|
|
1170
|
+
def link_to_flow_system(self, flow_system, prefix: str = '') -> None:
|
|
1171
|
+
"""Propagate flow_system reference to nested PiecewiseEffects object if present."""
|
|
1172
|
+
super().link_to_flow_system(flow_system, prefix)
|
|
1173
|
+
if self.piecewise_effects_of_investment is not None:
|
|
1174
|
+
self.piecewise_effects_of_investment.link_to_flow_system(flow_system, self._sub_prefix('PiecewiseEffects'))
|
|
1175
|
+
|
|
1176
|
+
def transform_data(self) -> None:
|
|
1177
|
+
# Validate that either fixed_size or maximum_size is set
|
|
1178
|
+
if self.fixed_size is None and self.maximum_size is None:
|
|
1179
|
+
raise ValueError(
|
|
1180
|
+
f'InvestParameters in "{self.prefix}" requires either fixed_size or maximum_size to be set. '
|
|
1181
|
+
f'An upper bound is needed to properly scale the optimization model.'
|
|
1182
|
+
)
|
|
1183
|
+
self.effects_of_investment = self._fit_effect_coords(
|
|
1184
|
+
prefix=self.prefix,
|
|
933
1185
|
effect_values=self.effects_of_investment,
|
|
934
|
-
|
|
1186
|
+
suffix='effects_of_investment',
|
|
935
1187
|
dims=['period', 'scenario'],
|
|
936
1188
|
)
|
|
937
|
-
self.effects_of_retirement =
|
|
938
|
-
|
|
1189
|
+
self.effects_of_retirement = self._fit_effect_coords(
|
|
1190
|
+
prefix=self.prefix,
|
|
939
1191
|
effect_values=self.effects_of_retirement,
|
|
940
|
-
|
|
1192
|
+
suffix='effects_of_retirement',
|
|
941
1193
|
dims=['period', 'scenario'],
|
|
942
1194
|
)
|
|
943
|
-
self.effects_of_investment_per_size =
|
|
944
|
-
|
|
1195
|
+
self.effects_of_investment_per_size = self._fit_effect_coords(
|
|
1196
|
+
prefix=self.prefix,
|
|
945
1197
|
effect_values=self.effects_of_investment_per_size,
|
|
946
|
-
|
|
1198
|
+
suffix='effects_of_investment_per_size',
|
|
947
1199
|
dims=['period', 'scenario'],
|
|
948
1200
|
)
|
|
949
1201
|
|
|
950
1202
|
if self.piecewise_effects_of_investment is not None:
|
|
951
1203
|
self.piecewise_effects_of_investment.has_time_dim = False
|
|
952
|
-
self.piecewise_effects_of_investment.transform_data(
|
|
1204
|
+
self.piecewise_effects_of_investment.transform_data()
|
|
953
1205
|
|
|
954
|
-
self.minimum_size =
|
|
955
|
-
f'{
|
|
1206
|
+
self.minimum_size = self._fit_coords(
|
|
1207
|
+
f'{self.prefix}|minimum_size', self.minimum_size, dims=['period', 'scenario']
|
|
956
1208
|
)
|
|
957
|
-
self.maximum_size =
|
|
958
|
-
f'{
|
|
1209
|
+
self.maximum_size = self._fit_coords(
|
|
1210
|
+
f'{self.prefix}|maximum_size', self.maximum_size, dims=['period', 'scenario']
|
|
959
1211
|
)
|
|
960
1212
|
# Convert tuple (first_period, last_period) to DataArray if needed
|
|
961
1213
|
if isinstance(self.linked_periods, (tuple, list)):
|
|
@@ -963,87 +1215,57 @@ class InvestParameters(Interface):
|
|
|
963
1215
|
raise TypeError(
|
|
964
1216
|
f'If you provide a tuple to "linked_periods", it needs to be len=2. Got {len(self.linked_periods)=}'
|
|
965
1217
|
)
|
|
1218
|
+
if self.flow_system.periods is None:
|
|
1219
|
+
raise ValueError(
|
|
1220
|
+
f'Cannot use linked_periods={self.linked_periods} when FlowSystem has no periods defined. '
|
|
1221
|
+
f'Please define periods in FlowSystem or use linked_periods=None.'
|
|
1222
|
+
)
|
|
966
1223
|
logger.debug(f'Computing linked_periods from {self.linked_periods}')
|
|
967
1224
|
start, end = self.linked_periods
|
|
968
|
-
if start not in flow_system.periods.values:
|
|
1225
|
+
if start not in self.flow_system.periods.values:
|
|
969
1226
|
logger.warning(
|
|
970
|
-
f'Start of linked periods ({start} not found in periods directly: {flow_system.periods.values}'
|
|
1227
|
+
f'Start of linked periods ({start} not found in periods directly: {self.flow_system.periods.values}'
|
|
971
1228
|
)
|
|
972
|
-
if end not in flow_system.periods.values:
|
|
1229
|
+
if end not in self.flow_system.periods.values:
|
|
973
1230
|
logger.warning(
|
|
974
|
-
f'End of linked periods ({end} not found in periods directly: {flow_system.periods.values}'
|
|
1231
|
+
f'End of linked periods ({end} not found in periods directly: {self.flow_system.periods.values}'
|
|
975
1232
|
)
|
|
976
|
-
self.linked_periods = self.compute_linked_periods(start, end, flow_system.periods)
|
|
1233
|
+
self.linked_periods = self.compute_linked_periods(start, end, self.flow_system.periods)
|
|
977
1234
|
logger.debug(f'Computed {self.linked_periods=}')
|
|
978
1235
|
|
|
979
|
-
self.linked_periods =
|
|
980
|
-
f'{
|
|
981
|
-
)
|
|
982
|
-
self.fixed_size = flow_system.fit_to_model_coords(
|
|
983
|
-
f'{name_prefix}|fixed_size', self.fixed_size, dims=['period', 'scenario']
|
|
1236
|
+
self.linked_periods = self._fit_coords(
|
|
1237
|
+
f'{self.prefix}|linked_periods', self.linked_periods, dims=['period', 'scenario']
|
|
984
1238
|
)
|
|
1239
|
+
self.fixed_size = self._fit_coords(f'{self.prefix}|fixed_size', self.fixed_size, dims=['period', 'scenario'])
|
|
985
1240
|
|
|
986
1241
|
@property
|
|
987
|
-
def
|
|
988
|
-
|
|
989
|
-
import warnings
|
|
990
|
-
|
|
991
|
-
warnings.warn("Property 'optional' is deprecated. Use 'mandatory' instead.", DeprecationWarning, stacklevel=2)
|
|
992
|
-
return not self.mandatory
|
|
993
|
-
|
|
994
|
-
@optional.setter
|
|
995
|
-
def optional(self, value: bool):
|
|
996
|
-
"""DEPRECATED: Use 'mandatory' property instead. Sets the opposite of the given value to 'mandatory'."""
|
|
997
|
-
warnings.warn("Property 'optional' is deprecated. Use 'mandatory' instead.", DeprecationWarning, stacklevel=2)
|
|
998
|
-
self.mandatory = not value
|
|
999
|
-
|
|
1000
|
-
@property
|
|
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
|
|
1242
|
+
def minimum_or_fixed_size(self) -> Numeric_PS:
|
|
1243
|
+
return self.fixed_size if self.fixed_size is not None else self.minimum_size
|
|
1009
1244
|
|
|
1010
1245
|
@property
|
|
1011
|
-
def
|
|
1012
|
-
|
|
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
|
|
1246
|
+
def maximum_or_fixed_size(self) -> Numeric_PS:
|
|
1247
|
+
return self.fixed_size if self.fixed_size is not None else self.maximum_size
|
|
1019
1248
|
|
|
1020
|
-
|
|
1021
|
-
|
|
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
|
|
1249
|
+
def format_for_repr(self) -> str:
|
|
1250
|
+
"""Format InvestParameters for display in repr methods.
|
|
1029
1251
|
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
"""
|
|
1033
|
-
|
|
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
|
|
1252
|
+
Returns:
|
|
1253
|
+
Formatted string showing size information
|
|
1254
|
+
"""
|
|
1255
|
+
from .io import numeric_to_str_for_repr
|
|
1039
1256
|
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1257
|
+
if self.fixed_size is not None:
|
|
1258
|
+
val = numeric_to_str_for_repr(self.fixed_size)
|
|
1259
|
+
status = 'mandatory' if self.mandatory else 'optional'
|
|
1260
|
+
return f'{val} ({status})'
|
|
1043
1261
|
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1262
|
+
# Show range if available
|
|
1263
|
+
parts = []
|
|
1264
|
+
if self.minimum_size is not None:
|
|
1265
|
+
parts.append(f'min: {numeric_to_str_for_repr(self.minimum_size)}')
|
|
1266
|
+
if self.maximum_size is not None:
|
|
1267
|
+
parts.append(f'max: {numeric_to_str_for_repr(self.maximum_size)}')
|
|
1268
|
+
return ', '.join(parts) if parts else 'invest'
|
|
1047
1269
|
|
|
1048
1270
|
@staticmethod
|
|
1049
1271
|
def compute_linked_periods(first_period: int, last_period: int, periods: pd.Index | list[int]) -> xr.DataArray:
|
|
@@ -1058,19 +1280,19 @@ class InvestParameters(Interface):
|
|
|
1058
1280
|
|
|
1059
1281
|
|
|
1060
1282
|
@register_class_for_io
|
|
1061
|
-
class
|
|
1062
|
-
"""Define operational constraints and effects for binary
|
|
1283
|
+
class StatusParameters(Interface):
|
|
1284
|
+
"""Define operational constraints and effects for binary status equipment behavior.
|
|
1063
1285
|
|
|
1064
|
-
This class models equipment that operates in discrete states (
|
|
1286
|
+
This class models equipment that operates in discrete states (active/inactive) rather than
|
|
1065
1287
|
continuous operation, capturing realistic operational constraints and associated
|
|
1066
1288
|
costs. It handles complex equipment behavior including startup costs, minimum
|
|
1067
1289
|
run times, cycling limitations, and maintenance scheduling requirements.
|
|
1068
1290
|
|
|
1069
1291
|
Key Modeling Capabilities:
|
|
1070
|
-
**
|
|
1071
|
-
**Runtime Constraints**: Minimum and maximum continuous operation periods
|
|
1072
|
-
**Cycling Limits**: Maximum number of
|
|
1073
|
-
**Operating Hours**: Total
|
|
1292
|
+
**Startup Costs**: One-time costs for starting equipment (fuel, wear, labor)
|
|
1293
|
+
**Runtime Constraints**: Minimum and maximum continuous operation periods (uptime/downtime)
|
|
1294
|
+
**Cycling Limits**: Maximum number of startups to prevent excessive wear
|
|
1295
|
+
**Operating Hours**: Total active hours limits and requirements over time horizon
|
|
1074
1296
|
|
|
1075
1297
|
Typical Equipment Applications:
|
|
1076
1298
|
- **Power Plants**: Combined cycle units, steam turbines with startup costs
|
|
@@ -1080,46 +1302,53 @@ class OnOffParameters(Interface):
|
|
|
1080
1302
|
- **Process Equipment**: Compressors, pumps with operational constraints
|
|
1081
1303
|
|
|
1082
1304
|
Mathematical Formulation:
|
|
1083
|
-
See
|
|
1084
|
-
[OnOffParameters](../user-guide/mathematical-notation/features/OnOffParameters.md)
|
|
1305
|
+
See <https://flixopt.github.io/flixopt/latest/user-guide/mathematical-notation/features/StatusParameters/>
|
|
1085
1306
|
|
|
1086
1307
|
Args:
|
|
1087
|
-
|
|
1088
|
-
|
|
1308
|
+
effects_per_startup: Costs or impacts incurred for each transition from
|
|
1309
|
+
inactive state (status=0) to active state (status=1). Represents startup costs,
|
|
1089
1310
|
wear and tear, or other switching impacts. Dictionary mapping effect
|
|
1090
1311
|
names to values (e.g., {'cost': 500, 'maintenance_hours': 2}).
|
|
1091
|
-
|
|
1092
|
-
in the
|
|
1312
|
+
effects_per_active_hour: Ongoing costs or impacts while equipment operates
|
|
1313
|
+
in the active state. Includes fuel costs, labor, consumables, or emissions.
|
|
1093
1314
|
Dictionary mapping effect names to hourly values (e.g., {'fuel_cost': 45}).
|
|
1094
|
-
|
|
1315
|
+
active_hours_min: Minimum total active hours across the entire time horizon per period.
|
|
1095
1316
|
Ensures equipment meets minimum utilization requirements or contractual
|
|
1096
1317
|
obligations (e.g., power purchase agreements, maintenance schedules).
|
|
1097
|
-
|
|
1318
|
+
active_hours_max: Maximum total active hours across the entire time horizon per period.
|
|
1098
1319
|
Limits equipment usage due to maintenance schedules, fuel availability,
|
|
1099
1320
|
environmental permits, or equipment lifetime constraints.
|
|
1100
|
-
|
|
1321
|
+
min_uptime: Minimum continuous operating duration once started (unit commitment term).
|
|
1101
1322
|
Models minimum run times due to thermal constraints, process stability,
|
|
1102
1323
|
or efficiency considerations. Can be time-varying to reflect different
|
|
1103
1324
|
constraints across the planning horizon.
|
|
1104
|
-
|
|
1325
|
+
max_uptime: Maximum continuous operating duration in one campaign (unit commitment term).
|
|
1105
1326
|
Models mandatory maintenance intervals, process batch sizes, or
|
|
1106
1327
|
equipment thermal limits requiring periodic shutdowns.
|
|
1107
|
-
|
|
1328
|
+
min_downtime: Minimum continuous shutdown duration between operations (unit commitment term).
|
|
1108
1329
|
Models cooling periods, maintenance requirements, or process constraints
|
|
1109
1330
|
that prevent immediate restart after shutdown.
|
|
1110
|
-
|
|
1331
|
+
max_downtime: Maximum continuous shutdown duration before mandatory
|
|
1111
1332
|
restart. Models equipment preservation, process stability, or contractual
|
|
1112
1333
|
requirements for minimum activity levels.
|
|
1113
|
-
|
|
1334
|
+
startup_limit: Maximum number of startup operations across the time horizon per period..
|
|
1114
1335
|
Limits equipment cycling to reduce wear, maintenance costs, or comply
|
|
1115
1336
|
with operational constraints (e.g., grid stability requirements).
|
|
1116
|
-
|
|
1117
|
-
|
|
1337
|
+
force_startup_tracking: When True, creates startup variables even without explicit
|
|
1338
|
+
startup_limit constraint. Useful for tracking or reporting startup
|
|
1118
1339
|
events without enforcing limits.
|
|
1340
|
+
cluster_mode: How inter-timestep constraints are handled at cluster boundaries.
|
|
1341
|
+
Only relevant when using ``transform.cluster()``. Options:
|
|
1342
|
+
|
|
1343
|
+
- ``'relaxed'``: No constraint at cluster boundaries. Startups at the first
|
|
1344
|
+
timestep of each cluster are not forced - the optimizer is free to choose.
|
|
1345
|
+
This prevents clustering from inducing "phantom" startups. (default)
|
|
1346
|
+
- ``'cyclic'``: Each cluster's final status equals its initial status.
|
|
1347
|
+
Ensures consistent behavior within each representative period.
|
|
1119
1348
|
|
|
1120
1349
|
Note:
|
|
1121
1350
|
**Time Series Boundary Handling**: The final time period constraints for
|
|
1122
|
-
|
|
1351
|
+
min_uptime/max_uptime and min_downtime/max_downtime are not
|
|
1123
1352
|
enforced, allowing the optimization to end with ongoing campaigns that
|
|
1124
1353
|
may be shorter than the specified minimums or longer than maximums.
|
|
1125
1354
|
|
|
@@ -1127,105 +1356,105 @@ class OnOffParameters(Interface):
|
|
|
1127
1356
|
Combined cycle power plant with startup costs and minimum run time:
|
|
1128
1357
|
|
|
1129
1358
|
```python
|
|
1130
|
-
power_plant_operation =
|
|
1131
|
-
|
|
1359
|
+
power_plant_operation = StatusParameters(
|
|
1360
|
+
effects_per_startup={
|
|
1132
1361
|
'startup_cost': 25000, # €25,000 per startup
|
|
1133
1362
|
'startup_fuel': 150, # GJ natural gas for startup
|
|
1134
1363
|
'startup_time': 4, # Hours to reach full output
|
|
1135
1364
|
'maintenance_impact': 0.1, # Fractional life consumption
|
|
1136
1365
|
},
|
|
1137
|
-
|
|
1138
|
-
'fixed_om': 125, # Fixed O&M costs while
|
|
1366
|
+
effects_per_active_hour={
|
|
1367
|
+
'fixed_om': 125, # Fixed O&M costs while active
|
|
1139
1368
|
'auxiliary_power': 2.5, # MW parasitic loads
|
|
1140
1369
|
},
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1370
|
+
min_uptime=8, # Minimum 8-hour run once started
|
|
1371
|
+
min_downtime=4, # Minimum 4-hour cooling period
|
|
1372
|
+
active_hours_max=6000, # Annual operating limit
|
|
1144
1373
|
)
|
|
1145
1374
|
```
|
|
1146
1375
|
|
|
1147
1376
|
Industrial batch process with cycling limits:
|
|
1148
1377
|
|
|
1149
1378
|
```python
|
|
1150
|
-
batch_reactor =
|
|
1151
|
-
|
|
1379
|
+
batch_reactor = StatusParameters(
|
|
1380
|
+
effects_per_startup={
|
|
1152
1381
|
'setup_cost': 1500, # Labor and materials for startup
|
|
1153
1382
|
'catalyst_consumption': 5, # kg catalyst per batch
|
|
1154
1383
|
'cleaning_chemicals': 200, # L cleaning solution
|
|
1155
1384
|
},
|
|
1156
|
-
|
|
1385
|
+
effects_per_active_hour={
|
|
1157
1386
|
'steam': 2.5, # t/h process steam
|
|
1158
1387
|
'electricity': 150, # kWh electrical load
|
|
1159
1388
|
'cooling_water': 50, # m³/h cooling water
|
|
1160
1389
|
},
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1390
|
+
min_uptime=12, # Minimum batch size (12 hours)
|
|
1391
|
+
max_uptime=24, # Maximum batch size (24 hours)
|
|
1392
|
+
min_downtime=6, # Cleaning and setup time
|
|
1393
|
+
startup_limit=200, # Maximum 200 batches per period
|
|
1394
|
+
active_hours_max=4000, # Maximum production time
|
|
1166
1395
|
)
|
|
1167
1396
|
```
|
|
1168
1397
|
|
|
1169
1398
|
HVAC system with thermostat control and maintenance:
|
|
1170
1399
|
|
|
1171
1400
|
```python
|
|
1172
|
-
hvac_operation =
|
|
1173
|
-
|
|
1401
|
+
hvac_operation = StatusParameters(
|
|
1402
|
+
effects_per_startup={
|
|
1174
1403
|
'compressor_wear': 0.5, # Hours of compressor life per start
|
|
1175
1404
|
'inrush_current': 15, # kW peak demand on startup
|
|
1176
1405
|
},
|
|
1177
|
-
|
|
1406
|
+
effects_per_active_hour={
|
|
1178
1407
|
'electricity': 25, # kW electrical consumption
|
|
1179
1408
|
'maintenance': 0.12, # €/hour maintenance reserve
|
|
1180
1409
|
},
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1410
|
+
min_uptime=1, # Minimum 1-hour run to avoid cycling
|
|
1411
|
+
min_downtime=0.5, # 30-minute minimum inactive time
|
|
1412
|
+
startup_limit=2000, # Limit cycling for compressor life
|
|
1413
|
+
active_hours_min=2000, # Minimum operation for humidity control
|
|
1414
|
+
active_hours_max=5000, # Maximum operation for energy budget
|
|
1186
1415
|
)
|
|
1187
1416
|
```
|
|
1188
1417
|
|
|
1189
1418
|
Backup generator with testing and maintenance requirements:
|
|
1190
1419
|
|
|
1191
1420
|
```python
|
|
1192
|
-
backup_generator =
|
|
1193
|
-
|
|
1421
|
+
backup_generator = StatusParameters(
|
|
1422
|
+
effects_per_startup={
|
|
1194
1423
|
'fuel_priming': 50, # L diesel for system priming
|
|
1195
1424
|
'wear_factor': 1.0, # Start cycles impact on maintenance
|
|
1196
1425
|
'testing_labor': 2, # Hours technician time per test
|
|
1197
1426
|
},
|
|
1198
|
-
|
|
1427
|
+
effects_per_active_hour={
|
|
1199
1428
|
'fuel_consumption': 180, # L/h diesel consumption
|
|
1200
1429
|
'emissions_permit': 15, # € emissions allowance cost
|
|
1201
1430
|
'noise_penalty': 25, # € noise compliance cost
|
|
1202
1431
|
},
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
|
|
1432
|
+
min_uptime=0.5, # Minimum test duration (30 min)
|
|
1433
|
+
max_downtime=720, # Maximum 30 days between tests
|
|
1434
|
+
startup_limit=52, # Weekly testing limit
|
|
1435
|
+
active_hours_min=26, # Minimum annual testing (0.5h × 52)
|
|
1436
|
+
active_hours_max=200, # Maximum runtime (emergencies + tests)
|
|
1208
1437
|
)
|
|
1209
1438
|
```
|
|
1210
1439
|
|
|
1211
1440
|
Peak shaving battery with cycling degradation:
|
|
1212
1441
|
|
|
1213
1442
|
```python
|
|
1214
|
-
battery_cycling =
|
|
1215
|
-
|
|
1443
|
+
battery_cycling = StatusParameters(
|
|
1444
|
+
effects_per_startup={
|
|
1216
1445
|
'cycle_degradation': 0.01, # % capacity loss per cycle
|
|
1217
1446
|
'inverter_startup': 0.5, # kWh losses during startup
|
|
1218
1447
|
},
|
|
1219
|
-
|
|
1448
|
+
effects_per_active_hour={
|
|
1220
1449
|
'standby_losses': 2, # kW standby consumption
|
|
1221
1450
|
'cooling': 5, # kW thermal management
|
|
1222
1451
|
'inverter_losses': 8, # kW conversion losses
|
|
1223
1452
|
},
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
|
|
1453
|
+
min_uptime=1, # Minimum discharge duration
|
|
1454
|
+
max_uptime=4, # Maximum continuous discharge
|
|
1455
|
+
min_downtime=1, # Minimum rest between cycles
|
|
1456
|
+
startup_limit=365, # Daily cycling limit
|
|
1457
|
+
force_startup_tracking=True, # Track all cycling events
|
|
1229
1458
|
)
|
|
1230
1459
|
```
|
|
1231
1460
|
|
|
@@ -1241,86 +1470,75 @@ class OnOffParameters(Interface):
|
|
|
1241
1470
|
|
|
1242
1471
|
def __init__(
|
|
1243
1472
|
self,
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
|
|
1473
|
+
effects_per_startup: Effect_TPS | Numeric_TPS | None = None,
|
|
1474
|
+
effects_per_active_hour: Effect_TPS | Numeric_TPS | None = None,
|
|
1475
|
+
active_hours_min: Numeric_PS | None = None,
|
|
1476
|
+
active_hours_max: Numeric_PS | None = None,
|
|
1477
|
+
min_uptime: Numeric_TPS | None = None,
|
|
1478
|
+
max_uptime: Numeric_TPS | None = None,
|
|
1479
|
+
min_downtime: Numeric_TPS | None = None,
|
|
1480
|
+
max_downtime: Numeric_TPS | None = None,
|
|
1481
|
+
startup_limit: Numeric_PS | None = None,
|
|
1482
|
+
force_startup_tracking: bool = False,
|
|
1483
|
+
cluster_mode: Literal['relaxed', 'cyclic'] = 'relaxed',
|
|
1254
1484
|
):
|
|
1255
|
-
self.
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
self.
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
self.
|
|
1262
|
-
self.
|
|
1263
|
-
self.
|
|
1264
|
-
self.
|
|
1265
|
-
self.
|
|
1266
|
-
|
|
1267
|
-
|
|
1268
|
-
self.
|
|
1269
|
-
|
|
1270
|
-
|
|
1271
|
-
|
|
1272
|
-
name_prefix, self.effects_per_switch_on, 'per_switch_on'
|
|
1273
|
-
)
|
|
1274
|
-
self.effects_per_running_hour = flow_system.fit_effects_to_model_coords(
|
|
1275
|
-
name_prefix, self.effects_per_running_hour, 'per_running_hour'
|
|
1485
|
+
self.effects_per_startup = effects_per_startup if effects_per_startup is not None else {}
|
|
1486
|
+
self.effects_per_active_hour = effects_per_active_hour if effects_per_active_hour is not None else {}
|
|
1487
|
+
self.active_hours_min = active_hours_min
|
|
1488
|
+
self.active_hours_max = active_hours_max
|
|
1489
|
+
self.min_uptime = min_uptime
|
|
1490
|
+
self.max_uptime = max_uptime
|
|
1491
|
+
self.min_downtime = min_downtime
|
|
1492
|
+
self.max_downtime = max_downtime
|
|
1493
|
+
self.startup_limit = startup_limit
|
|
1494
|
+
self.force_startup_tracking: bool = force_startup_tracking
|
|
1495
|
+
self.cluster_mode = cluster_mode
|
|
1496
|
+
|
|
1497
|
+
def transform_data(self) -> None:
|
|
1498
|
+
self.effects_per_startup = self._fit_effect_coords(
|
|
1499
|
+
prefix=self.prefix,
|
|
1500
|
+
effect_values=self.effects_per_startup,
|
|
1501
|
+
suffix='per_startup',
|
|
1276
1502
|
)
|
|
1277
|
-
self.
|
|
1278
|
-
|
|
1503
|
+
self.effects_per_active_hour = self._fit_effect_coords(
|
|
1504
|
+
prefix=self.prefix,
|
|
1505
|
+
effect_values=self.effects_per_active_hour,
|
|
1506
|
+
suffix='per_active_hour',
|
|
1279
1507
|
)
|
|
1280
|
-
self.
|
|
1281
|
-
|
|
1508
|
+
self.min_uptime = self._fit_coords(f'{self.prefix}|min_uptime', self.min_uptime)
|
|
1509
|
+
self.max_uptime = self._fit_coords(f'{self.prefix}|max_uptime', self.max_uptime)
|
|
1510
|
+
self.min_downtime = self._fit_coords(f'{self.prefix}|min_downtime', self.min_downtime)
|
|
1511
|
+
self.max_downtime = self._fit_coords(f'{self.prefix}|max_downtime', self.max_downtime)
|
|
1512
|
+
self.active_hours_max = self._fit_coords(
|
|
1513
|
+
f'{self.prefix}|active_hours_max', self.active_hours_max, dims=['period', 'scenario']
|
|
1282
1514
|
)
|
|
1283
|
-
self.
|
|
1284
|
-
f'{
|
|
1515
|
+
self.active_hours_min = self._fit_coords(
|
|
1516
|
+
f'{self.prefix}|active_hours_min', self.active_hours_min, dims=['period', 'scenario']
|
|
1285
1517
|
)
|
|
1286
|
-
self.
|
|
1287
|
-
f'{
|
|
1518
|
+
self.startup_limit = self._fit_coords(
|
|
1519
|
+
f'{self.prefix}|startup_limit', self.startup_limit, dims=['period', 'scenario']
|
|
1288
1520
|
)
|
|
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
|
-
)
|
|
1298
|
-
|
|
1299
|
-
@property
|
|
1300
|
-
def use_off(self) -> bool:
|
|
1301
|
-
"""Proxy: whether OFF variable is required"""
|
|
1302
|
-
return self.use_consecutive_off_hours
|
|
1303
1521
|
|
|
1304
1522
|
@property
|
|
1305
|
-
def
|
|
1306
|
-
"""Determines whether a Variable for consecutive
|
|
1307
|
-
return any(param is not None for param in [self.
|
|
1523
|
+
def use_uptime_tracking(self) -> bool:
|
|
1524
|
+
"""Determines whether a Variable for uptime (consecutive active hours) is needed or not"""
|
|
1525
|
+
return any(param is not None for param in [self.min_uptime, self.max_uptime])
|
|
1308
1526
|
|
|
1309
1527
|
@property
|
|
1310
|
-
def
|
|
1311
|
-
"""Determines whether a Variable for consecutive
|
|
1312
|
-
return any(param is not None for param in [self.
|
|
1528
|
+
def use_downtime_tracking(self) -> bool:
|
|
1529
|
+
"""Determines whether a Variable for downtime (consecutive inactive hours) is needed or not"""
|
|
1530
|
+
return any(param is not None for param in [self.min_downtime, self.max_downtime])
|
|
1313
1531
|
|
|
1314
1532
|
@property
|
|
1315
|
-
def
|
|
1316
|
-
"""Determines whether a variable for
|
|
1317
|
-
if self.
|
|
1533
|
+
def use_startup_tracking(self) -> bool:
|
|
1534
|
+
"""Determines whether a variable for startup is needed or not"""
|
|
1535
|
+
if self.force_startup_tracking:
|
|
1318
1536
|
return True
|
|
1319
1537
|
|
|
1320
1538
|
return any(
|
|
1321
|
-
param
|
|
1539
|
+
self._has_value(param)
|
|
1322
1540
|
for param in [
|
|
1323
|
-
self.
|
|
1324
|
-
self.
|
|
1541
|
+
self.effects_per_startup,
|
|
1542
|
+
self.startup_limit,
|
|
1325
1543
|
]
|
|
1326
1544
|
)
|