fram-core 0.0.0__py3-none-any.whl → 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (103) hide show
  1. fram_core-0.1.0.dist-info/METADATA +42 -0
  2. fram_core-0.1.0.dist-info/RECORD +100 -0
  3. {fram_core-0.0.0.dist-info → fram_core-0.1.0.dist-info}/WHEEL +1 -2
  4. fram_core-0.1.0.dist-info/licenses/LICENSE.md +8 -0
  5. framcore/Base.py +161 -0
  6. framcore/Model.py +90 -0
  7. framcore/__init__.py +10 -0
  8. framcore/aggregators/Aggregator.py +172 -0
  9. framcore/aggregators/HydroAggregator.py +849 -0
  10. framcore/aggregators/NodeAggregator.py +530 -0
  11. framcore/aggregators/WindSolarAggregator.py +315 -0
  12. framcore/aggregators/__init__.py +13 -0
  13. framcore/aggregators/_utils.py +184 -0
  14. framcore/attributes/Arrow.py +307 -0
  15. framcore/attributes/ElasticDemand.py +90 -0
  16. framcore/attributes/ReservoirCurve.py +23 -0
  17. framcore/attributes/SoftBound.py +16 -0
  18. framcore/attributes/StartUpCost.py +65 -0
  19. framcore/attributes/Storage.py +158 -0
  20. framcore/attributes/TargetBound.py +16 -0
  21. framcore/attributes/__init__.py +63 -0
  22. framcore/attributes/hydro/HydroBypass.py +49 -0
  23. framcore/attributes/hydro/HydroGenerator.py +100 -0
  24. framcore/attributes/hydro/HydroPump.py +178 -0
  25. framcore/attributes/hydro/HydroReservoir.py +27 -0
  26. framcore/attributes/hydro/__init__.py +13 -0
  27. framcore/attributes/level_profile_attributes.py +911 -0
  28. framcore/components/Component.py +136 -0
  29. framcore/components/Demand.py +144 -0
  30. framcore/components/Flow.py +189 -0
  31. framcore/components/HydroModule.py +371 -0
  32. framcore/components/Node.py +99 -0
  33. framcore/components/Thermal.py +208 -0
  34. framcore/components/Transmission.py +198 -0
  35. framcore/components/_PowerPlant.py +81 -0
  36. framcore/components/__init__.py +22 -0
  37. framcore/components/wind_solar.py +82 -0
  38. framcore/curves/Curve.py +44 -0
  39. framcore/curves/LoadedCurve.py +146 -0
  40. framcore/curves/__init__.py +9 -0
  41. framcore/events/__init__.py +21 -0
  42. framcore/events/events.py +51 -0
  43. framcore/expressions/Expr.py +591 -0
  44. framcore/expressions/__init__.py +30 -0
  45. framcore/expressions/_get_constant_from_expr.py +477 -0
  46. framcore/expressions/_utils.py +73 -0
  47. framcore/expressions/queries.py +416 -0
  48. framcore/expressions/units.py +227 -0
  49. framcore/fingerprints/__init__.py +11 -0
  50. framcore/fingerprints/fingerprint.py +292 -0
  51. framcore/juliamodels/JuliaModel.py +171 -0
  52. framcore/juliamodels/__init__.py +7 -0
  53. framcore/loaders/__init__.py +10 -0
  54. framcore/loaders/loaders.py +405 -0
  55. framcore/metadata/Div.py +73 -0
  56. framcore/metadata/ExprMeta.py +56 -0
  57. framcore/metadata/LevelExprMeta.py +32 -0
  58. framcore/metadata/Member.py +55 -0
  59. framcore/metadata/Meta.py +44 -0
  60. framcore/metadata/__init__.py +15 -0
  61. framcore/populators/Populator.py +108 -0
  62. framcore/populators/__init__.py +7 -0
  63. framcore/querydbs/CacheDB.py +50 -0
  64. framcore/querydbs/ModelDB.py +34 -0
  65. framcore/querydbs/QueryDB.py +45 -0
  66. framcore/querydbs/__init__.py +11 -0
  67. framcore/solvers/Solver.py +63 -0
  68. framcore/solvers/SolverConfig.py +272 -0
  69. framcore/solvers/__init__.py +9 -0
  70. framcore/timeindexes/AverageYearRange.py +27 -0
  71. framcore/timeindexes/ConstantTimeIndex.py +22 -0
  72. framcore/timeindexes/DailyIndex.py +33 -0
  73. framcore/timeindexes/FixedFrequencyTimeIndex.py +814 -0
  74. framcore/timeindexes/HourlyIndex.py +33 -0
  75. framcore/timeindexes/IsoCalendarDay.py +33 -0
  76. framcore/timeindexes/ListTimeIndex.py +277 -0
  77. framcore/timeindexes/ModelYear.py +23 -0
  78. framcore/timeindexes/ModelYears.py +27 -0
  79. framcore/timeindexes/OneYearProfileTimeIndex.py +29 -0
  80. framcore/timeindexes/ProfileTimeIndex.py +43 -0
  81. framcore/timeindexes/SinglePeriodTimeIndex.py +37 -0
  82. framcore/timeindexes/TimeIndex.py +103 -0
  83. framcore/timeindexes/WeeklyIndex.py +33 -0
  84. framcore/timeindexes/__init__.py +36 -0
  85. framcore/timeindexes/_time_vector_operations.py +689 -0
  86. framcore/timevectors/ConstantTimeVector.py +131 -0
  87. framcore/timevectors/LinearTransformTimeVector.py +131 -0
  88. framcore/timevectors/ListTimeVector.py +127 -0
  89. framcore/timevectors/LoadedTimeVector.py +97 -0
  90. framcore/timevectors/ReferencePeriod.py +51 -0
  91. framcore/timevectors/TimeVector.py +108 -0
  92. framcore/timevectors/__init__.py +17 -0
  93. framcore/utils/__init__.py +35 -0
  94. framcore/utils/get_regional_volumes.py +387 -0
  95. framcore/utils/get_supported_components.py +60 -0
  96. framcore/utils/global_energy_equivalent.py +63 -0
  97. framcore/utils/isolate_subnodes.py +172 -0
  98. framcore/utils/loaders.py +97 -0
  99. framcore/utils/node_flow_utils.py +236 -0
  100. framcore/utils/storage_subsystems.py +106 -0
  101. fram_core-0.0.0.dist-info/METADATA +0 -5
  102. fram_core-0.0.0.dist-info/RECORD +0 -4
  103. fram_core-0.0.0.dist-info/top_level.txt +0 -1
@@ -0,0 +1,136 @@
1
+ from __future__ import annotations
2
+
3
+ from abc import ABC, abstractmethod
4
+ from collections.abc import Iterable
5
+
6
+ from framcore import Base
7
+ from framcore.metadata import Meta
8
+
9
+
10
+ class Component(Base, ABC):
11
+ """
12
+ Components describe the main elements in the energy system. Can have additional Attributes and Metadata.
13
+
14
+ We have high-level and low-level Components. High-level Components, such as a HydroModule,
15
+ can be decomposed into low-level Components like Flows and Nodes. The high-level description lets
16
+ analysts work with recognizable domain objects, while the low-level descriptions enable generic algorithms
17
+ that minimize code duplication and simplify data manipulation.
18
+
19
+ Some energy market models like JulES, SpineOpt and PyPSA also have a generic description of the system,
20
+ so this two-tier system can be used to easier adapt the dataset to their required formats.
21
+
22
+ The method Component.get_simpler_components() is used to decompose high-level Components into low-level
23
+ Components. This can also be used together with the utility function get_supported_components() to transform
24
+ a set of Components into a set that only contains supported Component types.
25
+
26
+ Result attributes are initialized in the high-level Components. When they are transferred to low-level Components,
27
+ and the results are set by a model like JulES, the results will also appear in the high-level Components.
28
+
29
+ Nodes, Flows and Arrows are the main building blocks in FRAM's low-level representation of energy systems.
30
+ Node represent a point where a commodity can possibly be traded, stored or pass through.
31
+ Movement between Nodes is represented by Flows and Arrows. Flows represent a commodity flow,
32
+ and can have Arrows that each describe contribution of the Flow into a Node.
33
+ The Arrows have direction to determine input or output, and parameters for the contribution of the
34
+ Flow to the Node (conversion, efficiency and loss).
35
+ """
36
+
37
+ def __init__(self) -> None:
38
+ """Set mandatory private variables."""
39
+ self._parent: Component | None = None
40
+ self._meta: dict[str, Meta] = dict()
41
+
42
+ def add_meta(self, key: str, value: Meta) -> None:
43
+ """Add metadata to component. Overwrite if already exist."""
44
+ self._check_type(key, str)
45
+ self._check_type(value, Meta)
46
+ self._meta[key] = value
47
+
48
+ def get_meta(self, key: str) -> Meta | None:
49
+ """Get metadata from component or return None if not exist."""
50
+ self._check_type(key, str)
51
+ return self._meta.get(key, None)
52
+
53
+ def get_meta_keys(self) -> Iterable[str]:
54
+ """Get iterable with all metakeys in component."""
55
+ return self._meta.keys()
56
+
57
+ def get_simpler_components(
58
+ self,
59
+ base_name: str,
60
+ ) -> dict[str, Component]:
61
+ """
62
+ Return representation of self as dict of named simpler components.
63
+
64
+ The base_name should be unique within a model instance, and should
65
+ be used to prefix name of all simpler components.
66
+
67
+ Insert self as parent in each child.
68
+
69
+ Transfer metadata to each child.
70
+ """
71
+ self._check_type(base_name, str)
72
+ components = self._get_simpler_components(base_name)
73
+ assert base_name not in components, f"base_name: {base_name} should not be in \ncomponent: {self}"
74
+ components: dict[str, Component]
75
+ self._check_type(components, dict)
76
+ for name, c in components.items():
77
+ self._check_type(name, str)
78
+ self._check_type(c, Component)
79
+ self._check_component_not_self(c)
80
+ c: Component
81
+ c._parent = self # noqa: SLF001
82
+ for key in self.get_meta_keys():
83
+ value = self.get_meta(key)
84
+ for c in components.values():
85
+ c.add_meta(key, value)
86
+ return components
87
+
88
+ def get_parent(self) -> Component | None:
89
+ """Return parent if any, else None."""
90
+ self._check_type(self._parent, (Component, type(None)))
91
+ self._check_component_not_self(self._parent)
92
+ return self._parent
93
+
94
+ def get_parents(self) -> list[Component]:
95
+ """Return list of all parents, including self."""
96
+ child = self
97
+ parent = child.get_parent()
98
+ parents = [child]
99
+ while parent is not None:
100
+ child = parent
101
+ parent = child.get_parent()
102
+ parents.append(child)
103
+ self._check_unique_parents(parents)
104
+ return parents
105
+
106
+ def get_top_parent(self) -> Component:
107
+ """Return topmost parent. (May be object self)."""
108
+ parents = self.get_parents()
109
+ return parents[-1]
110
+
111
+ def replace_node(self, old: str, new: str) -> None:
112
+ """Replace old Node with new. Not error if no match."""
113
+ self._check_type(old, str)
114
+ self._check_type(new, str)
115
+ self._replace_node(old, new)
116
+
117
+ def _check_component_not_self(self, other: Component | None) -> None:
118
+ if not isinstance(other, Component):
119
+ return
120
+ if self != other:
121
+ return
122
+ message = f"Expected other component than {self}."
123
+ raise TypeError(message)
124
+
125
+ def _check_unique_parents(self, parents: list[Component]) -> None:
126
+ if len(parents) > len(set(parents)):
127
+ message = f"Parents for {self} are not unique."
128
+ raise TypeError(message)
129
+
130
+ @abstractmethod
131
+ def _replace_node(self, old: str, new: str) -> None:
132
+ pass
133
+
134
+ @abstractmethod
135
+ def _get_simpler_components(self, base_name: str) -> dict[str, Component]:
136
+ pass
@@ -0,0 +1,144 @@
1
+ """Demand class."""
2
+
3
+ from framcore.attributes import Arrow, AvgFlowVolume, Conversion, ElasticDemand, FlowVolume, ReservePrice
4
+ from framcore.components import Component, Flow
5
+ from framcore.expressions import Expr, ensure_expr
6
+ from framcore.timevectors import TimeVector
7
+
8
+
9
+ class Demand(Component):
10
+ """Demand class representing a simple demand with possible reserve price. Subclass of Component."""
11
+
12
+ def __init__(
13
+ self,
14
+ node: str,
15
+ capacity: FlowVolume | None = None,
16
+ reserve_price: ReservePrice | None = None,
17
+ elastic_demand: ElasticDemand | None = None,
18
+ temperature_profile: Expr | str | TimeVector | None = None,
19
+ consumption: AvgFlowVolume | None = None,
20
+ ) -> None:
21
+ """
22
+ Initialize the Demand class.
23
+
24
+ Args:
25
+ node (str): Node which this Demand consumes power on.
26
+ capacity (FlowVolume | None, optional): Maximum consumption capacity. Defaults to None.
27
+ reserve_price (ReservePrice | None, optional): Price in node at which the Demand will stop consumption. Defaults to None.
28
+ elastic_demand (ElasticDemand | None, optional): Describe changes in consumption based on commodity price in node. Defaults to None.
29
+ temperature_profile (Expr | str | TimeVector | None, optional): Describe changes in consumption based on temperatures. Defaults to None.
30
+ consumption (AvgFlowVolume | None, optional): Actual calculated consumption. Defaults to None.
31
+
32
+ Raises:
33
+ ValueError: When both reserve_price and elastic_demand is passed as arguments. This is ambiguous.
34
+
35
+ """
36
+ super().__init__()
37
+ self._check_type(node, str)
38
+ self._check_type(capacity, (FlowVolume, type(None)))
39
+ self._check_type(reserve_price, (ReservePrice, type(None)))
40
+ self._check_type(elastic_demand, (ElasticDemand, type(None)))
41
+ self._check_type(consumption, (AvgFlowVolume, type(None)))
42
+
43
+ if reserve_price is not None and elastic_demand is not None:
44
+ message = "Cannot have 'reserve_price' and 'elastic_demand' at the same time."
45
+ raise ValueError(message)
46
+
47
+ self._node = node
48
+ self._capacity = capacity
49
+ self._reserve_price = reserve_price
50
+ self._elastic_demand = elastic_demand
51
+ self._temperature_profile = ensure_expr(temperature_profile, is_profile=True)
52
+
53
+ if consumption is None:
54
+ consumption = AvgFlowVolume()
55
+
56
+ self._consumption: AvgFlowVolume = consumption
57
+
58
+ def get_capacity(self) -> FlowVolume:
59
+ """Get the capacity of the demand component."""
60
+ return self._capacity
61
+
62
+ def get_consumption(self) -> AvgFlowVolume:
63
+ """Get the consumption of the demand component."""
64
+ return self._consumption
65
+
66
+ def get_node(self) -> str:
67
+ """Get the node of the demand component."""
68
+ return self._node
69
+
70
+ def set_node(self, node: str) -> None:
71
+ """Set the node of the demand component."""
72
+ self._check_type(node, str)
73
+ self.node = node
74
+
75
+ def get_reserve_price(self) -> ReservePrice | None:
76
+ """Get the reserve price level of the demand component."""
77
+ return self._reserve_price
78
+
79
+ def set_reserve_price(self, reserve_price: ReservePrice | None) -> None:
80
+ """Set the reserve price level of the demand component."""
81
+ self._check_type(reserve_price, (ReservePrice, type(None)))
82
+ if self._elastic_demand and reserve_price:
83
+ message = "Cannot set reserve_price when elastic_demand is not None."
84
+ raise ValueError(message)
85
+ self._reserve_price = reserve_price
86
+
87
+ def get_elastic_demand(self) -> ElasticDemand | None:
88
+ """Get the elastic demand of the demand component."""
89
+ return self._elastic_demand
90
+
91
+ def set_elastic_demand(self, elastic_demand: ElasticDemand | None) -> None:
92
+ """Set the elastic demand of the demand component."""
93
+ self._check_type(elastic_demand, (ElasticDemand, type(None)))
94
+ if self._reserve_price is not None and elastic_demand is not None:
95
+ message = "Cannot set elastic_demand when reserve_price is not None."
96
+ raise ValueError(message)
97
+ self._elastic_demand = elastic_demand
98
+
99
+ def get_temperature_profile(self) -> Expr | None:
100
+ """Get the temperature profile of the demand component."""
101
+ return self._temperature_profile
102
+
103
+ def set_temperature_profile(self, temperature_profile: Expr | str | None) -> None:
104
+ """Set the temperature profile of the demand component."""
105
+ self._check_type(temperature_profile, (Expr, str, TimeVector, type(None)))
106
+ self._temperature_profile = ensure_expr(temperature_profile, is_profile=True)
107
+
108
+ """Implementation of Component interface"""
109
+
110
+ def _replace_node(self, old: str, new: str) -> None:
111
+ if old == self._node:
112
+ self._node = new
113
+ else:
114
+ message = f"{old} not found in {self}. Expected existing node {self._node}."
115
+ raise ValueError(message)
116
+
117
+ def _get_simpler_components(self, base_name: str) -> dict[str, Component]:
118
+ return {base_name + "_Flow": self._create_flow()}
119
+
120
+ def _create_flow(self) -> Flow:
121
+ is_exogenous = self._elastic_demand is None and self._reserve_price is None
122
+
123
+ flow = Flow(
124
+ main_node=self._node,
125
+ max_capacity=self._capacity,
126
+ min_capacity=self._capacity if is_exogenous else None,
127
+ volume=self._consumption,
128
+ arrow_volumes=None,
129
+ is_exogenous=is_exogenous,
130
+ )
131
+
132
+ power_arrow = Arrow(self._node, False, conversion=Conversion(value=1))
133
+ flow.add_arrow(power_arrow)
134
+
135
+ if self._reserve_price is not None:
136
+ flow.add_cost_term("reserve_price", self._reserve_price)
137
+
138
+ # TODO: Implement correctly when Curve is ready. For now, model as inelastic consumer w. reserve_price
139
+ elif self._elastic_demand is not None:
140
+ price = self._elastic_demand.get_max_price()
141
+ reserve_price = ReservePrice(level=price.get_level(), profile=price.get_profile())
142
+ flow.add_cost_term("reserve_price", cost_term=reserve_price)
143
+
144
+ return flow
@@ -0,0 +1,189 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import TYPE_CHECKING
4
+
5
+ from framcore.attributes import Arrow, AvgFlowVolume, FlowVolume, ObjectiveCoefficient, StartUpCost
6
+ from framcore.components import Component
7
+ from framcore.fingerprints import Fingerprint
8
+ from framcore.loaders import Loader
9
+
10
+ if TYPE_CHECKING:
11
+ from framcore.loaders import Loader
12
+
13
+
14
+ class Flow(Component):
15
+ """
16
+ Represents a commodity flow in or out of one or more nodes. Can have Attributes and Metadata.
17
+
18
+ Main attributes are arrows, main_node, max_capacity, min_capacity, startupcost and if it is exogenous.
19
+
20
+ Arrows describes contribution of a Flow into a Node. Has direction to determine input or output,
21
+ and parameters for the contribution of the Flow to the Node (conversion, efficiency, loss).
22
+ Nodes, Flows and Arrows are the main building blocks in FRAM's low-level representation of energy systems.
23
+ """
24
+
25
+ def __init__(
26
+ self,
27
+ main_node: str,
28
+ max_capacity: FlowVolume | None = None,
29
+ min_capacity: FlowVolume | None = None,
30
+ startupcost: StartUpCost | None = None,
31
+ volume: AvgFlowVolume | None = None,
32
+ arrow_volumes: dict[Arrow, AvgFlowVolume] | None = None,
33
+ is_exogenous: bool = False,
34
+ ) -> None:
35
+ """
36
+ Initialize Flow with main node, capacity, and startup cost.
37
+
38
+ Args:
39
+ main_node (str): Node which the Flow is primarily associated with.
40
+ max_capacity (FlowVolume | None, optional): Maximum capacity of the Flow. Defaults to None.
41
+ min_capacity (FlowVolume | None, optional): Minimum capacity of the Flow. Defaults to None.
42
+ startupcost (StartUpCost | None, optional): Costs associated with starting up this Flow. Defaults to None.
43
+ volume (AvgFlowVolume | None, optional): The actual volume carried by this Flow at a given moment. Defaults to None.
44
+ arrow_volumes (dict[Arrow, AvgFlowVolume] | None, optional): Possibility to store a version of volume for each Arrow. Can account for conversion,
45
+ efficiency and loss to represent the result for different commodities and units. Defaults to None.
46
+ is_exogenous (bool, optional): Flag denoting if a Solver should calculate the volumes associated with this flow or use its predefined volume.
47
+ Defaults to False.
48
+
49
+ """
50
+ super().__init__()
51
+ self._check_type(main_node, str)
52
+ self._check_type(max_capacity, (FlowVolume, type(None)))
53
+ self._check_type(min_capacity, (FlowVolume, type(None)))
54
+ self._check_type(startupcost, (StartUpCost, type(None)))
55
+ self._check_type(volume, (FlowVolume, type(None)))
56
+ self._check_type(arrow_volumes, (dict, type(None)))
57
+ self._main_node: str = main_node
58
+ self._max_capacity = max_capacity
59
+ self._min_capacity = min_capacity
60
+ self._startupcost = startupcost
61
+ self._arrows: set[Arrow] = set()
62
+ self._cost_terms: dict[str, ObjectiveCoefficient] = dict()
63
+ self._is_exogenous: bool = is_exogenous
64
+
65
+ if not volume:
66
+ volume = AvgFlowVolume()
67
+ self._volume: AvgFlowVolume = volume
68
+
69
+ if arrow_volumes is None:
70
+ arrow_volumes = dict()
71
+ self._arrow_volumes: dict[Arrow, AvgFlowVolume] = arrow_volumes
72
+
73
+ def is_exogenous(self) -> bool:
74
+ """Return True if Flow is exogenous."""
75
+ return self._is_exogenous
76
+
77
+ def set_exogenous(self) -> None:
78
+ """
79
+ Treat flow as fixed variable.
80
+
81
+ Use volume if it exists.
82
+ If no volume, then try to use
83
+ min_capacity and max_capacity, which must
84
+ be equal. Error if this fails.
85
+ """
86
+ self._is_exogenous = True
87
+
88
+ def set_endogenous(self) -> None:
89
+ """
90
+ Treat flow as decision variable.
91
+
92
+ Volume should be updated with results after a solve.
93
+ """
94
+ self._is_exogenous = False
95
+
96
+ def get_main_node(self) -> str:
97
+ """Get the main node of the flow."""
98
+ return self._main_node
99
+
100
+ def get_volume(self) -> AvgFlowVolume:
101
+ """Get the volume of the flow."""
102
+ return self._volume
103
+
104
+ def get_arrow_volumes(self) -> dict[Arrow, AvgFlowVolume]:
105
+ """Get dict of volume converted to volume at node pointed to by Arrow."""
106
+ return self._arrow_volumes
107
+
108
+ def get_max_capacity(self) -> FlowVolume | None:
109
+ """Get the maximum capacity of the flow."""
110
+ return self._max_capacity
111
+
112
+ def set_max_capacity(self, capacity: FlowVolume | None) -> None:
113
+ """Set the maximum capacity of the flow."""
114
+ self._check_type(capacity, (FlowVolume, type(None)))
115
+ self._max_capacity = capacity
116
+
117
+ def get_min_capacity(self) -> FlowVolume | None:
118
+ """Get the minimum capacity of the flow."""
119
+ return self._min_capacity
120
+
121
+ def set_min_capacity(self, capacity: FlowVolume | None) -> None:
122
+ """Set the minimum capacity of the flow."""
123
+ self._check_type(capacity, (FlowVolume, type(None)))
124
+ self._min_capacity = capacity
125
+
126
+ def get_startupcost(self) -> StartUpCost | None:
127
+ """Get the startup cost of the flow."""
128
+ self._check_type(self._startupcost, (StartUpCost, type(None)))
129
+ return self._startupcost
130
+
131
+ def set_startupcost(self, startupcost: StartUpCost | None) -> None:
132
+ """Set the startup cost of the flow."""
133
+ self._check_type(startupcost, (StartUpCost, type(None)))
134
+ self._startupcost = startupcost
135
+
136
+ def get_arrows(self) -> set[Arrow]:
137
+ """Get the arrows of the flow."""
138
+ return self._arrows
139
+
140
+ def add_arrow(self, arrow: Arrow) -> None:
141
+ """Add an arrow to the flow."""
142
+ self._check_type(arrow, Arrow)
143
+ self._arrows.add(arrow)
144
+
145
+ def add_cost_term(self, key: str, cost_term: ObjectiveCoefficient) -> None:
146
+ """Add a cost term to the flow."""
147
+ self._check_type(key, str)
148
+ self._check_type(cost_term, ObjectiveCoefficient)
149
+ self._cost_terms[key] = cost_term
150
+
151
+ def get_cost_terms(self) -> dict[str, ObjectiveCoefficient]:
152
+ """Get the cost terms of the flow."""
153
+ return self._cost_terms
154
+
155
+ def add_loaders(self, loaders: set[Loader]) -> None:
156
+ """Add loaders stored in attributes to loaders."""
157
+ from framcore.utils import add_loaders_if
158
+
159
+ add_loaders_if(loaders, self.get_volume())
160
+ add_loaders_if(loaders, self.get_max_capacity())
161
+ add_loaders_if(loaders, self.get_min_capacity())
162
+
163
+ for cost in self.get_cost_terms().values():
164
+ add_loaders_if(loaders, cost)
165
+
166
+ for arrow in self.get_arrows():
167
+ add_loaders_if(loaders, arrow)
168
+
169
+ for volume in self.get_arrow_volumes().values():
170
+ add_loaders_if(loaders, volume)
171
+
172
+ def _replace_node(self, old: str, new: str) -> None:
173
+ # Component.replace_node does input type check
174
+ if old == self._main_node:
175
+ self._main_node = new
176
+ for a in self._arrows:
177
+ a: Arrow
178
+ if a.get_node() == old:
179
+ a.set_node(new)
180
+ return
181
+
182
+ def _get_simpler_components(self, _: str) -> dict[str, Component]:
183
+ return dict()
184
+
185
+ def _get_fingerprint(self) -> Fingerprint:
186
+ refs = {}
187
+ refs["_main_node"] = self._main_node
188
+
189
+ return self.get_fingerprint_default()