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,307 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import TYPE_CHECKING
4
+
5
+ import numpy as np
6
+ from numpy.typing import NDArray
7
+
8
+ from framcore import Base
9
+ from framcore.attributes import Conversion, Efficiency, Loss
10
+ from framcore.querydbs import QueryDB
11
+ from framcore.timeindexes import FixedFrequencyTimeIndex, SinglePeriodTimeIndex, TimeIndex
12
+
13
+ if TYPE_CHECKING:
14
+ from framcore import Model
15
+ from framcore.loaders import Loader
16
+
17
+
18
+ class Arrow(Base):
19
+ """
20
+ Arrow class is used by Flows to represent contribution of its commodity to Nodes.
21
+
22
+ The Arrow has direction to determine input or output (is_ingoing), and parameters for the contribution of the Flow to the Node.
23
+ The main parameters are conversion, efficiency and loss which together form the coefficient = conversion * (1 / efficiency) * (1 - loss)
24
+ Arrow has its own implementation of get_scenario_vector and get_data_value to calculate the coefficient shown above.
25
+ """
26
+
27
+ def __init__(
28
+ self,
29
+ node: str,
30
+ is_ingoing: bool,
31
+ conversion: Conversion | None = None,
32
+ efficiency: Efficiency | None = None,
33
+ loss: Loss | None = None,
34
+ ) -> None:
35
+ """Initialize the Arrow class."""
36
+ self._check_type(node, str)
37
+ self._check_type(is_ingoing, bool)
38
+ self._check_type(conversion, (Conversion, type(None)))
39
+ self._check_type(efficiency, (Efficiency, type(None)))
40
+ self._check_type(loss, (Loss, type(None)))
41
+ self._node = node
42
+ self._is_ingoing = is_ingoing
43
+ self._conversion = conversion
44
+ self._efficiency = efficiency
45
+ self._loss = loss
46
+
47
+ def get_node(self) -> str:
48
+ """Get the node the arrow is pointing to."""
49
+ return self._node
50
+
51
+ def set_node(self, node: str) -> None:
52
+ """Set the node the arrow is pointing to."""
53
+ self._check_type(node, str)
54
+ self._node = node
55
+
56
+ def is_ingoing(self) -> bool:
57
+ """
58
+ Return True if arrow is ingoing.
59
+
60
+ Ingoing means the flow variable supplies to node.
61
+ Outgoing means the flow variable takes out of node.
62
+ """
63
+ return self._is_ingoing
64
+
65
+ def get_conversion(self) -> Conversion | None:
66
+ """Get the conversion."""
67
+ return self._conversion
68
+
69
+ def set_conversion(self, value: Conversion | None) -> None:
70
+ """Set the conversion."""
71
+ self._check_type(value, Conversion, type(None))
72
+ self._conversion = value
73
+
74
+ def get_efficiency(self) -> Efficiency | None:
75
+ """Get the efficiency."""
76
+ return self._efficiency
77
+
78
+ def set_efficiency(self, value: Efficiency | None) -> None:
79
+ """Set the efficiency."""
80
+ self._check_type(value, Efficiency, type(None))
81
+ self._efficiency = value
82
+
83
+ def get_loss(self) -> Loss | None:
84
+ """Get the loss."""
85
+ return self._loss
86
+
87
+ def set_loss(self, value: Loss | None) -> None:
88
+ """Set the loss."""
89
+ self._check_type(value, Loss, type(None))
90
+ self._loss = value
91
+
92
+ def has_profile(self) -> bool:
93
+ """Return True if any of conversion, efficiency or loss has profile."""
94
+ if self._conversion is not None and self._conversion.has_profile():
95
+ return True
96
+ if self._efficiency is not None and self._efficiency.has_profile():
97
+ return True
98
+ return bool(self._loss is not None and self._loss.has_profile())
99
+
100
+ def get_conversion_unit_set(
101
+ self,
102
+ db: QueryDB | Model,
103
+ ) -> set[str]:
104
+ """Get set of units behind conversion level expr (if any)."""
105
+ if self._conversion is None:
106
+ return set()
107
+ return self._conversion.get_level_unit_set(db)
108
+
109
+ def get_profile_timeindex_set(
110
+ self,
111
+ db: QueryDB | Model,
112
+ ) -> set[TimeIndex]:
113
+ """
114
+ Get set of timeindexes behind profile.
115
+
116
+ Can be used to run optimized queries, i.e. not asking for
117
+ finer time resolutions than necessary.
118
+ """
119
+ if self.has_profile() is None:
120
+ return set()
121
+ s = set()
122
+ if self._conversion is not None:
123
+ s.update(self._conversion.get_profile_timeindex_set(db))
124
+ if self._loss is not None:
125
+ s.update(self._loss.get_profile_timeindex_set(db))
126
+ if self._efficiency is not None:
127
+ s.update(self._efficiency.get_profile_timeindex_set(db))
128
+ return s
129
+
130
+ def get_scenario_vector( # noqa: C901, PLR0915
131
+ self,
132
+ db: QueryDB | Model,
133
+ scenario_horizon: FixedFrequencyTimeIndex,
134
+ level_period: SinglePeriodTimeIndex,
135
+ unit: str | None,
136
+ is_float32: bool = True,
137
+ ) -> NDArray:
138
+ """Return vector with values along the given scenario horizon using level over level_period."""
139
+ conversion_vector = None
140
+ efficiency_vector = None
141
+ loss_vector = None
142
+ conversion_value = None
143
+ efficiency_value = None
144
+ loss_value = None
145
+
146
+ if self._conversion is not None:
147
+ if self._conversion.has_profile():
148
+ conversion_vector = self._conversion.get_scenario_vector(
149
+ db=db,
150
+ scenario_horizon=scenario_horizon,
151
+ level_period=level_period,
152
+ unit=unit,
153
+ is_float32=is_float32,
154
+ )
155
+ elif self._conversion.has_level():
156
+ conversion_value = self._conversion.get_data_value(
157
+ db=db,
158
+ scenario_horizon=scenario_horizon,
159
+ level_period=level_period,
160
+ unit=unit,
161
+ )
162
+ conversion_value = float(conversion_value)
163
+
164
+ if self._efficiency is not None:
165
+ if self._efficiency.has_profile():
166
+ efficiency_vector = self._efficiency.get_scenario_vector(
167
+ db=db,
168
+ scenario_horizon=scenario_horizon,
169
+ level_period=level_period,
170
+ unit=None,
171
+ is_float32=is_float32,
172
+ )
173
+ elif self._efficiency.has_level():
174
+ efficiency_value = self._efficiency.get_data_value(
175
+ db=db,
176
+ scenario_horizon=scenario_horizon,
177
+ level_period=level_period,
178
+ unit=None,
179
+ )
180
+ efficiency_value = float(efficiency_value)
181
+
182
+ if self._loss is not None:
183
+ if self._loss.has_profile():
184
+ loss_vector = self._loss.get_scenario_vector(
185
+ db=db,
186
+ scenario_horizon=scenario_horizon,
187
+ level_period=level_period,
188
+ unit=None,
189
+ is_float32=is_float32,
190
+ )
191
+ elif self._loss.has_level():
192
+ loss_value = self._loss.get_data_value(
193
+ db=db,
194
+ scenario_horizon=scenario_horizon,
195
+ level_period=level_period,
196
+ unit=None,
197
+ )
198
+ loss_value = float(loss_value)
199
+
200
+ if conversion_value is not None:
201
+ assert conversion_value >= 0, f"Arrow with invalid conversion ({conversion_value}): {self}"
202
+ out = conversion_value
203
+ else:
204
+ out = 1.0
205
+
206
+ if efficiency_value is not None:
207
+ assert efficiency_value > 0, f"Arrow with invalid efficiency ({efficiency_value}): {self}"
208
+ out = out / efficiency_value
209
+
210
+ if loss_value is not None:
211
+ assert loss_value >= 0 or loss_value < 1, f"Arrow with invalid loss ({loss_value}): {self}"
212
+ out = out - out * loss_value
213
+
214
+ if conversion_vector is not None:
215
+ np.multiply(conversion_vector, out, out=conversion_vector)
216
+ out = conversion_vector
217
+
218
+ if efficiency_vector is not None:
219
+ if isinstance(out, float):
220
+ np.divide(out, efficiency_vector, out=efficiency_vector)
221
+ out = efficiency_vector
222
+ else:
223
+ np.divide(out, efficiency_vector, out=out)
224
+
225
+ if loss_vector is not None:
226
+ if isinstance(out, float):
227
+ np.multiply(out, loss_vector, out=loss_vector)
228
+ np.subtract(out, loss_vector, out=loss_vector)
229
+ out = loss_vector
230
+ else:
231
+ np.multiply(out, loss_vector, out=loss_vector)
232
+ np.subtract(out, loss_vector, out=out)
233
+
234
+ if isinstance(out, float):
235
+ num_periods = scenario_horizon.get_num_periods()
236
+ vector = np.ones(num_periods, dtype=np.float32 if is_float32 else np.float64)
237
+ vector.fill(out)
238
+ return vector
239
+
240
+ return out
241
+
242
+ def get_data_value(
243
+ self,
244
+ db: QueryDB | Model,
245
+ scenario_horizon: FixedFrequencyTimeIndex,
246
+ level_period: SinglePeriodTimeIndex,
247
+ unit: str | None,
248
+ is_max_level: bool = False,
249
+ ) -> float:
250
+ """Return float for level_period."""
251
+ conversion_value = None
252
+ efficiency_value = None
253
+ loss_value = None
254
+
255
+ if self._conversion is not None and self._conversion.has_level():
256
+ conversion_value = self._conversion.get_data_value(
257
+ db=db,
258
+ scenario_horizon=scenario_horizon,
259
+ level_period=level_period,
260
+ unit=unit,
261
+ is_max_level=is_max_level,
262
+ )
263
+ conversion_value = float(conversion_value)
264
+
265
+ if self._efficiency is not None and self._efficiency.has_level():
266
+ efficiency_value = self._efficiency.get_data_value(
267
+ db=db,
268
+ scenario_horizon=scenario_horizon,
269
+ level_period=level_period,
270
+ unit=None,
271
+ is_max_level=is_max_level,
272
+ )
273
+ efficiency_value = float(efficiency_value)
274
+
275
+ if self._loss is not None and self._loss.has_level():
276
+ loss_value = self._loss.get_data_value(
277
+ db=db,
278
+ scenario_horizon=scenario_horizon,
279
+ level_period=level_period,
280
+ unit=None,
281
+ is_max_level=is_max_level,
282
+ )
283
+ loss_value = float(loss_value)
284
+
285
+ if conversion_value is not None:
286
+ assert conversion_value >= 0, f"Arrow with invalid conversion ({conversion_value}): {self}"
287
+ out = conversion_value
288
+ else:
289
+ out = 1.0
290
+
291
+ if efficiency_value is not None:
292
+ assert efficiency_value > 0, f"Arrow with invalid efficiency ({efficiency_value}): {self}"
293
+ out = out / efficiency_value
294
+
295
+ if loss_value is not None:
296
+ assert loss_value >= 0 or loss_value < 1, f"Arrow with invalid loss ({loss_value}): {self}"
297
+ out = out - out * loss_value
298
+
299
+ return out
300
+
301
+ def add_loaders(self, loaders: set[Loader]) -> None:
302
+ """Add all loaders stored in attributes to loaders."""
303
+ from framcore.utils import add_loaders_if
304
+
305
+ add_loaders_if(loaders, self.get_conversion())
306
+ add_loaders_if(loaders, self.get_loss())
307
+ add_loaders_if(loaders, self.get_efficiency())
@@ -0,0 +1,90 @@
1
+ """ElasticDemand attribute class."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import TYPE_CHECKING
6
+
7
+ from framcore import Base
8
+ from framcore.attributes import Elasticity, Price
9
+
10
+ # TODO: Discuss price interval validation. Should we check in init? How?
11
+
12
+ if TYPE_CHECKING:
13
+ from framcore.loaders import Loader
14
+
15
+
16
+ class ElasticDemand(Base):
17
+ """ElasticDemand class representing the price elasticity of a demand Component."""
18
+
19
+ def __init__(
20
+ self,
21
+ price_elasticity: Elasticity,
22
+ min_price: Price,
23
+ normal_price: Price,
24
+ max_price: Price,
25
+ ) -> None:
26
+ """
27
+ Initialize the ElasticDemand class.
28
+
29
+ Args:
30
+ price_elasticity (Elasticity): The price elasticity factor of the demand consumer.
31
+ min_price (Price): Lower limit for price elasticity.
32
+ normal_price (Price): Price for which the demand is inelastic. If it deviates from this price, the consumer will adjust
33
+ it's consumption according to the _price_elasticity factor.
34
+ max_price (Price): Upper limit for price elasticity / reservation price level.
35
+
36
+ """
37
+ self._check_type(price_elasticity, Elasticity)
38
+ self._check_type(min_price, Price)
39
+ self._check_type(normal_price, Price)
40
+ self._check_type(max_price, Price)
41
+
42
+ self._price_elasticity = price_elasticity
43
+ self._min_price = min_price
44
+ self._normal_price = normal_price
45
+ self._max_price = max_price
46
+
47
+ def get_price_elasticity(self) -> Elasticity:
48
+ """Get the price elasticity."""
49
+ return self._price_elasticity
50
+
51
+ def set_price_elasticity(self, elasticity: Price) -> None:
52
+ """Set the price elasticity."""
53
+ self._check_type(elasticity, Elasticity)
54
+ self._price_elasticity = elasticity
55
+
56
+ def get_min_price(self) -> Price:
57
+ """Get the minimum price."""
58
+ return self._min_price
59
+
60
+ def set_min_price(self, min_price: Price) -> None:
61
+ """Set the minimum price."""
62
+ self._check_type(min_price, Price)
63
+ self._min_price = min_price
64
+
65
+ def get_normal_price(self) -> Price:
66
+ """Get the normal price."""
67
+ return self._normal_price
68
+
69
+ def set_normal_price(self, normal_price: Price) -> None:
70
+ """Set the normal price."""
71
+ self._check_type(normal_price, Price)
72
+ self._normal_price = normal_price
73
+
74
+ def get_max_price(self) -> Price:
75
+ """Get the maximum price."""
76
+ return self._max_price
77
+
78
+ def set_max_price(self, max_price: Price) -> None:
79
+ """Set the maximum price."""
80
+ self._check_type(max_price, Price)
81
+ self._max_price = max_price
82
+
83
+ def add_loaders(self, loaders: set[Loader]) -> None:
84
+ """Add all loaders stored in attributes to loaders."""
85
+ from framcore.utils import add_loaders_if
86
+
87
+ add_loaders_if(loaders, self._normal_price)
88
+ add_loaders_if(loaders, self._price_elasticity)
89
+ add_loaders_if(loaders, self._max_price)
90
+ add_loaders_if(loaders, self._min_price)
@@ -0,0 +1,23 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import TYPE_CHECKING
4
+
5
+ from framcore import Base
6
+
7
+ if TYPE_CHECKING:
8
+ from framcore.loaders import Loader
9
+
10
+
11
+ class ReservoirCurve(Base):
12
+ """Water level elevation to water volume characteristics for HydroStorage."""
13
+
14
+ # TODO: Implement and comment, also too generic name
15
+
16
+ def __init__(self, value: str | None) -> None:
17
+ """Initialize a ReservoirCurve instance."""
18
+ self._check_type(value, (str, type(None)))
19
+ self._value = value
20
+
21
+ def add_loaders(self, loaders: set[Loader]) -> None:
22
+ """Add all loaders stored in attributes to loaders."""
23
+ return
@@ -0,0 +1,16 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import TYPE_CHECKING
4
+
5
+ if TYPE_CHECKING:
6
+ from framcore.loaders import Loader
7
+
8
+
9
+ class SoftBound:
10
+ """Represents a soft bound attribute. Penalty applied if the bound is violated."""
11
+
12
+ # TODO: Implement and comment
13
+
14
+ def add_loaders(self, loaders: set[Loader]) -> None:
15
+ """Add all loaders stored in attributes to loaders."""
16
+ return
@@ -0,0 +1,65 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import TYPE_CHECKING
4
+
5
+ from framcore import Base
6
+ from framcore.attributes import Cost, Efficiency, Hours, Proportion
7
+ from framcore.fingerprints import Fingerprint
8
+
9
+ if TYPE_CHECKING:
10
+ from framcore.loaders import Loader
11
+
12
+
13
+ class StartUpCost(Base):
14
+ """Represent the costs associated with starting up the operation of a Component."""
15
+
16
+ # TODO: Complete description
17
+
18
+ def __init__(
19
+ self,
20
+ startup_cost: Cost,
21
+ min_stable_load: Proportion,
22
+ start_hours: Hours,
23
+ part_load_efficiency: Efficiency,
24
+ ) -> None:
25
+ """
26
+ Initialize the StartUpCost class.
27
+
28
+ Args:
29
+ startup_cost (Cost): _description_
30
+ min_stable_load (Proportion): _description_
31
+ start_hours (Hours): _description_
32
+ part_load_efficiency (Efficiency): _description_
33
+
34
+ """
35
+ self._check_type(startup_cost, Cost)
36
+ self._check_type(min_stable_load, Proportion)
37
+ self._check_type(start_hours, Hours)
38
+ self._check_type(part_load_efficiency, Efficiency)
39
+
40
+ self._startup_cost = startup_cost
41
+ self._min_stable_load = min_stable_load
42
+ self._start_hours = start_hours
43
+ self._part_load_efficiency = part_load_efficiency
44
+
45
+ def get_startupcost(self) -> Cost:
46
+ """Get the startup cost."""
47
+ return self._startup_cost
48
+
49
+ def set_startupcost(self, startupcost: Cost) -> None:
50
+ """Set the startup cost."""
51
+ self._check_type(startupcost, Cost)
52
+ self._startup_cost = startupcost
53
+
54
+ def get_fingerprint(self) -> Fingerprint:
55
+ """Get the fingerprint of the startup cost."""
56
+ return self.get_fingerprint_default()
57
+
58
+ def add_loaders(self, loaders: set[Loader]) -> None:
59
+ """Get all loaders stored in attributes."""
60
+ from framcore.utils import add_loaders_if
61
+
62
+ add_loaders_if(loaders, self.get_startupcost())
63
+ add_loaders_if(loaders, self._start_hours)
64
+ add_loaders_if(loaders, self._min_stable_load)
65
+ add_loaders_if(loaders, self._part_load_efficiency)
@@ -0,0 +1,158 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import TYPE_CHECKING
4
+
5
+ from framcore import Base
6
+ from framcore.attributes import Loss, ObjectiveCoefficient, ReservoirCurve, SoftBound, StockVolume, TargetBound
7
+
8
+ if TYPE_CHECKING:
9
+ from framcore.loaders import Loader
10
+
11
+
12
+ class Storage(Base):
13
+ """
14
+ Represents all types of storage this system supports.
15
+
16
+ Subclasses are supposed to restrict which attributes that are used, not add more.
17
+ """
18
+
19
+ def __init__(
20
+ self,
21
+ capacity: StockVolume,
22
+ volume: StockVolume | None = None,
23
+ loss: Loss | None = None, # TODO: Should be loss percentage per time.
24
+ reservoir_curve: ReservoirCurve | None = None,
25
+ max_soft_bound: SoftBound | None = None,
26
+ min_soft_bound: SoftBound | None = None,
27
+ target_bound: TargetBound | None = None,
28
+ initial_storage_percentage: float | None = None,
29
+ ) -> None:
30
+ """
31
+ Create new storage.
32
+
33
+ Args:
34
+ capacity (StockVolume): Storage capacity.
35
+ volume (StockVolume | None, optional): Storage filling (actual/result). Defaults to None.
36
+ loss (Loss | None, optional): Loss percentage per time. Defaults to None.
37
+ reservoir_curve (ReservoirCurve | None, optional): Water level elevation to water volume for HydroStorage. Defaults to None.
38
+ max_soft_bound (SoftBound | None, optional): Upper soft boundary that is penalized if broken. Defaults to None.
39
+ min_soft_bound (SoftBound | None, optional): Lower soft boundary that is penalized if broken. Defaults to None.
40
+ target_bound (TargetBound | None, optional): Target filling, can be penalized if deviation. Defaults to None.
41
+ initial_storage_percentage (float | None, optional): Initial storage filling percentage at start of simulation. Defaults to None.
42
+
43
+ """
44
+ super().__init__()
45
+
46
+ self._check_type(capacity, StockVolume)
47
+ self._check_type(volume, (StockVolume, type(None)))
48
+ self._check_type(loss, (StockVolume, type(None)))
49
+ self._check_type(reservoir_curve, (ReservoirCurve, type(None)))
50
+ self._check_type(max_soft_bound, (SoftBound, type(None)))
51
+ self._check_type(min_soft_bound, (SoftBound, type(None)))
52
+ self._check_type(target_bound, (TargetBound, type(None)))
53
+ self._check_type(initial_storage_percentage, (float, type(None)))
54
+
55
+ if initial_storage_percentage is not None:
56
+ self._check_float(initial_storage_percentage, lower_bound=0.0, upper_bound=1.0)
57
+
58
+ self._capacity = capacity
59
+
60
+ self._loss = loss
61
+ self._reservoir_curve = reservoir_curve
62
+ self._max_soft_bound = max_soft_bound
63
+ self._min_soft_bound = min_soft_bound
64
+ self._target_bound = target_bound
65
+ self._initial_storage_percentage = initial_storage_percentage
66
+
67
+ self._cost_terms: dict[str, ObjectiveCoefficient] = dict()
68
+
69
+ if volume is None:
70
+ volume = StockVolume()
71
+ self._volume = volume
72
+
73
+ def get_capacity(self) -> StockVolume:
74
+ """Get the capacity."""
75
+ return self._capacity
76
+
77
+ def get_volume(self) -> StockVolume:
78
+ """Get the volume."""
79
+ return self._volume
80
+
81
+ def add_cost_term(self, key: str, cost_term: ObjectiveCoefficient) -> None:
82
+ """Add a cost term."""
83
+ self._check_type(key, str)
84
+ self._check_type(cost_term, ObjectiveCoefficient)
85
+ self._cost_terms[key] = cost_term
86
+
87
+ def get_cost_terms(self) -> dict[str, ObjectiveCoefficient]:
88
+ """Get the cost terms."""
89
+ return self._cost_terms
90
+
91
+ def get_loss(self) -> Loss | None:
92
+ """Get the loss."""
93
+ return self._loss
94
+
95
+ def set_loss(self, value: Loss | None) -> None:
96
+ """Set the loss."""
97
+ self._check_type(value, (Loss, type(None)))
98
+ self._loss = value
99
+
100
+ def get_reservoir_curve(self) -> ReservoirCurve | None:
101
+ """Get the reservoir curve."""
102
+ return self._reservoir_curve
103
+
104
+ def set_reservoir_curve(self, value: ReservoirCurve | None) -> None:
105
+ """Set the reservoir curve."""
106
+ self._check_type(value, (ReservoirCurve, type(None)))
107
+ self._reservoir_curve = value
108
+
109
+ def get_max_soft_bound(self) -> SoftBound | None:
110
+ """Get the max soft bound."""
111
+ return self._max_soft_bound
112
+
113
+ def set_max_soft_bound(self, value: SoftBound | None) -> None:
114
+ """Set the max soft bound."""
115
+ self._check_type(value, (SoftBound, type(None)))
116
+ self._max_soft_bound = value
117
+
118
+ def get_min_soft_bound(self) -> SoftBound | None:
119
+ """Get the min soft bound."""
120
+ return self._min_soft_bound
121
+
122
+ def set_min_soft_bound(self, value: SoftBound | None) -> None:
123
+ """Set the min soft bound."""
124
+ self._check_type(value, (SoftBound, type(None)))
125
+ self._min_soft_bound = value
126
+
127
+ def get_target_bound(self) -> TargetBound | None:
128
+ """Get the target bound."""
129
+ return self._target_bound
130
+
131
+ def set_target_bound(self, value: TargetBound | None) -> None:
132
+ """Set the target bound."""
133
+ self._check_type(value, (TargetBound, type(None)))
134
+ self._target_bound = value
135
+
136
+ def get_initial_storage_percentage(self) -> float | None:
137
+ """Get the initial storage percentage (float in [0, 1])."""
138
+ return self._initial_storage_percentage
139
+
140
+ def set_initial_storage_percentage(self, value: float) -> None:
141
+ """Set the initial storage percentage (float in [0, 1])."""
142
+ self._check_float(value, lower_bound=0.0, upper_bound=1.0)
143
+ self._initial_storage_percentage = value
144
+
145
+ def add_loaders(self, loaders: set[Loader]) -> None:
146
+ """Add all loaders stored in attributes to loaders."""
147
+ from framcore.utils import add_loaders_if
148
+
149
+ add_loaders_if(loaders, self.get_capacity())
150
+ add_loaders_if(loaders, self.get_loss())
151
+ add_loaders_if(loaders, self.get_volume())
152
+ add_loaders_if(loaders, self.get_max_soft_bound())
153
+ add_loaders_if(loaders, self.get_min_soft_bound())
154
+ add_loaders_if(loaders, self.get_reservoir_curve())
155
+ add_loaders_if(loaders, self.get_target_bound())
156
+
157
+ for cost in self.get_cost_terms().values():
158
+ add_loaders_if(loaders, cost)
@@ -0,0 +1,16 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import TYPE_CHECKING
4
+
5
+ if TYPE_CHECKING:
6
+ from framcore.loaders import Loader
7
+
8
+
9
+ class TargetBound:
10
+ """Target boundary attribute. Can be penalized if deviation from target."""
11
+
12
+ # TODO: Implement and comment
13
+
14
+ def add_loaders(self, loaders: set[Loader]) -> None:
15
+ """Add all loaders stored in attributes to loaders."""
16
+ return