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/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 Dict, List, Optional, Tuple, Union
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: [linopy.Variable],
30
- relative_bounds_of_defining_variable: Tuple[NumericData, NumericData],
31
- label: Optional[str] = None,
32
- on_variable: Optional[linopy.Variable] = None,
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: Optional[Union[Scalar, linopy.Variable]] = None
36
- self.is_invested: Optional[linopy.Variable] = None
38
+ self.size: Scalar | linopy.Variable | None = None
39
+ self.is_invested: linopy.Variable | None = None
37
40
 
38
- self.piecewise_effects: Optional[PiecewiseEffectsModel] = None
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={effect: -self.is_invested * factor + factor for effect, factor in self.parameters.divest_effects.items()},
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: List[linopy.Variable],
206
- defining_bounds: List[Tuple[NumericData, NumericData]],
207
- previous_values: List[Optional[NumericData]] = None,
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: Optional[NumericData] = 0,
210
- on_hours_total_max: Optional[NumericData] = None,
211
- effects_per_running_hour: Dict[str, NumericData] = None,
212
- label: Optional[str] = None,
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: Optional[linopy.Variable] = None
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: List[NumericData], epsilon: float = 1e-5) -> np.ndarray:
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: Optional[Scalar] = None,
372
- label: Optional[str] = None,
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(self.switch_on + self.switch_off <= 1.1, name=f'{self.label_full}|switch_on_or_off'),
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: Optional[NumericData] = None,
455
- maximum_duration: Optional[NumericData] = None,
456
- previous_states: Optional[NumericData] = None,
457
- label: Optional[str] = None,
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: Union[int, float, np.ndarray]
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'({len(nr_of_indexes_with_consecutive_ones)}) is longer than the provided hours_per_timestep ({len(hours_per_timestep)}), '
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(binary_values[-nr_of_indexes_with_consecutive_ones:] * hours_per_timestep[-nr_of_indexes_with_consecutive_ones:])
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: List[linopy.Variable],
637
- defining_bounds: List[Tuple[NumericData, NumericData]],
638
- previous_values: List[Optional[NumericData]],
639
- label: Optional[str] = None,
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: Optional[linopy.Variable] = None
786
- self.lambda0: Optional[linopy.Variable] = None
787
- self.lambda1: Optional[linopy.Variable] = None
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: Dict[str, Piecewise],
835
- zero_point: Optional[Union[bool, linopy.Variable]],
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: List[PieceModel] = []
858
- self.zero_point: Optional[linopy.Variable] = None
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: Optional[str] = None,
922
- label: Optional[str] = None,
923
- label_full: Optional[str] = None,
924
- total_max: Optional[Scalar] = None,
925
- total_min: Optional[Scalar] = None,
926
- max_per_hour: Optional[NumericData] = None,
927
- min_per_hour: Optional[NumericData] = None,
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: Optional[linopy.Variable] = None
935
- self.total: Optional[linopy.Variable] = None
936
- self.shares: Dict[str, linopy.Variable] = {}
937
- self.share_constraints: Dict[str, linopy.Constraint] = {}
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: Optional[linopy.Constraint] = None
940
- self._eq_total: Optional[linopy.Constraint] = None
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 total_min is not None else np.inf
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: Tuple[str, Piecewise],
1028
- piecewise_shares: Dict[str, Piecewise],
1029
- zero_point: Optional[Union[bool, linopy.Variable]],
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: Dict[str, linopy.Variable] = {}
1046
+ self.shares: dict[str, linopy.Variable] = {}
1040
1047
 
1041
- self.piecewise_model: Optional[PiecewiseModel] = None
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: List[linopy.Variable],
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, Dict, List, Literal, Optional, Tuple, Union
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, NumericDataTS, TimeSeries, TimeSeriesCollection, TimeSeriesData
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, get_compact_representation, get_str_representation
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: Optional[float] = None,
39
- hours_of_previous_timesteps: Optional[Union[int, float, np.ndarray]] = None,
55
+ hours_of_last_timestep: float | None = None,
56
+ hours_of_previous_timesteps: int | float | np.ndarray | None = None,
40
57
  ):
41
58
  """
42
- Args:
43
- timesteps: The timesteps of the model.
44
- hours_of_last_timestep: The duration of the last time step. Uses the last time interval if not specified
45
- hours_of_previous_timesteps: The duration of previous timesteps.
46
- If None, the first time increment of time_series is used.
47
- This is needed to calculate previous durations (for example consecutive_on_hours).
48
- If you use an array, take care that its long enough to cover all previous values!
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: Dict[str, Component] = {}
58
- self.buses: Dict[str, Bus] = {}
78
+ self.components: dict[str, Component] = {}
79
+ self.buses: dict[str, Bus] = {}
59
80
  self.effects: EffectCollection = EffectCollection()
60
- self.model: Optional[SystemModel] = None
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: Dict) -> 'FlowSystem':
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: Union[str, pathlib.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: Union[str, pathlib.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') -> Dict:
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: Union[str, pathlib.Path], compression: int = 0, constants_in_dataset: bool = True):
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: Union[bool, str, pathlib.Path] = 'flow_system.html',
208
- controls: Union[
209
- bool,
210
- List[
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
- ) -> Optional['pyvis.network.Network']:
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
- - Optional[pyvis.network.Network]: The `Network` instance representing the visualization, or `None` if `pyvis` is not installed.
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"Network visualization requires optional dependencies. "
261
- f"Install with: pip install flixopt[viz], flixopt[full] or pip install dash dash_cytoscape networkx werkzeug. "
262
- f"Original error: {VISUALIZATION_ERROR}"
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[viz]. '
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) -> Tuple[Dict[str, Dict[str, str]], Dict[str, Dict[str, str]]]:
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: Optional[Union[NumericData, TimeSeriesData, TimeSeries]],
352
+ data: NumericData | TimeSeriesData | TimeSeries | None,
331
353
  needs_extra_timestep: bool = False,
332
- ) -> Optional[TimeSeries]:
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: Optional[str],
377
+ label_prefix: str | None,
356
378
  effect_values: EffectValuesUser,
357
- label_suffix: Optional[str] = None,
358
- ) -> Optional[EffectTimeSeries]:
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
- effect_values: Optional[EffectValuesDict] = self.effects.create_effect_values_dict(effect_values)
368
- if effect_values is None:
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 effect_values.items()
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
- UserWarning,
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) -> Dict[str, Flow]:
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) -> Dict[str, Element]:
483
+ def all_elements(self) -> dict[str, Element]:
462
484
  return {**self.components, **self.effects.effects, **self.flows, **self.buses}