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.
- docs/examples/00-Minimal Example.md +1 -1
- docs/examples/01-Basic Example.md +1 -1
- docs/examples/02-Complex Example.md +1 -1
- docs/examples/index.md +1 -1
- docs/faq/contribute.md +26 -14
- docs/faq/index.md +1 -1
- docs/javascripts/mathjax.js +1 -1
- docs/user-guide/Mathematical Notation/Bus.md +1 -1
- docs/user-guide/Mathematical Notation/Effects, Penalty & Objective.md +21 -21
- docs/user-guide/Mathematical Notation/Flow.md +3 -3
- docs/user-guide/Mathematical Notation/InvestParameters.md +3 -0
- docs/user-guide/Mathematical Notation/LinearConverter.md +5 -5
- docs/user-guide/Mathematical Notation/OnOffParameters.md +3 -0
- docs/user-guide/Mathematical Notation/Piecewise.md +1 -1
- docs/user-guide/Mathematical Notation/Storage.md +2 -2
- docs/user-guide/Mathematical Notation/index.md +1 -1
- docs/user-guide/Mathematical Notation/others.md +1 -1
- docs/user-guide/index.md +2 -2
- flixopt/__init__.py +4 -0
- flixopt/aggregation.py +33 -32
- flixopt/calculation.py +161 -65
- flixopt/components.py +687 -154
- flixopt/config.py +17 -8
- flixopt/core.py +69 -60
- flixopt/effects.py +146 -64
- flixopt/elements.py +297 -110
- flixopt/features.py +78 -71
- flixopt/flow_system.py +72 -50
- flixopt/interface.py +952 -113
- flixopt/io.py +15 -10
- flixopt/linear_converters.py +373 -81
- flixopt/network_app.py +445 -266
- flixopt/plotting.py +215 -87
- flixopt/results.py +382 -209
- flixopt/solvers.py +25 -21
- flixopt/structure.py +41 -39
- flixopt/utils.py +10 -7
- {flixopt-2.1.6.dist-info → flixopt-2.1.8.dist-info}/METADATA +64 -53
- flixopt-2.1.8.dist-info/RECORD +56 -0
- scripts/extract_release_notes.py +5 -5
- scripts/gen_ref_pages.py +1 -1
- flixopt-2.1.6.dist-info/RECORD +0 -54
- {flixopt-2.1.6.dist-info → flixopt-2.1.8.dist-info}/WHEEL +0 -0
- {flixopt-2.1.6.dist-info → flixopt-2.1.8.dist-info}/licenses/LICENSE +0 -0
- {flixopt-2.1.6.dist-info → flixopt-2.1.8.dist-info}/top_level.txt +0 -0
flixopt/features.py
CHANGED
|
@@ -3,18 +3,21 @@ This module contains the features of the flixopt framework.
|
|
|
3
3
|
Features extend the functionality of Elements.
|
|
4
4
|
"""
|
|
5
5
|
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
6
8
|
import logging
|
|
7
|
-
from typing import
|
|
9
|
+
from typing import TYPE_CHECKING
|
|
8
10
|
|
|
9
11
|
import linopy
|
|
10
12
|
import numpy as np
|
|
11
13
|
|
|
12
|
-
from . import utils
|
|
13
14
|
from .config import CONFIG
|
|
14
15
|
from .core import NumericData, Scalar, TimeSeries
|
|
15
|
-
from .interface import InvestParameters, OnOffParameters, Piecewise
|
|
16
16
|
from .structure import Model, SystemModel
|
|
17
17
|
|
|
18
|
+
if TYPE_CHECKING:
|
|
19
|
+
from .interface import InvestParameters, OnOffParameters, Piecewise
|
|
20
|
+
|
|
18
21
|
logger = logging.getLogger('flixopt')
|
|
19
22
|
|
|
20
23
|
|
|
@@ -26,16 +29,16 @@ class InvestmentModel(Model):
|
|
|
26
29
|
model: SystemModel,
|
|
27
30
|
label_of_element: str,
|
|
28
31
|
parameters: InvestParameters,
|
|
29
|
-
defining_variable:
|
|
30
|
-
relative_bounds_of_defining_variable:
|
|
31
|
-
label:
|
|
32
|
-
on_variable:
|
|
32
|
+
defining_variable: linopy.Variable,
|
|
33
|
+
relative_bounds_of_defining_variable: tuple[NumericData, NumericData],
|
|
34
|
+
label: str | None = None,
|
|
35
|
+
on_variable: linopy.Variable | None = None,
|
|
33
36
|
):
|
|
34
37
|
super().__init__(model, label_of_element, label)
|
|
35
|
-
self.size:
|
|
36
|
-
self.is_invested:
|
|
38
|
+
self.size: Scalar | linopy.Variable | None = None
|
|
39
|
+
self.is_invested: linopy.Variable | None = None
|
|
37
40
|
|
|
38
|
-
self.piecewise_effects:
|
|
41
|
+
self.piecewise_effects: PiecewiseEffectsModel | None = None
|
|
39
42
|
|
|
40
43
|
self._on_variable = on_variable
|
|
41
44
|
self._defining_variable = defining_variable
|
|
@@ -90,7 +93,10 @@ class InvestmentModel(Model):
|
|
|
90
93
|
# share: divest_effects - isInvested * divest_effects
|
|
91
94
|
self._model.effects.add_share_to_effects(
|
|
92
95
|
name=self.label_of_element,
|
|
93
|
-
expressions={
|
|
96
|
+
expressions={
|
|
97
|
+
effect: -self.is_invested * factor + factor
|
|
98
|
+
for effect, factor in self.parameters.divest_effects.items()
|
|
99
|
+
},
|
|
94
100
|
target='invest',
|
|
95
101
|
)
|
|
96
102
|
|
|
@@ -202,14 +208,14 @@ class StateModel(Model):
|
|
|
202
208
|
self,
|
|
203
209
|
model: SystemModel,
|
|
204
210
|
label_of_element: str,
|
|
205
|
-
defining_variables:
|
|
206
|
-
defining_bounds:
|
|
207
|
-
previous_values:
|
|
211
|
+
defining_variables: list[linopy.Variable],
|
|
212
|
+
defining_bounds: list[tuple[NumericData, NumericData]],
|
|
213
|
+
previous_values: list[NumericData | None] | None = None,
|
|
208
214
|
use_off: bool = True,
|
|
209
|
-
on_hours_total_min:
|
|
210
|
-
on_hours_total_max:
|
|
211
|
-
effects_per_running_hour:
|
|
212
|
-
label:
|
|
215
|
+
on_hours_total_min: NumericData | None = 0,
|
|
216
|
+
on_hours_total_max: NumericData | None = None,
|
|
217
|
+
effects_per_running_hour: dict[str, NumericData] | None = None,
|
|
218
|
+
label: str | None = None,
|
|
213
219
|
):
|
|
214
220
|
"""
|
|
215
221
|
Models binary state variables based on a continous variable.
|
|
@@ -237,7 +243,7 @@ class StateModel(Model):
|
|
|
237
243
|
self._effects_per_running_hour = effects_per_running_hour or {}
|
|
238
244
|
|
|
239
245
|
self.on = None
|
|
240
|
-
self.total_on_hours:
|
|
246
|
+
self.total_on_hours: linopy.Variable | None = None
|
|
241
247
|
self.off = None
|
|
242
248
|
|
|
243
249
|
def do_modeling(self):
|
|
@@ -304,13 +310,11 @@ class StateModel(Model):
|
|
|
304
310
|
)
|
|
305
311
|
|
|
306
312
|
# Constraint: on * upper_bound >= def_var
|
|
307
|
-
self.add(
|
|
308
|
-
self._model.add_constraints(self.on * ub >= def_var, name=f'{self.label_full}|on_con2'), 'on_con2'
|
|
309
|
-
)
|
|
313
|
+
self.add(self._model.add_constraints(self.on * ub >= def_var, name=f'{self.label_full}|on_con2'), 'on_con2')
|
|
310
314
|
else:
|
|
311
315
|
# Case for multiple defining variables
|
|
312
316
|
ub = sum(bound[1] for bound in self._defining_bounds) / nr_of_def_vars
|
|
313
|
-
lb = CONFIG.modeling.EPSILON #TODO: Can this be a bigger value? (maybe the smallest bound?)
|
|
317
|
+
lb = CONFIG.modeling.EPSILON # TODO: Can this be a bigger value? (maybe the smallest bound?)
|
|
314
318
|
|
|
315
319
|
# Constraint: on * epsilon <= sum(all_defining_variables)
|
|
316
320
|
self.add(
|
|
@@ -344,7 +348,7 @@ class StateModel(Model):
|
|
|
344
348
|
return 1 - self.previous_states
|
|
345
349
|
|
|
346
350
|
@staticmethod
|
|
347
|
-
def compute_previous_states(previous_values:
|
|
351
|
+
def compute_previous_states(previous_values: list[NumericData | None] | None, epsilon: float = 1e-5) -> np.ndarray:
|
|
348
352
|
"""Computes the previous states {0, 1} of defining variables as a binary array from their previous values."""
|
|
349
353
|
if not previous_values or all([val is None for val in previous_values]):
|
|
350
354
|
return np.array([0])
|
|
@@ -368,8 +372,8 @@ class SwitchStateModel(Model):
|
|
|
368
372
|
label_of_element: str,
|
|
369
373
|
state_variable: linopy.Variable,
|
|
370
374
|
previous_state=0,
|
|
371
|
-
switch_on_max:
|
|
372
|
-
label:
|
|
375
|
+
switch_on_max: Scalar | None = None,
|
|
376
|
+
label: str | None = None,
|
|
373
377
|
):
|
|
374
378
|
super().__init__(model, label_of_element, label)
|
|
375
379
|
self._state_variable = state_variable
|
|
@@ -426,7 +430,9 @@ class SwitchStateModel(Model):
|
|
|
426
430
|
|
|
427
431
|
# Mutual exclusivity constraint
|
|
428
432
|
self.add(
|
|
429
|
-
self._model.add_constraints(
|
|
433
|
+
self._model.add_constraints(
|
|
434
|
+
self.switch_on + self.switch_off <= 1.1, name=f'{self.label_full}|switch_on_or_off'
|
|
435
|
+
),
|
|
430
436
|
'switch_on_or_off',
|
|
431
437
|
)
|
|
432
438
|
|
|
@@ -451,10 +457,10 @@ class ConsecutiveStateModel(Model):
|
|
|
451
457
|
model: SystemModel,
|
|
452
458
|
label_of_element: str,
|
|
453
459
|
state_variable: linopy.Variable,
|
|
454
|
-
minimum_duration:
|
|
455
|
-
maximum_duration:
|
|
456
|
-
previous_states:
|
|
457
|
-
label:
|
|
460
|
+
minimum_duration: NumericData | None = None,
|
|
461
|
+
maximum_duration: NumericData | None = None,
|
|
462
|
+
previous_states: NumericData | None = None,
|
|
463
|
+
label: str | None = None,
|
|
458
464
|
):
|
|
459
465
|
"""
|
|
460
466
|
Model and constraint the consecutive duration of a state variable.
|
|
@@ -502,9 +508,7 @@ class ConsecutiveStateModel(Model):
|
|
|
502
508
|
|
|
503
509
|
# Upper bound constraint
|
|
504
510
|
self.add(
|
|
505
|
-
self._model.add_constraints(
|
|
506
|
-
self.duration <= self._state_variable * mega, name=f'{self.label_full}|con1'
|
|
507
|
-
),
|
|
511
|
+
self._model.add_constraints(self.duration <= self._state_variable * mega, name=f'{self.label_full}|con1'),
|
|
508
512
|
'con1',
|
|
509
513
|
)
|
|
510
514
|
|
|
@@ -556,8 +560,8 @@ class ConsecutiveStateModel(Model):
|
|
|
556
560
|
# Set initial value
|
|
557
561
|
self.add(
|
|
558
562
|
self._model.add_constraints(
|
|
559
|
-
self.duration.isel(time=0)
|
|
560
|
-
(hours_per_step.isel(time=0) + self.previous_duration) * self._state_variable.isel(time=0),
|
|
563
|
+
self.duration.isel(time=0)
|
|
564
|
+
== (hours_per_step.isel(time=0) + self.previous_duration) * self._state_variable.isel(time=0),
|
|
561
565
|
name=f'{self.label_full}|initial',
|
|
562
566
|
),
|
|
563
567
|
'initial',
|
|
@@ -568,14 +572,14 @@ class ConsecutiveStateModel(Model):
|
|
|
568
572
|
@property
|
|
569
573
|
def previous_duration(self) -> Scalar:
|
|
570
574
|
"""Computes the previous duration of the state variable"""
|
|
571
|
-
#TODO: Allow for other/dynamic timestep resolutions
|
|
575
|
+
# TODO: Allow for other/dynamic timestep resolutions
|
|
572
576
|
return ConsecutiveStateModel.compute_consecutive_hours_in_state(
|
|
573
577
|
self._previous_states, self._model.hours_per_step.isel(time=0).item()
|
|
574
578
|
)
|
|
575
579
|
|
|
576
580
|
@staticmethod
|
|
577
581
|
def compute_consecutive_hours_in_state(
|
|
578
|
-
binary_values: NumericData, hours_per_timestep:
|
|
582
|
+
binary_values: NumericData, hours_per_timestep: int | float | np.ndarray
|
|
579
583
|
) -> Scalar:
|
|
580
584
|
"""
|
|
581
585
|
Computes the final consecutive duration in state 'on' (=1) in hours, from a binary array.
|
|
@@ -615,11 +619,14 @@ class ConsecutiveStateModel(Model):
|
|
|
615
619
|
if len(hours_per_timestep) < nr_of_indexes_with_consecutive_ones:
|
|
616
620
|
raise ValueError(
|
|
617
621
|
f'When trying to calculate the consecutive duration, the length of the last duration '
|
|
618
|
-
f'({
|
|
622
|
+
f'({nr_of_indexes_with_consecutive_ones}) is longer than the provided hours_per_timestep ({len(hours_per_timestep)}), '
|
|
619
623
|
f'as {binary_values=}'
|
|
620
624
|
)
|
|
621
625
|
|
|
622
|
-
return np.sum(
|
|
626
|
+
return np.sum(
|
|
627
|
+
binary_values[-nr_of_indexes_with_consecutive_ones:]
|
|
628
|
+
* hours_per_timestep[-nr_of_indexes_with_consecutive_ones:]
|
|
629
|
+
)
|
|
623
630
|
|
|
624
631
|
|
|
625
632
|
class OnOffModel(Model):
|
|
@@ -633,10 +640,10 @@ class OnOffModel(Model):
|
|
|
633
640
|
model: SystemModel,
|
|
634
641
|
on_off_parameters: OnOffParameters,
|
|
635
642
|
label_of_element: str,
|
|
636
|
-
defining_variables:
|
|
637
|
-
defining_bounds:
|
|
638
|
-
previous_values:
|
|
639
|
-
label:
|
|
643
|
+
defining_variables: list[linopy.Variable],
|
|
644
|
+
defining_bounds: list[tuple[NumericData, NumericData]],
|
|
645
|
+
previous_values: list[NumericData | None],
|
|
646
|
+
label: str | None = None,
|
|
640
647
|
):
|
|
641
648
|
"""
|
|
642
649
|
Constructor for OnOffModel
|
|
@@ -782,9 +789,9 @@ class PieceModel(Model):
|
|
|
782
789
|
as_time_series: bool = True,
|
|
783
790
|
):
|
|
784
791
|
super().__init__(model, label_of_element, label)
|
|
785
|
-
self.inside_piece:
|
|
786
|
-
self.lambda0:
|
|
787
|
-
self.lambda1:
|
|
792
|
+
self.inside_piece: linopy.Variable | None = None
|
|
793
|
+
self.lambda0: linopy.Variable | None = None
|
|
794
|
+
self.lambda1: linopy.Variable | None = None
|
|
788
795
|
self._as_time_series = as_time_series
|
|
789
796
|
|
|
790
797
|
def do_modeling(self):
|
|
@@ -831,8 +838,8 @@ class PiecewiseModel(Model):
|
|
|
831
838
|
self,
|
|
832
839
|
model: SystemModel,
|
|
833
840
|
label_of_element: str,
|
|
834
|
-
piecewise_variables:
|
|
835
|
-
zero_point:
|
|
841
|
+
piecewise_variables: dict[str, Piecewise],
|
|
842
|
+
zero_point: bool | linopy.Variable | None,
|
|
836
843
|
as_time_series: bool,
|
|
837
844
|
label: str = '',
|
|
838
845
|
):
|
|
@@ -854,8 +861,8 @@ class PiecewiseModel(Model):
|
|
|
854
861
|
self._zero_point = zero_point
|
|
855
862
|
self._as_time_series = as_time_series
|
|
856
863
|
|
|
857
|
-
self.pieces:
|
|
858
|
-
self.zero_point:
|
|
864
|
+
self.pieces: list[PieceModel] = []
|
|
865
|
+
self.zero_point: linopy.Variable | None = None
|
|
859
866
|
|
|
860
867
|
def do_modeling(self):
|
|
861
868
|
for i in range(len(list(self._piecewise_variables.values())[0])):
|
|
@@ -918,30 +925,30 @@ class ShareAllocationModel(Model):
|
|
|
918
925
|
self,
|
|
919
926
|
model: SystemModel,
|
|
920
927
|
shares_are_time_series: bool,
|
|
921
|
-
label_of_element:
|
|
922
|
-
label:
|
|
923
|
-
label_full:
|
|
924
|
-
total_max:
|
|
925
|
-
total_min:
|
|
926
|
-
max_per_hour:
|
|
927
|
-
min_per_hour:
|
|
928
|
+
label_of_element: str | None = None,
|
|
929
|
+
label: str | None = None,
|
|
930
|
+
label_full: str | None = None,
|
|
931
|
+
total_max: Scalar | None = None,
|
|
932
|
+
total_min: Scalar | None = None,
|
|
933
|
+
max_per_hour: NumericData | None = None,
|
|
934
|
+
min_per_hour: NumericData | None = None,
|
|
928
935
|
):
|
|
929
936
|
super().__init__(model, label_of_element=label_of_element, label=label, label_full=label_full)
|
|
930
937
|
if not shares_are_time_series: # If the condition is True
|
|
931
938
|
assert max_per_hour is None and min_per_hour is None, (
|
|
932
939
|
'Both max_per_hour and min_per_hour cannot be used when shares_are_time_series is False'
|
|
933
940
|
)
|
|
934
|
-
self.total_per_timestep:
|
|
935
|
-
self.total:
|
|
936
|
-
self.shares:
|
|
937
|
-
self.share_constraints:
|
|
941
|
+
self.total_per_timestep: linopy.Variable | None = None
|
|
942
|
+
self.total: linopy.Variable | None = None
|
|
943
|
+
self.shares: dict[str, linopy.Variable] = {}
|
|
944
|
+
self.share_constraints: dict[str, linopy.Constraint] = {}
|
|
938
945
|
|
|
939
|
-
self._eq_total_per_timestep:
|
|
940
|
-
self._eq_total:
|
|
946
|
+
self._eq_total_per_timestep: linopy.Constraint | None = None
|
|
947
|
+
self._eq_total: linopy.Constraint | None = None
|
|
941
948
|
|
|
942
949
|
# Parameters
|
|
943
950
|
self._shares_are_time_series = shares_are_time_series
|
|
944
|
-
self._total_max = total_max if
|
|
951
|
+
self._total_max = total_max if total_max is not None else np.inf
|
|
945
952
|
self._total_min = total_min if total_min is not None else -np.inf
|
|
946
953
|
self._max_per_hour = max_per_hour if max_per_hour is not None else np.inf
|
|
947
954
|
self._min_per_hour = min_per_hour if min_per_hour is not None else -np.inf
|
|
@@ -1024,9 +1031,9 @@ class PiecewiseEffectsModel(Model):
|
|
|
1024
1031
|
self,
|
|
1025
1032
|
model: SystemModel,
|
|
1026
1033
|
label_of_element: str,
|
|
1027
|
-
piecewise_origin:
|
|
1028
|
-
piecewise_shares:
|
|
1029
|
-
zero_point:
|
|
1034
|
+
piecewise_origin: tuple[str, Piecewise],
|
|
1035
|
+
piecewise_shares: dict[str, Piecewise],
|
|
1036
|
+
zero_point: bool | linopy.Variable | None,
|
|
1030
1037
|
label: str = 'PiecewiseEffects',
|
|
1031
1038
|
):
|
|
1032
1039
|
super().__init__(model, label_of_element, label)
|
|
@@ -1036,9 +1043,9 @@ class PiecewiseEffectsModel(Model):
|
|
|
1036
1043
|
self._zero_point = zero_point
|
|
1037
1044
|
self._piecewise_origin = piecewise_origin
|
|
1038
1045
|
self._piecewise_shares = piecewise_shares
|
|
1039
|
-
self.shares:
|
|
1046
|
+
self.shares: dict[str, linopy.Variable] = {}
|
|
1040
1047
|
|
|
1041
|
-
self.piecewise_model:
|
|
1048
|
+
self.piecewise_model: PiecewiseModel | None = None
|
|
1042
1049
|
|
|
1043
1050
|
def do_modeling(self):
|
|
1044
1051
|
self.shares = {
|
|
@@ -1096,7 +1103,7 @@ class PreventSimultaneousUsageModel(Model):
|
|
|
1096
1103
|
def __init__(
|
|
1097
1104
|
self,
|
|
1098
1105
|
model: SystemModel,
|
|
1099
|
-
variables:
|
|
1106
|
+
variables: list[linopy.Variable],
|
|
1100
1107
|
label_of_element: str,
|
|
1101
1108
|
label: str = 'PreventSimultaneousUsage',
|
|
1102
1109
|
):
|
flixopt/flow_system.py
CHANGED
|
@@ -2,50 +2,71 @@
|
|
|
2
2
|
This module contains the FlowSystem class, which is used to collect instances of many other classes by the end User.
|
|
3
3
|
"""
|
|
4
4
|
|
|
5
|
+
from __future__ import annotations
|
|
6
|
+
|
|
5
7
|
import json
|
|
6
8
|
import logging
|
|
7
|
-
import pathlib
|
|
8
9
|
import warnings
|
|
9
10
|
from io import StringIO
|
|
10
|
-
from typing import TYPE_CHECKING,
|
|
11
|
+
from typing import TYPE_CHECKING, Literal
|
|
11
12
|
|
|
12
|
-
import numpy as np
|
|
13
13
|
import pandas as pd
|
|
14
|
-
import xarray as xr
|
|
15
14
|
from rich.console import Console
|
|
16
15
|
from rich.pretty import Pretty
|
|
17
16
|
|
|
18
17
|
from . import io as fx_io
|
|
19
|
-
from .core import NumericData,
|
|
18
|
+
from .core import NumericData, TimeSeries, TimeSeriesCollection, TimeSeriesData
|
|
20
19
|
from .effects import Effect, EffectCollection, EffectTimeSeries, EffectValuesDict, EffectValuesUser
|
|
21
20
|
from .elements import Bus, Component, Flow
|
|
22
|
-
from .structure import CLASS_REGISTRY, Element, SystemModel
|
|
21
|
+
from .structure import CLASS_REGISTRY, Element, SystemModel
|
|
23
22
|
|
|
24
23
|
if TYPE_CHECKING:
|
|
24
|
+
import pathlib
|
|
25
|
+
|
|
26
|
+
import numpy as np
|
|
25
27
|
import pyvis
|
|
28
|
+
import xarray as xr
|
|
26
29
|
|
|
27
30
|
logger = logging.getLogger('flixopt')
|
|
28
31
|
|
|
29
32
|
|
|
30
33
|
class FlowSystem:
|
|
31
34
|
"""
|
|
32
|
-
A FlowSystem organizes the high level Elements (Components & Effects).
|
|
35
|
+
A FlowSystem organizes the high level Elements (Components, Buses & Effects).
|
|
36
|
+
|
|
37
|
+
This is the main container class that users work with to build and manage their System.
|
|
38
|
+
|
|
39
|
+
Args:
|
|
40
|
+
timesteps: The timesteps of the model.
|
|
41
|
+
hours_of_last_timestep: The duration of the last time step. Uses the last time interval if not specified
|
|
42
|
+
hours_of_previous_timesteps: The duration of previous timesteps.
|
|
43
|
+
If None, the first time increment of time_series is used.
|
|
44
|
+
This is needed to calculate previous durations (for example consecutive_on_hours).
|
|
45
|
+
If you use an array, take care that its long enough to cover all previous values!
|
|
46
|
+
|
|
47
|
+
Notes:
|
|
48
|
+
- Creates an empty registry for components and buses, an empty EffectCollection, and a placeholder for a SystemModel.
|
|
49
|
+
- The instance starts disconnected (self._connected == False) and will be connected automatically when trying to solve a calculation.
|
|
33
50
|
"""
|
|
34
51
|
|
|
35
52
|
def __init__(
|
|
36
53
|
self,
|
|
37
54
|
timesteps: pd.DatetimeIndex,
|
|
38
|
-
hours_of_last_timestep:
|
|
39
|
-
hours_of_previous_timesteps:
|
|
55
|
+
hours_of_last_timestep: float | None = None,
|
|
56
|
+
hours_of_previous_timesteps: int | float | np.ndarray | None = None,
|
|
40
57
|
):
|
|
41
58
|
"""
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
59
|
+
Initialize a FlowSystem that manages components, buses, effects, and their time-series.
|
|
60
|
+
|
|
61
|
+
Parameters:
|
|
62
|
+
timesteps: DatetimeIndex defining the primary timesteps for the system's TimeSeriesCollection.
|
|
63
|
+
hours_of_last_timestep: Duration (in hours) of the final timestep; if None, inferred from timesteps or defaults in TimeSeriesCollection.
|
|
64
|
+
hours_of_previous_timesteps: Scalar or array-like durations (in hours) for the preceding timesteps; used to configure non-uniform timestep lengths.
|
|
65
|
+
|
|
66
|
+
Notes:
|
|
67
|
+
Creates an empty registry for components and buses, an empty EffectCollection, and a placeholder for a SystemModel.
|
|
68
|
+
The instance starts disconnected (self._connected == False) and with no active network visualization app.
|
|
69
|
+
This can also be triggered manually with `_connect_network()`.
|
|
49
70
|
"""
|
|
50
71
|
self.time_series_collection = TimeSeriesCollection(
|
|
51
72
|
timesteps=timesteps,
|
|
@@ -54,10 +75,10 @@ class FlowSystem:
|
|
|
54
75
|
)
|
|
55
76
|
|
|
56
77
|
# defaults:
|
|
57
|
-
self.components:
|
|
58
|
-
self.buses:
|
|
78
|
+
self.components: dict[str, Component] = {}
|
|
79
|
+
self.buses: dict[str, Bus] = {}
|
|
59
80
|
self.effects: EffectCollection = EffectCollection()
|
|
60
|
-
self.model:
|
|
81
|
+
self.model: SystemModel | None = None
|
|
61
82
|
|
|
62
83
|
self._connected = False
|
|
63
84
|
|
|
@@ -83,7 +104,7 @@ class FlowSystem:
|
|
|
83
104
|
return flow_system
|
|
84
105
|
|
|
85
106
|
@classmethod
|
|
86
|
-
def from_dict(cls, data:
|
|
107
|
+
def from_dict(cls, data: dict) -> FlowSystem:
|
|
87
108
|
"""
|
|
88
109
|
Load a FlowSystem from a dictionary.
|
|
89
110
|
|
|
@@ -112,7 +133,7 @@ class FlowSystem:
|
|
|
112
133
|
return flow_system
|
|
113
134
|
|
|
114
135
|
@classmethod
|
|
115
|
-
def from_netcdf(cls, path:
|
|
136
|
+
def from_netcdf(cls, path: str | pathlib.Path):
|
|
116
137
|
"""
|
|
117
138
|
Load a FlowSystem from a netcdf file
|
|
118
139
|
"""
|
|
@@ -144,7 +165,7 @@ class FlowSystem:
|
|
|
144
165
|
f'Tried to add incompatible object to FlowSystem: {type(new_element)=}: {new_element=} '
|
|
145
166
|
)
|
|
146
167
|
|
|
147
|
-
def to_json(self, path:
|
|
168
|
+
def to_json(self, path: str | pathlib.Path):
|
|
148
169
|
"""
|
|
149
170
|
Saves the flow system to a json file.
|
|
150
171
|
This not meant to be reloaded and recreate the object,
|
|
@@ -156,7 +177,7 @@ class FlowSystem:
|
|
|
156
177
|
with open(path, 'w', encoding='utf-8') as f:
|
|
157
178
|
json.dump(self.as_dict('stats'), f, indent=4, ensure_ascii=False)
|
|
158
179
|
|
|
159
|
-
def as_dict(self, data_mode: Literal['data', 'name', 'stats'] = 'data') ->
|
|
180
|
+
def as_dict(self, data_mode: Literal['data', 'name', 'stats'] = 'data') -> dict:
|
|
160
181
|
"""Convert the object to a dictionary representation."""
|
|
161
182
|
data = {
|
|
162
183
|
'components': {
|
|
@@ -190,7 +211,7 @@ class FlowSystem:
|
|
|
190
211
|
ds.attrs = self.as_dict(data_mode='name')
|
|
191
212
|
return ds
|
|
192
213
|
|
|
193
|
-
def to_netcdf(self, path:
|
|
214
|
+
def to_netcdf(self, path: str | pathlib.Path, compression: int = 0, constants_in_dataset: bool = True):
|
|
194
215
|
"""
|
|
195
216
|
Saves the FlowSystem to a netCDF file.
|
|
196
217
|
Args:
|
|
@@ -204,15 +225,13 @@ class FlowSystem:
|
|
|
204
225
|
|
|
205
226
|
def plot_network(
|
|
206
227
|
self,
|
|
207
|
-
path:
|
|
208
|
-
controls:
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
Literal['nodes', 'edges', 'layout', 'interaction', 'manipulation', 'physics', 'selection', 'renderer']
|
|
212
|
-
],
|
|
228
|
+
path: bool | str | pathlib.Path = 'flow_system.html',
|
|
229
|
+
controls: bool
|
|
230
|
+
| list[
|
|
231
|
+
Literal['nodes', 'edges', 'layout', 'interaction', 'manipulation', 'physics', 'selection', 'renderer']
|
|
213
232
|
] = True,
|
|
214
233
|
show: bool = False,
|
|
215
|
-
) ->
|
|
234
|
+
) -> pyvis.network.Network | None:
|
|
216
235
|
"""
|
|
217
236
|
Visualizes the network structure of a FlowSystem using PyVis, saving it as an interactive HTML file.
|
|
218
237
|
|
|
@@ -227,7 +246,7 @@ class FlowSystem:
|
|
|
227
246
|
show: Whether to open the visualization in the web browser.
|
|
228
247
|
|
|
229
248
|
Returns:
|
|
230
|
-
-
|
|
249
|
+
- 'pyvis.network.Network' | None: The `Network` instance representing the visualization, or `None` if `pyvis` is not installed.
|
|
231
250
|
|
|
232
251
|
Examples:
|
|
233
252
|
>>> flow_system.plot_network()
|
|
@@ -245,7 +264,7 @@ class FlowSystem:
|
|
|
245
264
|
|
|
246
265
|
def start_network_app(self):
|
|
247
266
|
"""Visualizes the network structure of a FlowSystem using Dash, Cytoscape, and networkx.
|
|
248
|
-
Requires optional dependencies: dash, dash-cytoscape, networkx, werkzeug.
|
|
267
|
+
Requires optional dependencies: dash, dash-cytoscape, dash-daq, networkx, flask, werkzeug.
|
|
249
268
|
"""
|
|
250
269
|
from .network_app import DASH_CYTOSCAPE_AVAILABLE, VISUALIZATION_ERROR, flow_graph, shownetwork
|
|
251
270
|
|
|
@@ -257,9 +276,10 @@ class FlowSystem:
|
|
|
257
276
|
|
|
258
277
|
if not DASH_CYTOSCAPE_AVAILABLE:
|
|
259
278
|
raise ImportError(
|
|
260
|
-
f
|
|
261
|
-
f
|
|
262
|
-
f
|
|
279
|
+
f'Network visualization requires optional dependencies. '
|
|
280
|
+
f'Install with: `pip install flixopt[network_viz]`, `pip install flixopt[full]` '
|
|
281
|
+
f'or: `pip install dash dash-cytoscape dash-daq networkx werkzeug`. '
|
|
282
|
+
f'Original error: {VISUALIZATION_ERROR}'
|
|
263
283
|
)
|
|
264
284
|
|
|
265
285
|
if not self._connected:
|
|
@@ -274,10 +294,12 @@ class FlowSystem:
|
|
|
274
294
|
def stop_network_app(self):
|
|
275
295
|
"""Stop the network visualization server."""
|
|
276
296
|
from .network_app import DASH_CYTOSCAPE_AVAILABLE, VISUALIZATION_ERROR
|
|
297
|
+
|
|
277
298
|
if not DASH_CYTOSCAPE_AVAILABLE:
|
|
278
299
|
raise ImportError(
|
|
279
300
|
f'Network visualization requires optional dependencies. '
|
|
280
|
-
f'Install with: pip install flixopt[
|
|
301
|
+
f'Install with: `pip install flixopt[network_viz]`, `pip install flixopt[full]` '
|
|
302
|
+
f'or: `pip install dash dash-cytoscape dash-daq networkx werkzeug`. '
|
|
281
303
|
f'Original error: {VISUALIZATION_ERROR}'
|
|
282
304
|
)
|
|
283
305
|
|
|
@@ -294,7 +316,7 @@ class FlowSystem:
|
|
|
294
316
|
finally:
|
|
295
317
|
self._network_app = None
|
|
296
318
|
|
|
297
|
-
def network_infos(self) ->
|
|
319
|
+
def network_infos(self) -> tuple[dict[str, dict[str, str]], dict[str, dict[str, str]]]:
|
|
298
320
|
if not self._connected:
|
|
299
321
|
self._connect_network()
|
|
300
322
|
nodes = {
|
|
@@ -327,9 +349,9 @@ class FlowSystem:
|
|
|
327
349
|
def create_time_series(
|
|
328
350
|
self,
|
|
329
351
|
name: str,
|
|
330
|
-
data:
|
|
352
|
+
data: NumericData | TimeSeriesData | TimeSeries | None,
|
|
331
353
|
needs_extra_timestep: bool = False,
|
|
332
|
-
) ->
|
|
354
|
+
) -> TimeSeries | None:
|
|
333
355
|
"""
|
|
334
356
|
Tries to create a TimeSeries from NumericData Data and adds it to the time_series_collection
|
|
335
357
|
If the data already is a TimeSeries, nothing happens and the TimeSeries gets reset and returned
|
|
@@ -352,10 +374,10 @@ class FlowSystem:
|
|
|
352
374
|
|
|
353
375
|
def create_effect_time_series(
|
|
354
376
|
self,
|
|
355
|
-
label_prefix:
|
|
377
|
+
label_prefix: str | None,
|
|
356
378
|
effect_values: EffectValuesUser,
|
|
357
|
-
label_suffix:
|
|
358
|
-
) ->
|
|
379
|
+
label_suffix: str | None = None,
|
|
380
|
+
) -> EffectTimeSeries | None:
|
|
359
381
|
"""
|
|
360
382
|
Transform EffectValues to EffectTimeSeries.
|
|
361
383
|
Creates a TimeSeries for each key in the nested_values dictionary, using the value as the data.
|
|
@@ -364,13 +386,13 @@ class FlowSystem:
|
|
|
364
386
|
followed by the label of the Effect in the nested_values and the label_suffix.
|
|
365
387
|
If the key in the EffectValues is None, the alias 'Standard_Effect' is used
|
|
366
388
|
"""
|
|
367
|
-
|
|
368
|
-
if
|
|
389
|
+
effect_values_dict: EffectValuesDict | None = self.effects.create_effect_values_dict(effect_values)
|
|
390
|
+
if effect_values_dict is None:
|
|
369
391
|
return None
|
|
370
392
|
|
|
371
393
|
return {
|
|
372
394
|
effect: self.create_time_series('|'.join(filter(None, [label_prefix, effect, label_suffix])), value)
|
|
373
|
-
for effect, value in
|
|
395
|
+
for effect, value in effect_values_dict.items()
|
|
374
396
|
}
|
|
375
397
|
|
|
376
398
|
def create_model(self) -> SystemModel:
|
|
@@ -416,14 +438,14 @@ class FlowSystem:
|
|
|
416
438
|
|
|
417
439
|
# Add Bus if not already added (deprecated)
|
|
418
440
|
if flow._bus_object is not None and flow._bus_object not in self.buses.values():
|
|
419
|
-
self._add_buses(flow._bus_object)
|
|
420
441
|
warnings.warn(
|
|
421
442
|
f'The Bus {flow._bus_object.label} was added to the FlowSystem from {flow.label_full}.'
|
|
422
443
|
f'This is deprecated and will be removed in the future. '
|
|
423
444
|
f'Please pass the Bus.label to the Flow and the Bus to the FlowSystem instead.',
|
|
424
|
-
|
|
445
|
+
DeprecationWarning,
|
|
425
446
|
stacklevel=1,
|
|
426
447
|
)
|
|
448
|
+
self._add_buses(flow._bus_object)
|
|
427
449
|
|
|
428
450
|
# Connect Buses
|
|
429
451
|
bus = self.buses.get(flow.bus)
|
|
@@ -453,10 +475,10 @@ class FlowSystem:
|
|
|
453
475
|
return value
|
|
454
476
|
|
|
455
477
|
@property
|
|
456
|
-
def flows(self) ->
|
|
478
|
+
def flows(self) -> dict[str, Flow]:
|
|
457
479
|
set_of_flows = {flow for comp in self.components.values() for flow in comp.inputs + comp.outputs}
|
|
458
480
|
return {flow.label_full: flow for flow in set_of_flows}
|
|
459
481
|
|
|
460
482
|
@property
|
|
461
|
-
def all_elements(self) ->
|
|
483
|
+
def all_elements(self) -> dict[str, Element]:
|
|
462
484
|
return {**self.components, **self.effects.effects, **self.flows, **self.buses}
|