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.
- fram_core-0.1.0.dist-info/METADATA +42 -0
- fram_core-0.1.0.dist-info/RECORD +100 -0
- {fram_core-0.0.0.dist-info → fram_core-0.1.0.dist-info}/WHEEL +1 -2
- fram_core-0.1.0.dist-info/licenses/LICENSE.md +8 -0
- framcore/Base.py +161 -0
- framcore/Model.py +90 -0
- framcore/__init__.py +10 -0
- framcore/aggregators/Aggregator.py +172 -0
- framcore/aggregators/HydroAggregator.py +849 -0
- framcore/aggregators/NodeAggregator.py +530 -0
- framcore/aggregators/WindSolarAggregator.py +315 -0
- framcore/aggregators/__init__.py +13 -0
- framcore/aggregators/_utils.py +184 -0
- framcore/attributes/Arrow.py +307 -0
- framcore/attributes/ElasticDemand.py +90 -0
- framcore/attributes/ReservoirCurve.py +23 -0
- framcore/attributes/SoftBound.py +16 -0
- framcore/attributes/StartUpCost.py +65 -0
- framcore/attributes/Storage.py +158 -0
- framcore/attributes/TargetBound.py +16 -0
- framcore/attributes/__init__.py +63 -0
- framcore/attributes/hydro/HydroBypass.py +49 -0
- framcore/attributes/hydro/HydroGenerator.py +100 -0
- framcore/attributes/hydro/HydroPump.py +178 -0
- framcore/attributes/hydro/HydroReservoir.py +27 -0
- framcore/attributes/hydro/__init__.py +13 -0
- framcore/attributes/level_profile_attributes.py +911 -0
- framcore/components/Component.py +136 -0
- framcore/components/Demand.py +144 -0
- framcore/components/Flow.py +189 -0
- framcore/components/HydroModule.py +371 -0
- framcore/components/Node.py +99 -0
- framcore/components/Thermal.py +208 -0
- framcore/components/Transmission.py +198 -0
- framcore/components/_PowerPlant.py +81 -0
- framcore/components/__init__.py +22 -0
- framcore/components/wind_solar.py +82 -0
- framcore/curves/Curve.py +44 -0
- framcore/curves/LoadedCurve.py +146 -0
- framcore/curves/__init__.py +9 -0
- framcore/events/__init__.py +21 -0
- framcore/events/events.py +51 -0
- framcore/expressions/Expr.py +591 -0
- framcore/expressions/__init__.py +30 -0
- framcore/expressions/_get_constant_from_expr.py +477 -0
- framcore/expressions/_utils.py +73 -0
- framcore/expressions/queries.py +416 -0
- framcore/expressions/units.py +227 -0
- framcore/fingerprints/__init__.py +11 -0
- framcore/fingerprints/fingerprint.py +292 -0
- framcore/juliamodels/JuliaModel.py +171 -0
- framcore/juliamodels/__init__.py +7 -0
- framcore/loaders/__init__.py +10 -0
- framcore/loaders/loaders.py +405 -0
- framcore/metadata/Div.py +73 -0
- framcore/metadata/ExprMeta.py +56 -0
- framcore/metadata/LevelExprMeta.py +32 -0
- framcore/metadata/Member.py +55 -0
- framcore/metadata/Meta.py +44 -0
- framcore/metadata/__init__.py +15 -0
- framcore/populators/Populator.py +108 -0
- framcore/populators/__init__.py +7 -0
- framcore/querydbs/CacheDB.py +50 -0
- framcore/querydbs/ModelDB.py +34 -0
- framcore/querydbs/QueryDB.py +45 -0
- framcore/querydbs/__init__.py +11 -0
- framcore/solvers/Solver.py +63 -0
- framcore/solvers/SolverConfig.py +272 -0
- framcore/solvers/__init__.py +9 -0
- framcore/timeindexes/AverageYearRange.py +27 -0
- framcore/timeindexes/ConstantTimeIndex.py +22 -0
- framcore/timeindexes/DailyIndex.py +33 -0
- framcore/timeindexes/FixedFrequencyTimeIndex.py +814 -0
- framcore/timeindexes/HourlyIndex.py +33 -0
- framcore/timeindexes/IsoCalendarDay.py +33 -0
- framcore/timeindexes/ListTimeIndex.py +277 -0
- framcore/timeindexes/ModelYear.py +23 -0
- framcore/timeindexes/ModelYears.py +27 -0
- framcore/timeindexes/OneYearProfileTimeIndex.py +29 -0
- framcore/timeindexes/ProfileTimeIndex.py +43 -0
- framcore/timeindexes/SinglePeriodTimeIndex.py +37 -0
- framcore/timeindexes/TimeIndex.py +103 -0
- framcore/timeindexes/WeeklyIndex.py +33 -0
- framcore/timeindexes/__init__.py +36 -0
- framcore/timeindexes/_time_vector_operations.py +689 -0
- framcore/timevectors/ConstantTimeVector.py +131 -0
- framcore/timevectors/LinearTransformTimeVector.py +131 -0
- framcore/timevectors/ListTimeVector.py +127 -0
- framcore/timevectors/LoadedTimeVector.py +97 -0
- framcore/timevectors/ReferencePeriod.py +51 -0
- framcore/timevectors/TimeVector.py +108 -0
- framcore/timevectors/__init__.py +17 -0
- framcore/utils/__init__.py +35 -0
- framcore/utils/get_regional_volumes.py +387 -0
- framcore/utils/get_supported_components.py +60 -0
- framcore/utils/global_energy_equivalent.py +63 -0
- framcore/utils/isolate_subnodes.py +172 -0
- framcore/utils/loaders.py +97 -0
- framcore/utils/node_flow_utils.py +236 -0
- framcore/utils/storage_subsystems.py +106 -0
- fram_core-0.0.0.dist-info/METADATA +0 -5
- fram_core-0.0.0.dist-info/RECORD +0 -4
- fram_core-0.0.0.dist-info/top_level.txt +0 -1
|
@@ -0,0 +1,371 @@
|
|
|
1
|
+
from framcore.attributes import Arrow, AvgFlowVolume, Conversion, FlowVolume, HydroBypass, HydroGenerator, HydroPump, HydroReservoir, WaterValue
|
|
2
|
+
from framcore.components import Component, Flow, Node
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
class HydroModule(Component):
|
|
6
|
+
"""
|
|
7
|
+
HydroModules represents a physical element in a river system, with its topology and other attributes.
|
|
8
|
+
|
|
9
|
+
The hydromodule can contain a HydroReservoir, HydroGenerator, HydroPump, HydroBypass and local inflow, aswell as the topological attributes release_to
|
|
10
|
+
and spill_to:
|
|
11
|
+
|
|
12
|
+
- HydroGenerator uses the release pathway of the HydroModule to generate power, while HydroPump has its own water way that consumes
|
|
13
|
+
power. Both HydroGenerator and HydroPump connects to power nodes.
|
|
14
|
+
- HydroBypass also have attributes that define the topology of the river system.
|
|
15
|
+
- HydroReservoir represents the water storage of the HydroModule.
|
|
16
|
+
- The hydraulic_coupling attribute is used to identify which HydroModules have hydraulic coupled reservoirs.
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
Results for the release volume, spill volume and the water value are stored directly in the HydroModule, while the production, pumping,
|
|
20
|
+
reservoir volume and bypass volume are stored in the attributes.
|
|
21
|
+
|
|
22
|
+
HydroModule is compatible with HydroAggregator for aggregation of multiple HydroModules into one.
|
|
23
|
+
|
|
24
|
+
"""
|
|
25
|
+
|
|
26
|
+
# We add this to module name to get corresponding node name
|
|
27
|
+
_NODE_NAME_POSTFIX = "_node"
|
|
28
|
+
|
|
29
|
+
def __init__(
|
|
30
|
+
self,
|
|
31
|
+
release_to: str | None = None, # Must be reference to another HydroModule
|
|
32
|
+
release_capacity: FlowVolume | None = None,
|
|
33
|
+
generator: HydroGenerator | None = None, # attribute
|
|
34
|
+
pump: HydroPump | None = None,
|
|
35
|
+
inflow: AvgFlowVolume | None = None,
|
|
36
|
+
reservoir: HydroReservoir | None = None, # attribute
|
|
37
|
+
hydraulic_coupling: int = 0,
|
|
38
|
+
bypass: HydroBypass | None = None, # attribute
|
|
39
|
+
spill_to: str | None = None, # Must be reference to another HydroModule
|
|
40
|
+
commodity: str = "Hydro",
|
|
41
|
+
water_value: WaterValue | None = None,
|
|
42
|
+
release_volume: AvgFlowVolume | None = None,
|
|
43
|
+
spill_volume: AvgFlowVolume | None = None,
|
|
44
|
+
) -> None:
|
|
45
|
+
"""
|
|
46
|
+
Initialize the HydroModule with its parameters.
|
|
47
|
+
|
|
48
|
+
Args:
|
|
49
|
+
release_to (str | None, optional): Reference to another HydroModule which recieves the water releases through the main release. Defaults to None.
|
|
50
|
+
release_capacity (FlowVolume | None): Amount of water which can be released via main release at a given moment. Defaults to None.
|
|
51
|
+
generator (HydroGenerator | None, optional): Represents generation of electricity from the movement of water through the Modules main release
|
|
52
|
+
pathway. Defaults to None.
|
|
53
|
+
pump (HydroPump | None): Pump associated with this Module. Can move water to another using power. Defaults to None.
|
|
54
|
+
inflow (AvgFlowVolume | None, optional): The local inflow of the HydroModule. Defaults to None.
|
|
55
|
+
reservoir (HydroReservoir | None, optional): The Modules water storage. Defaults to None.
|
|
56
|
+
hydraulic_coupling (int): Number other than 0 if the HydroModules reservoir is hydraulic coupled to another reservoir. Defaults to 0.
|
|
57
|
+
TODO: Replace with HydraulicCoupling class
|
|
58
|
+
bypass (HydroBypass | None, optional): Bypass water way. Defaults to None.
|
|
59
|
+
spill_to (str | None): Reference to another Module recieving this ones spill volume. Defaults to None.
|
|
60
|
+
commodity (str, optional): Commodity of the hydro node. Defaults to "Hydro".
|
|
61
|
+
water_value (WaterValue | None, optional): Water value of the reservoir in currency per water volume. Defaults to None.
|
|
62
|
+
TODO: Allow water values with multiple demimensions?
|
|
63
|
+
release_volume (AvgFlowVolume | None, optional): Volume of water released via main waterway. Defaults to None.
|
|
64
|
+
spill_volume (AvgFlowVolume | None, optional): Volume of water spilled. Defaults to None.
|
|
65
|
+
|
|
66
|
+
"""
|
|
67
|
+
super().__init__()
|
|
68
|
+
self._check_type(release_to, (str, type(None)))
|
|
69
|
+
self._check_type(release_capacity, (FlowVolume, type(None)))
|
|
70
|
+
self._check_type(generator, (HydroGenerator, type(None)))
|
|
71
|
+
self._check_type(pump, (HydroPump, type(None)))
|
|
72
|
+
self._check_type(inflow, (AvgFlowVolume, type(None)))
|
|
73
|
+
self._check_type(reservoir, (HydroReservoir, type(None)))
|
|
74
|
+
self._check_type(hydraulic_coupling, int)
|
|
75
|
+
self._check_type(bypass, (HydroBypass, type(None)))
|
|
76
|
+
self._check_type(spill_to, (str, type(None)))
|
|
77
|
+
self._check_type(commodity, str)
|
|
78
|
+
self._check_type(water_value, (WaterValue, type(None)))
|
|
79
|
+
self._check_type(release_volume, (AvgFlowVolume, type(None)))
|
|
80
|
+
self._check_type(spill_volume, (AvgFlowVolume, type(None)))
|
|
81
|
+
|
|
82
|
+
self._release_to = release_to
|
|
83
|
+
self._release_capacity = release_capacity
|
|
84
|
+
self._generator = generator
|
|
85
|
+
self._pump = pump
|
|
86
|
+
self._inflow = inflow
|
|
87
|
+
self._reservoir = reservoir
|
|
88
|
+
self._hydraulic_coupling = hydraulic_coupling
|
|
89
|
+
self._bypass = bypass
|
|
90
|
+
self._spill_to = spill_to
|
|
91
|
+
self._commodity = commodity
|
|
92
|
+
|
|
93
|
+
if not water_value:
|
|
94
|
+
water_value = WaterValue()
|
|
95
|
+
|
|
96
|
+
if not release_volume:
|
|
97
|
+
release_volume = AvgFlowVolume()
|
|
98
|
+
|
|
99
|
+
if not spill_volume:
|
|
100
|
+
spill_volume = AvgFlowVolume()
|
|
101
|
+
|
|
102
|
+
self._water_value: WaterValue = water_value
|
|
103
|
+
self._release_volume: AvgFlowVolume = release_volume
|
|
104
|
+
self._spill_volume: AvgFlowVolume = spill_volume
|
|
105
|
+
|
|
106
|
+
def get_release_capacity(self) -> FlowVolume | None:
|
|
107
|
+
"""Get the capacity of the thermal unit."""
|
|
108
|
+
return self._release_capacity
|
|
109
|
+
|
|
110
|
+
def get_hydraulic_coupling(self) -> int:
|
|
111
|
+
"""Get the Modules hydraulic code."""
|
|
112
|
+
return self._hydraulic_coupling
|
|
113
|
+
|
|
114
|
+
def get_reservoir(self) -> HydroReservoir | None:
|
|
115
|
+
"""Get the reservoir of the hydro module."""
|
|
116
|
+
return self._reservoir
|
|
117
|
+
|
|
118
|
+
def set_reservoir(self, reservoir: HydroReservoir | None) -> None:
|
|
119
|
+
"""Set the reservoir of the hydro module."""
|
|
120
|
+
self._check_type(reservoir, (HydroReservoir, type(None)))
|
|
121
|
+
self._reservoir = reservoir
|
|
122
|
+
|
|
123
|
+
def get_pump(self) -> HydroPump | None:
|
|
124
|
+
"""Get the pump of the hydro module."""
|
|
125
|
+
return self._pump
|
|
126
|
+
|
|
127
|
+
def set_pump(self, pump: HydroPump | None) -> None:
|
|
128
|
+
"""Set the pump of the hydro module."""
|
|
129
|
+
self._check_type(pump, (HydroPump, type(None)))
|
|
130
|
+
self._pump = pump
|
|
131
|
+
|
|
132
|
+
def get_generator(self) -> HydroGenerator | None:
|
|
133
|
+
"""Get the generator of the hydro module."""
|
|
134
|
+
return self._generator
|
|
135
|
+
|
|
136
|
+
def set_generator(self, generator: HydroGenerator | None) -> None:
|
|
137
|
+
"""Set the generator of the hydro module."""
|
|
138
|
+
self._check_type(generator, (HydroGenerator, type(None)))
|
|
139
|
+
self._generator = generator
|
|
140
|
+
|
|
141
|
+
def get_bypass(self) -> HydroBypass | None:
|
|
142
|
+
"""Get the bypass of the hydro module."""
|
|
143
|
+
return self._bypass
|
|
144
|
+
|
|
145
|
+
def set_bypass(self, bypass: HydroBypass | None) -> None:
|
|
146
|
+
"""Set the bypass of the hydro module."""
|
|
147
|
+
self._check_type(bypass, (HydroBypass, type(None)))
|
|
148
|
+
self._bypass = bypass
|
|
149
|
+
|
|
150
|
+
def get_inflow(self) -> AvgFlowVolume | None:
|
|
151
|
+
"""Get the inflow of the hydro module."""
|
|
152
|
+
return self._inflow
|
|
153
|
+
|
|
154
|
+
def set_inflow(self, inflow: AvgFlowVolume | None) -> None:
|
|
155
|
+
"""Set the inflow of the hydro module."""
|
|
156
|
+
self._check_type(inflow, (AvgFlowVolume, type(None)))
|
|
157
|
+
self._inflow = inflow
|
|
158
|
+
|
|
159
|
+
def get_release_to(self) -> str | None:
|
|
160
|
+
"""Get the release_to module of the hydro module."""
|
|
161
|
+
return self._release_to
|
|
162
|
+
|
|
163
|
+
def set_release_to(self, release_to: str | None) -> None:
|
|
164
|
+
"""Set the release_to module of the hydro module."""
|
|
165
|
+
self._check_type(release_to, (str, type(None)))
|
|
166
|
+
self._release_to = release_to
|
|
167
|
+
|
|
168
|
+
def get_spill_to(self) -> str | None:
|
|
169
|
+
"""Get the spill_to module of the hydro module."""
|
|
170
|
+
return self._spill_to
|
|
171
|
+
|
|
172
|
+
def get_water_value(self) -> WaterValue:
|
|
173
|
+
"""Get water value at the hydro node."""
|
|
174
|
+
return self._water_value
|
|
175
|
+
|
|
176
|
+
def get_release_volume(self) -> FlowVolume:
|
|
177
|
+
"""Get the release_volume volume of the thermal unit."""
|
|
178
|
+
return self._release_volume
|
|
179
|
+
|
|
180
|
+
def get_spill_volume(self) -> FlowVolume:
|
|
181
|
+
"""Get the spill_volume volume of the thermal unit."""
|
|
182
|
+
return self._spill_volume
|
|
183
|
+
|
|
184
|
+
"""Implementation of Component interface"""
|
|
185
|
+
|
|
186
|
+
def _replace_node(self, old: str, new: str) -> None:
|
|
187
|
+
if self._pump and old == self._pump.get_power_node():
|
|
188
|
+
self._pump.set_power_node(new)
|
|
189
|
+
if self._generator and old == self._generator.get_power_node():
|
|
190
|
+
self._generator.set_power_node(new)
|
|
191
|
+
|
|
192
|
+
def _get_simpler_components(self, module_name: str) -> dict[str, Component]:
|
|
193
|
+
out: dict[str, Component] = {}
|
|
194
|
+
|
|
195
|
+
node_name = module_name + self._NODE_NAME_POSTFIX
|
|
196
|
+
|
|
197
|
+
out[node_name] = self._create_hydro_node()
|
|
198
|
+
out[module_name + "_release_flow"] = self._create_release_flow(node_name)
|
|
199
|
+
out[module_name + "_spill_flow"] = self._create_spill_flow(node_name)
|
|
200
|
+
|
|
201
|
+
if self._inflow is not None:
|
|
202
|
+
out[module_name + "_inflow_flow"] = self._create_inflow_flow(node_name)
|
|
203
|
+
|
|
204
|
+
if self._bypass is not None:
|
|
205
|
+
out[module_name + "_bypass_flow"] = self._create_bypass_flow(node_name)
|
|
206
|
+
|
|
207
|
+
if self._pump is not None:
|
|
208
|
+
out[module_name + "_pump_flow"] = self._create_pump_flow(node_name)
|
|
209
|
+
|
|
210
|
+
return out
|
|
211
|
+
|
|
212
|
+
def _create_hydro_node(self) -> Node:
|
|
213
|
+
return Node(
|
|
214
|
+
commodity=self._commodity,
|
|
215
|
+
price=self._water_value,
|
|
216
|
+
storage=self._reservoir,
|
|
217
|
+
)
|
|
218
|
+
|
|
219
|
+
def _create_release_flow(self, node_name: str) -> Flow:
|
|
220
|
+
# TODO: pq_curve, nominal_head, tailwater_elevation
|
|
221
|
+
flow = Flow(
|
|
222
|
+
main_node=node_name,
|
|
223
|
+
max_capacity=self._release_capacity,
|
|
224
|
+
volume=self._release_volume,
|
|
225
|
+
startupcost=None,
|
|
226
|
+
arrow_volumes=None,
|
|
227
|
+
is_exogenous=False,
|
|
228
|
+
)
|
|
229
|
+
|
|
230
|
+
arrow_volumes = flow.get_arrow_volumes()
|
|
231
|
+
|
|
232
|
+
outgoing_arrow = Arrow(
|
|
233
|
+
node=node_name,
|
|
234
|
+
is_ingoing=False,
|
|
235
|
+
conversion=Conversion(value=1),
|
|
236
|
+
)
|
|
237
|
+
|
|
238
|
+
flow.add_arrow(outgoing_arrow)
|
|
239
|
+
|
|
240
|
+
if self._release_to:
|
|
241
|
+
flow.add_arrow(
|
|
242
|
+
Arrow(
|
|
243
|
+
node=self._release_to + self._NODE_NAME_POSTFIX,
|
|
244
|
+
is_ingoing=True,
|
|
245
|
+
conversion=Conversion(value=1),
|
|
246
|
+
),
|
|
247
|
+
)
|
|
248
|
+
|
|
249
|
+
if self._generator:
|
|
250
|
+
production_arrow = Arrow(
|
|
251
|
+
node=self._generator.get_power_node(),
|
|
252
|
+
is_ingoing=True,
|
|
253
|
+
conversion=self._generator.get_energy_equivalent(),
|
|
254
|
+
)
|
|
255
|
+
flow.add_arrow(production_arrow)
|
|
256
|
+
arrow_volumes[production_arrow] = self._generator.get_production()
|
|
257
|
+
|
|
258
|
+
if self._generator.get_voc() is not None:
|
|
259
|
+
flow.add_cost_term("VOC", self._generator.get_voc())
|
|
260
|
+
|
|
261
|
+
return flow
|
|
262
|
+
|
|
263
|
+
def _create_spill_flow(self, node_name: str) -> Flow:
|
|
264
|
+
flow = Flow(
|
|
265
|
+
main_node=node_name,
|
|
266
|
+
max_capacity=None,
|
|
267
|
+
volume=self._spill_volume,
|
|
268
|
+
)
|
|
269
|
+
|
|
270
|
+
flow.add_arrow(
|
|
271
|
+
Arrow(
|
|
272
|
+
node=node_name,
|
|
273
|
+
is_ingoing=False,
|
|
274
|
+
conversion=Conversion(value=1),
|
|
275
|
+
),
|
|
276
|
+
)
|
|
277
|
+
|
|
278
|
+
if self._spill_to is not None:
|
|
279
|
+
flow.add_arrow(
|
|
280
|
+
Arrow(
|
|
281
|
+
node=self._spill_to + self._NODE_NAME_POSTFIX,
|
|
282
|
+
is_ingoing=True,
|
|
283
|
+
conversion=Conversion(value=1),
|
|
284
|
+
),
|
|
285
|
+
)
|
|
286
|
+
|
|
287
|
+
return flow
|
|
288
|
+
|
|
289
|
+
def _create_bypass_flow(self, node_name: str) -> Flow:
|
|
290
|
+
flow = Flow(
|
|
291
|
+
main_node=node_name,
|
|
292
|
+
max_capacity=self._bypass.get_capacity(),
|
|
293
|
+
volume=self._bypass.get_volume(),
|
|
294
|
+
is_exogenous=False,
|
|
295
|
+
)
|
|
296
|
+
|
|
297
|
+
flow.add_arrow(
|
|
298
|
+
Arrow(
|
|
299
|
+
node=node_name,
|
|
300
|
+
is_ingoing=False,
|
|
301
|
+
conversion=Conversion(value=1),
|
|
302
|
+
),
|
|
303
|
+
)
|
|
304
|
+
|
|
305
|
+
if self._bypass.get_to_module() is not None:
|
|
306
|
+
flow.add_arrow(
|
|
307
|
+
Arrow(
|
|
308
|
+
node=self._bypass.get_to_module() + self._NODE_NAME_POSTFIX,
|
|
309
|
+
is_ingoing=True,
|
|
310
|
+
conversion=Conversion(value=1),
|
|
311
|
+
),
|
|
312
|
+
)
|
|
313
|
+
|
|
314
|
+
return flow
|
|
315
|
+
|
|
316
|
+
def _create_inflow_flow(self, node_name: str) -> Flow:
|
|
317
|
+
flow = Flow(
|
|
318
|
+
main_node=node_name,
|
|
319
|
+
max_capacity=None,
|
|
320
|
+
volume=self._inflow,
|
|
321
|
+
is_exogenous=True,
|
|
322
|
+
)
|
|
323
|
+
|
|
324
|
+
flow.add_arrow(
|
|
325
|
+
Arrow(
|
|
326
|
+
node=node_name,
|
|
327
|
+
is_ingoing=True,
|
|
328
|
+
conversion=Conversion(value=1),
|
|
329
|
+
),
|
|
330
|
+
)
|
|
331
|
+
|
|
332
|
+
return flow
|
|
333
|
+
|
|
334
|
+
def _create_pump_flow(self, node_name: str) -> Flow:
|
|
335
|
+
# TODO: add rest of attributes
|
|
336
|
+
|
|
337
|
+
arrow_volumes: dict[Arrow, FlowVolume] = dict()
|
|
338
|
+
|
|
339
|
+
flow = Flow(
|
|
340
|
+
main_node=node_name,
|
|
341
|
+
max_capacity=self._pump.get_water_capacity(),
|
|
342
|
+
volume=self._pump.get_water_consumption(),
|
|
343
|
+
arrow_volumes=arrow_volumes,
|
|
344
|
+
is_exogenous=False,
|
|
345
|
+
)
|
|
346
|
+
|
|
347
|
+
flow.add_arrow(
|
|
348
|
+
Arrow(
|
|
349
|
+
node=self._pump.get_to_module() + self._NODE_NAME_POSTFIX,
|
|
350
|
+
is_ingoing=True,
|
|
351
|
+
conversion=Conversion(value=1),
|
|
352
|
+
),
|
|
353
|
+
)
|
|
354
|
+
|
|
355
|
+
flow.add_arrow(
|
|
356
|
+
Arrow(
|
|
357
|
+
node=self._pump.get_from_module() + self._NODE_NAME_POSTFIX,
|
|
358
|
+
is_ingoing=False,
|
|
359
|
+
conversion=Conversion(value=1),
|
|
360
|
+
),
|
|
361
|
+
)
|
|
362
|
+
|
|
363
|
+
pump_arrow = Arrow(
|
|
364
|
+
node=self._pump.get_power_node(),
|
|
365
|
+
is_ingoing=False,
|
|
366
|
+
conversion=self._pump.get_energy_equivalent(),
|
|
367
|
+
)
|
|
368
|
+
flow.add_arrow(pump_arrow)
|
|
369
|
+
arrow_volumes[pump_arrow] = self._pump.get_power_consumption()
|
|
370
|
+
|
|
371
|
+
return flow
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import TYPE_CHECKING
|
|
4
|
+
|
|
5
|
+
from framcore.attributes import Price, ShadowPrice, Storage
|
|
6
|
+
from framcore.components import Component
|
|
7
|
+
|
|
8
|
+
if TYPE_CHECKING:
|
|
9
|
+
from framcore.loaders import Loader
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class Node(Component):
|
|
13
|
+
"""
|
|
14
|
+
Represents a point in the energy system where a commodity can possibly be traded, stored or pass through.
|
|
15
|
+
|
|
16
|
+
A node is characterized by the commodity it handles, its price, and optionally storage capabilities. If the
|
|
17
|
+
node is exogenous, the commodity can be bought and sold at a fixed price determined by the user.
|
|
18
|
+
If the node is endogenous, the price is determined by the market dynamics at the Node.
|
|
19
|
+
|
|
20
|
+
Nodes, Flows and Arrows are the main building blocks in FRAM's low-level representation of energy systems.
|
|
21
|
+
Movement between Nodes is represented by Flows and Arrows. Flows represent a commodity flow,
|
|
22
|
+
and can have Arrows that each describe contribution of the Flow into a Node.
|
|
23
|
+
The Arrows have direction to determine input or output,
|
|
24
|
+
and parameters for the contribution of the Flow to the Node (conversion, efficiency and loss).
|
|
25
|
+
|
|
26
|
+
"""
|
|
27
|
+
|
|
28
|
+
def __init__(
|
|
29
|
+
self,
|
|
30
|
+
commodity: str,
|
|
31
|
+
is_exogenous: bool = False, # TODO
|
|
32
|
+
price: ShadowPrice | None = None,
|
|
33
|
+
storage: Storage | None = None,
|
|
34
|
+
) -> None:
|
|
35
|
+
"""
|
|
36
|
+
Initialize the Node class.
|
|
37
|
+
|
|
38
|
+
Args:
|
|
39
|
+
commodity (str): Commodity at the Node. Power/electricity, gas, heat, etc.
|
|
40
|
+
is_exogenous (bool, optional): Flag used to signal Solvers whether they should simulate the node endogenously or use the pre-set price.
|
|
41
|
+
Defaults to False.
|
|
42
|
+
price (ShadowPrice | None): Actual, calculated price of Commodity in this Node for each moment of simulation. Defaulta to None.
|
|
43
|
+
storage (Storage | None, optional): The amount of the Commodity stored on this Node. Defaults to None.
|
|
44
|
+
|
|
45
|
+
"""
|
|
46
|
+
super().__init__()
|
|
47
|
+
self._check_type(commodity, str)
|
|
48
|
+
self._check_type(is_exogenous, bool)
|
|
49
|
+
self._check_type(price, (ShadowPrice, type(None)))
|
|
50
|
+
self._check_type(storage, (Storage, type(None)))
|
|
51
|
+
|
|
52
|
+
self._commodity = commodity
|
|
53
|
+
self._is_exogenous = is_exogenous
|
|
54
|
+
|
|
55
|
+
self._storage = storage
|
|
56
|
+
|
|
57
|
+
if price is None:
|
|
58
|
+
price = Price()
|
|
59
|
+
|
|
60
|
+
self._price: Price = price
|
|
61
|
+
|
|
62
|
+
def set_exogenous(self) -> None:
|
|
63
|
+
"""Set the Node to be exogenous."""
|
|
64
|
+
self._check_type(self._is_exogenous, bool)
|
|
65
|
+
self._is_exogenous = True
|
|
66
|
+
|
|
67
|
+
def set_endogenous(self) -> None:
|
|
68
|
+
"""Set the Node to be endogenous."""
|
|
69
|
+
self._check_type(self._is_exogenous, bool)
|
|
70
|
+
self._is_exogenous = False
|
|
71
|
+
|
|
72
|
+
def is_exogenous(self) -> bool:
|
|
73
|
+
"""Return True if Node is exogenous (i.e. has fixed prices determined outside the model) else False."""
|
|
74
|
+
return self._is_exogenous
|
|
75
|
+
|
|
76
|
+
def get_price(self) -> ShadowPrice:
|
|
77
|
+
"""Return price."""
|
|
78
|
+
return self._price
|
|
79
|
+
|
|
80
|
+
def get_storage(self) -> Storage | None:
|
|
81
|
+
"""Get Storage if any."""
|
|
82
|
+
return self._storage
|
|
83
|
+
|
|
84
|
+
def get_commodity(self) -> str:
|
|
85
|
+
"""Return commodity."""
|
|
86
|
+
return self._commodity
|
|
87
|
+
|
|
88
|
+
def add_loaders(self, loaders: set[Loader]) -> None:
|
|
89
|
+
"""Add loaders stored in attributes to loaders."""
|
|
90
|
+
from framcore.utils import add_loaders_if
|
|
91
|
+
|
|
92
|
+
add_loaders_if(loaders, self.get_price())
|
|
93
|
+
add_loaders_if(loaders, self.get_storage())
|
|
94
|
+
|
|
95
|
+
def _replace_node(self, old: str, new: str) -> None:
|
|
96
|
+
return None
|
|
97
|
+
|
|
98
|
+
def _get_simpler_components(self, _: str) -> dict[str, Component]:
|
|
99
|
+
return dict()
|
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
from framcore.attributes import Arrow, AvgFlowVolume, Conversion, Cost, Efficiency, FlowVolume, StartUpCost
|
|
2
|
+
from framcore.components import Component, Flow
|
|
3
|
+
from framcore.components._PowerPlant import _PowerPlant
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class Thermal(_PowerPlant):
|
|
7
|
+
"""
|
|
8
|
+
Represents a thermal power plant, subclassing PowerPlant.
|
|
9
|
+
|
|
10
|
+
This class models a thermal power plant with attributes inherited from PowerPlant.
|
|
11
|
+
Additionally, it includes specific attributes such as:
|
|
12
|
+
|
|
13
|
+
- fuel node
|
|
14
|
+
- efficiency
|
|
15
|
+
- emission node
|
|
16
|
+
- emission coefficient
|
|
17
|
+
- startup costs
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
This class is compatible with ThermalAggregator.
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
def __init__(
|
|
24
|
+
self,
|
|
25
|
+
power_node: str,
|
|
26
|
+
fuel_node: str,
|
|
27
|
+
efficiency: Efficiency,
|
|
28
|
+
max_capacity: FlowVolume,
|
|
29
|
+
emission_node: str | None = None,
|
|
30
|
+
emission_coefficient: Conversion | None = None,
|
|
31
|
+
startupcost: StartUpCost | None = None,
|
|
32
|
+
min_capacity: FlowVolume | None = None,
|
|
33
|
+
voc: Cost | None = None,
|
|
34
|
+
production: AvgFlowVolume | None = None,
|
|
35
|
+
fuel_demand: AvgFlowVolume | None = None,
|
|
36
|
+
emission_demand: AvgFlowVolume | None = None,
|
|
37
|
+
) -> None:
|
|
38
|
+
"""
|
|
39
|
+
Initialize a Thermal power plant instance.
|
|
40
|
+
|
|
41
|
+
Args:
|
|
42
|
+
power_node (str): The power node of the plant.
|
|
43
|
+
fuel_node (str): The fuel node of the plant.
|
|
44
|
+
efficiency (Efficiency): Efficiency of the plant.
|
|
45
|
+
emission_node (str | None, optional): Emission node.
|
|
46
|
+
emission_coefficient (Conversion | None, optional): Emission coefficient.
|
|
47
|
+
startupcost (StartUpCost | None, optional): Cost associated with starting up the Plant.
|
|
48
|
+
max_capacity (FlowVolume | None, optional): Maximum production capacity.
|
|
49
|
+
min_capacity (FlowVolume | None, optional): Minimum production capacity.
|
|
50
|
+
voc (Cost | None, optional): Variable operating cost.
|
|
51
|
+
production (AvgFlowVolume | None, optional): Production volume.
|
|
52
|
+
fuel_demand (AvgFlowVolume | None, optional): Fuel demand.
|
|
53
|
+
emission_demand (AvgFlowVolume | None, optional): Emission demand.
|
|
54
|
+
|
|
55
|
+
"""
|
|
56
|
+
super().__init__(
|
|
57
|
+
power_node=power_node,
|
|
58
|
+
max_capacity=max_capacity,
|
|
59
|
+
min_capacity=min_capacity,
|
|
60
|
+
voc=voc,
|
|
61
|
+
production=production,
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
self._check_type(fuel_node, str)
|
|
65
|
+
self._check_type(emission_node, (str, type(None)))
|
|
66
|
+
self._check_type(emission_coefficient, (Conversion, type(None)))
|
|
67
|
+
self._check_type(startupcost, (StartUpCost, type(None)))
|
|
68
|
+
self._check_type(production, (AvgFlowVolume, type(None)))
|
|
69
|
+
self._check_type(fuel_demand, (AvgFlowVolume, type(None)))
|
|
70
|
+
self._check_type(emission_demand, (AvgFlowVolume, type(None)))
|
|
71
|
+
|
|
72
|
+
self._fuel_node = fuel_node
|
|
73
|
+
self._efficiency = efficiency
|
|
74
|
+
self._emission_node = emission_node
|
|
75
|
+
self._emission_coefficient = emission_coefficient
|
|
76
|
+
self._startupcost = startupcost
|
|
77
|
+
|
|
78
|
+
if production is None:
|
|
79
|
+
production = AvgFlowVolume()
|
|
80
|
+
|
|
81
|
+
if fuel_demand is None:
|
|
82
|
+
fuel_demand = AvgFlowVolume()
|
|
83
|
+
|
|
84
|
+
if emission_demand is None and emission_node is not None:
|
|
85
|
+
emission_demand = AvgFlowVolume()
|
|
86
|
+
|
|
87
|
+
self._production = production
|
|
88
|
+
self._fuel_demand = fuel_demand
|
|
89
|
+
self._emission_demand = emission_demand
|
|
90
|
+
|
|
91
|
+
def get_fuel_node(self) -> str:
|
|
92
|
+
"""Get the fuel node of the thermal unit."""
|
|
93
|
+
return self._fuel_node
|
|
94
|
+
|
|
95
|
+
def set_fuel_node(self, fuel_node: str) -> None:
|
|
96
|
+
"""Set the fuel node of the thermal unit."""
|
|
97
|
+
self._check_type(fuel_node, str)
|
|
98
|
+
self._fuel_node = fuel_node
|
|
99
|
+
|
|
100
|
+
def get_emission_node(self) -> str | None:
|
|
101
|
+
"""Get the emission node of the thermal unit."""
|
|
102
|
+
return self._emission_node
|
|
103
|
+
|
|
104
|
+
def set_emission_node(self, emission_node: str | None) -> None:
|
|
105
|
+
"""Set the emission node of the thermal unit."""
|
|
106
|
+
self._check_type(emission_node, (str, type(None)))
|
|
107
|
+
self._emission_node = emission_node
|
|
108
|
+
|
|
109
|
+
def get_emission_coefficient(self) -> Conversion | None:
|
|
110
|
+
"""Get the emission coefficient of the thermal unit."""
|
|
111
|
+
return self._emission_coefficient
|
|
112
|
+
|
|
113
|
+
def set_emission_coefficient(self, emission_coefficient: Conversion | None) -> None:
|
|
114
|
+
"""Set the emission coefficient of the thermal unit."""
|
|
115
|
+
self._check_type(emission_coefficient, (Conversion, type(None)))
|
|
116
|
+
self._emission_coefficient = emission_coefficient
|
|
117
|
+
|
|
118
|
+
def get_fuel_demand(self) -> AvgFlowVolume:
|
|
119
|
+
"""Get the fuel demand of the thermal unit."""
|
|
120
|
+
return self._fuel_demand
|
|
121
|
+
|
|
122
|
+
def get_emission_demand(self) -> AvgFlowVolume | None:
|
|
123
|
+
"""Get the emission demand of the thermal unit."""
|
|
124
|
+
return self._emission_demand
|
|
125
|
+
|
|
126
|
+
def set_emission_demand(self, value: AvgFlowVolume | None) -> None:
|
|
127
|
+
"""Set the emission demand of the thermal unit."""
|
|
128
|
+
self._check_type(value, (AvgFlowVolume, type(None)))
|
|
129
|
+
self._emission_demand = value
|
|
130
|
+
|
|
131
|
+
def get_efficiency(self) -> Efficiency:
|
|
132
|
+
"""Get the efficiency of the thermal unit."""
|
|
133
|
+
return self._efficiency
|
|
134
|
+
|
|
135
|
+
def get_startupcost(self) -> StartUpCost | None:
|
|
136
|
+
"""Get the startup cost of the thermal unit."""
|
|
137
|
+
return self._startupcost
|
|
138
|
+
|
|
139
|
+
def set_startupcost(self, startupcost: StartUpCost | None) -> None:
|
|
140
|
+
"""Set the startup cost of the thermal unit."""
|
|
141
|
+
self._check_type(startupcost, (StartUpCost, type(None)))
|
|
142
|
+
self._startupcost = startupcost
|
|
143
|
+
|
|
144
|
+
"""Implementation of Component interface"""
|
|
145
|
+
|
|
146
|
+
def _get_simpler_components(self, base_name: str) -> dict[str, Component]:
|
|
147
|
+
return {base_name + "_Flow": self._create_flow()}
|
|
148
|
+
|
|
149
|
+
def _replace_node(self, old: str, new: str) -> None:
|
|
150
|
+
existing_nodes = [self._power_node, self._fuel_node]
|
|
151
|
+
existing_nodes = existing_nodes if self._emission_node is None else [*existing_nodes, self._emission_node]
|
|
152
|
+
if old not in existing_nodes:
|
|
153
|
+
message = f"{old} not found in {self}. Expected one of the existing nodes {existing_nodes}."
|
|
154
|
+
raise ValueError(message)
|
|
155
|
+
|
|
156
|
+
if self._power_node == old:
|
|
157
|
+
self._power_node = new
|
|
158
|
+
if self._fuel_node == old:
|
|
159
|
+
self._fuel_node = new
|
|
160
|
+
if (self._emission_node is not None) and (old == self._emission_node):
|
|
161
|
+
self._emission_node = new
|
|
162
|
+
|
|
163
|
+
def _create_flow(self) -> Flow:
|
|
164
|
+
arrow_volumes: dict[Arrow, AvgFlowVolume] = dict()
|
|
165
|
+
|
|
166
|
+
is_exogenous = self._max_capacity == self._min_capacity
|
|
167
|
+
|
|
168
|
+
flow = Flow(
|
|
169
|
+
main_node=self._power_node,
|
|
170
|
+
max_capacity=self._max_capacity,
|
|
171
|
+
min_capacity=self._min_capacity,
|
|
172
|
+
startupcost=self._startupcost,
|
|
173
|
+
volume=self._production,
|
|
174
|
+
arrow_volumes=arrow_volumes,
|
|
175
|
+
is_exogenous=is_exogenous,
|
|
176
|
+
)
|
|
177
|
+
|
|
178
|
+
power_arrow = Arrow(
|
|
179
|
+
node=self._power_node,
|
|
180
|
+
is_ingoing=True,
|
|
181
|
+
conversion=Conversion(value=1),
|
|
182
|
+
)
|
|
183
|
+
flow.add_arrow(power_arrow)
|
|
184
|
+
|
|
185
|
+
fuel_arrow = Arrow(
|
|
186
|
+
node=self._fuel_node,
|
|
187
|
+
is_ingoing=False,
|
|
188
|
+
efficiency=self._efficiency,
|
|
189
|
+
)
|
|
190
|
+
flow.add_arrow(fuel_arrow)
|
|
191
|
+
arrow_volumes[fuel_arrow] = self._fuel_demand
|
|
192
|
+
|
|
193
|
+
if self._emission_node is not None:
|
|
194
|
+
if self._emission_demand is None:
|
|
195
|
+
self._emission_demand = AvgFlowVolume()
|
|
196
|
+
emission_arrow = Arrow(
|
|
197
|
+
node=self._emission_node,
|
|
198
|
+
is_ingoing=False,
|
|
199
|
+
conversion=self._emission_coefficient,
|
|
200
|
+
efficiency=self._efficiency,
|
|
201
|
+
)
|
|
202
|
+
flow.add_arrow(emission_arrow)
|
|
203
|
+
arrow_volumes[emission_arrow] = self._emission_demand
|
|
204
|
+
|
|
205
|
+
if self._voc:
|
|
206
|
+
flow.add_cost_term("VOC", self._voc)
|
|
207
|
+
|
|
208
|
+
return flow
|