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,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