fram-core 0.0.0__py3-none-any.whl → 0.1.0a1__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.0a1.dist-info/METADATA +41 -0
  2. fram_core-0.1.0a1.dist-info/RECORD +100 -0
  3. {fram_core-0.0.0.dist-info → fram_core-0.1.0a1.dist-info}/WHEEL +1 -2
  4. fram_core-0.1.0a1.dist-info/licenses/LICENSE.md +8 -0
  5. framcore/Base.py +142 -0
  6. framcore/Model.py +73 -0
  7. framcore/__init__.py +9 -0
  8. framcore/aggregators/Aggregator.py +153 -0
  9. framcore/aggregators/HydroAggregator.py +837 -0
  10. framcore/aggregators/NodeAggregator.py +495 -0
  11. framcore/aggregators/WindSolarAggregator.py +323 -0
  12. framcore/aggregators/__init__.py +13 -0
  13. framcore/aggregators/_utils.py +184 -0
  14. framcore/attributes/Arrow.py +305 -0
  15. framcore/attributes/ElasticDemand.py +90 -0
  16. framcore/attributes/ReservoirCurve.py +37 -0
  17. framcore/attributes/SoftBound.py +19 -0
  18. framcore/attributes/StartUpCost.py +54 -0
  19. framcore/attributes/Storage.py +146 -0
  20. framcore/attributes/TargetBound.py +18 -0
  21. framcore/attributes/__init__.py +65 -0
  22. framcore/attributes/hydro/HydroBypass.py +42 -0
  23. framcore/attributes/hydro/HydroGenerator.py +83 -0
  24. framcore/attributes/hydro/HydroPump.py +156 -0
  25. framcore/attributes/hydro/HydroReservoir.py +27 -0
  26. framcore/attributes/hydro/__init__.py +13 -0
  27. framcore/attributes/level_profile_attributes.py +714 -0
  28. framcore/components/Component.py +112 -0
  29. framcore/components/Demand.py +130 -0
  30. framcore/components/Flow.py +167 -0
  31. framcore/components/HydroModule.py +330 -0
  32. framcore/components/Node.py +76 -0
  33. framcore/components/Thermal.py +204 -0
  34. framcore/components/Transmission.py +183 -0
  35. framcore/components/_PowerPlant.py +81 -0
  36. framcore/components/__init__.py +22 -0
  37. framcore/components/wind_solar.py +67 -0
  38. framcore/curves/Curve.py +44 -0
  39. framcore/curves/LoadedCurve.py +155 -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 +490 -0
  44. framcore/expressions/__init__.py +28 -0
  45. framcore/expressions/_get_constant_from_expr.py +483 -0
  46. framcore/expressions/_time_vector_operations.py +615 -0
  47. framcore/expressions/_utils.py +73 -0
  48. framcore/expressions/queries.py +423 -0
  49. framcore/expressions/units.py +207 -0
  50. framcore/fingerprints/__init__.py +11 -0
  51. framcore/fingerprints/fingerprint.py +293 -0
  52. framcore/juliamodels/JuliaModel.py +161 -0
  53. framcore/juliamodels/__init__.py +7 -0
  54. framcore/loaders/__init__.py +10 -0
  55. framcore/loaders/loaders.py +407 -0
  56. framcore/metadata/Div.py +73 -0
  57. framcore/metadata/ExprMeta.py +50 -0
  58. framcore/metadata/LevelExprMeta.py +17 -0
  59. framcore/metadata/Member.py +55 -0
  60. framcore/metadata/Meta.py +44 -0
  61. framcore/metadata/__init__.py +15 -0
  62. framcore/populators/Populator.py +108 -0
  63. framcore/populators/__init__.py +7 -0
  64. framcore/querydbs/CacheDB.py +50 -0
  65. framcore/querydbs/ModelDB.py +34 -0
  66. framcore/querydbs/QueryDB.py +45 -0
  67. framcore/querydbs/__init__.py +11 -0
  68. framcore/solvers/Solver.py +48 -0
  69. framcore/solvers/SolverConfig.py +272 -0
  70. framcore/solvers/__init__.py +9 -0
  71. framcore/timeindexes/AverageYearRange.py +20 -0
  72. framcore/timeindexes/ConstantTimeIndex.py +17 -0
  73. framcore/timeindexes/DailyIndex.py +21 -0
  74. framcore/timeindexes/FixedFrequencyTimeIndex.py +762 -0
  75. framcore/timeindexes/HourlyIndex.py +21 -0
  76. framcore/timeindexes/IsoCalendarDay.py +31 -0
  77. framcore/timeindexes/ListTimeIndex.py +197 -0
  78. framcore/timeindexes/ModelYear.py +17 -0
  79. framcore/timeindexes/ModelYears.py +18 -0
  80. framcore/timeindexes/OneYearProfileTimeIndex.py +21 -0
  81. framcore/timeindexes/ProfileTimeIndex.py +32 -0
  82. framcore/timeindexes/SinglePeriodTimeIndex.py +37 -0
  83. framcore/timeindexes/TimeIndex.py +90 -0
  84. framcore/timeindexes/WeeklyIndex.py +21 -0
  85. framcore/timeindexes/__init__.py +36 -0
  86. framcore/timevectors/ConstantTimeVector.py +135 -0
  87. framcore/timevectors/LinearTransformTimeVector.py +114 -0
  88. framcore/timevectors/ListTimeVector.py +123 -0
  89. framcore/timevectors/LoadedTimeVector.py +104 -0
  90. framcore/timevectors/ReferencePeriod.py +41 -0
  91. framcore/timevectors/TimeVector.py +94 -0
  92. framcore/timevectors/__init__.py +17 -0
  93. framcore/utils/__init__.py +36 -0
  94. framcore/utils/get_regional_volumes.py +369 -0
  95. framcore/utils/get_supported_components.py +60 -0
  96. framcore/utils/global_energy_equivalent.py +46 -0
  97. framcore/utils/isolate_subnodes.py +163 -0
  98. framcore/utils/loaders.py +97 -0
  99. framcore/utils/node_flow_utils.py +236 -0
  100. framcore/utils/storage_subsystems.py +107 -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,305 @@
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
+ coefficient = conversion * (1 / efficiency) * (1 - loss)
23
+ """
24
+
25
+ def __init__(
26
+ self,
27
+ node: str,
28
+ is_ingoing: bool,
29
+ conversion: Conversion | None = None,
30
+ efficiency: Efficiency | None = None,
31
+ loss: Loss | None = None,
32
+ ) -> None:
33
+ """Initialize the Arrow class."""
34
+ self._check_type(node, str)
35
+ self._check_type(is_ingoing, bool)
36
+ self._check_type(conversion, (Conversion, type(None)))
37
+ self._check_type(efficiency, (Efficiency, type(None)))
38
+ self._check_type(loss, (Loss, type(None)))
39
+ self._node = node
40
+ self._is_ingoing = is_ingoing
41
+ self._conversion = conversion
42
+ self._efficiency = efficiency
43
+ self._loss = loss
44
+
45
+ def get_node(self) -> str:
46
+ """Get the node the arrow is pointing to."""
47
+ return self._node
48
+
49
+ def set_node(self, node: str) -> None:
50
+ """Set the node the arrow is pointing to."""
51
+ self._check_type(node, str)
52
+ self._node = node
53
+
54
+ def is_ingoing(self) -> bool:
55
+ """
56
+ Return True if arrow is ingoing.
57
+
58
+ Ingoing means the flow variable supplies to node.
59
+ Outgoing means the flow variable takes out of node.
60
+ """
61
+ return self._is_ingoing
62
+
63
+ def get_conversion(self) -> Conversion | None:
64
+ """Get the conversion."""
65
+ return self._conversion
66
+
67
+ def set_conversion(self, value: Conversion | None) -> None:
68
+ """Set the conversion."""
69
+ self._check_type(value, Conversion, type(None))
70
+ self._conversion = value
71
+
72
+ def get_efficiency(self) -> Efficiency | None:
73
+ """Get the efficiency."""
74
+ return self._efficiency
75
+
76
+ def set_efficiency(self, value: Efficiency | None) -> None:
77
+ """Set the efficiency."""
78
+ self._check_type(value, Efficiency, type(None))
79
+ self._efficiency = value
80
+
81
+ def get_loss(self) -> Loss | None:
82
+ """Get the loss."""
83
+ return self._loss
84
+
85
+ def set_loss(self, value: Loss | None) -> None:
86
+ """Set the loss."""
87
+ self._check_type(value, Loss, type(None))
88
+ self._loss = value
89
+
90
+ def has_profile(self) -> bool:
91
+ """Return True if any of conversion, efficiency or loss has profile."""
92
+ if self._conversion is not None and self._conversion.has_profile():
93
+ return True
94
+ if self._efficiency is not None and self._efficiency.has_profile():
95
+ return True
96
+ return bool(self._loss is not None and self._loss.has_profile())
97
+
98
+ def get_conversion_unit_set(
99
+ self,
100
+ db: QueryDB | Model,
101
+ ) -> set[str]:
102
+ """Get set of units behind conversion level expr (if any)."""
103
+ if self._conversion is None:
104
+ return set()
105
+ return self._conversion.get_level_unit_set()
106
+
107
+ def get_profile_timeindex_set(
108
+ self,
109
+ db: QueryDB | Model,
110
+ ) -> set[TimeIndex]:
111
+ """
112
+ Get set of timeindexes behind profile.
113
+
114
+ Can be used to run optimized queries, i.e. not asking for
115
+ finer time resolutions than necessary.
116
+ """
117
+ if self.has_profile() is None:
118
+ return set()
119
+ s = set()
120
+ if self._conversion is not None:
121
+ s.update(self._conversion.get_profile_timeindex_set(db))
122
+ if self._loss is not None:
123
+ s.update(self._loss.get_profile_timeindex_set(db))
124
+ if self._efficiency is not None:
125
+ s.update(self._efficiency.get_profile_timeindex_set(db))
126
+ return s
127
+
128
+ def get_scenario_vector(
129
+ self,
130
+ db: QueryDB | Model,
131
+ scenario_horizon: FixedFrequencyTimeIndex,
132
+ level_period: SinglePeriodTimeIndex,
133
+ unit: str | None,
134
+ is_float32: bool = True,
135
+ ) -> NDArray:
136
+ """Return vector with values along the given scenario horizon using level over level_period."""
137
+ conversion_vector = None
138
+ efficiency_vector = None
139
+ loss_vector = None
140
+ conversion_value = None
141
+ efficiency_value = None
142
+ loss_value = None
143
+
144
+ if self._conversion is not None:
145
+ if self._conversion.has_profile():
146
+ conversion_vector = self._conversion.get_scenario_vector(
147
+ db=db,
148
+ scenario_horizon=scenario_horizon,
149
+ level_period=level_period,
150
+ unit=unit,
151
+ is_float32=is_float32,
152
+ )
153
+ elif self._conversion.has_level():
154
+ conversion_value = self._conversion.get_data_value(
155
+ db=db,
156
+ scenario_horizon=scenario_horizon,
157
+ level_period=level_period,
158
+ unit=unit,
159
+ )
160
+ conversion_value = float(conversion_value)
161
+
162
+ if self._efficiency is not None:
163
+ if self._efficiency.has_profile():
164
+ efficiency_vector = self._efficiency.get_scenario_vector(
165
+ db=db,
166
+ scenario_horizon=scenario_horizon,
167
+ level_period=level_period,
168
+ unit=None,
169
+ is_float32=is_float32,
170
+ )
171
+ elif self._efficiency.has_level():
172
+ efficiency_value = self._efficiency.get_data_value(
173
+ db=db,
174
+ scenario_horizon=scenario_horizon,
175
+ level_period=level_period,
176
+ unit=None,
177
+ )
178
+ efficiency_value = float(efficiency_value)
179
+
180
+ if self._loss is not None:
181
+ if self._loss.has_profile():
182
+ loss_vector = self._loss.get_scenario_vector(
183
+ db=db,
184
+ scenario_horizon=scenario_horizon,
185
+ level_period=level_period,
186
+ unit=None,
187
+ is_float32=is_float32,
188
+ )
189
+ elif self._loss.has_level():
190
+ loss_value = self._loss.get_data_value(
191
+ db=db,
192
+ scenario_horizon=scenario_horizon,
193
+ level_period=level_period,
194
+ unit=None,
195
+ )
196
+ loss_value = float(loss_value)
197
+
198
+ if conversion_value is not None:
199
+ assert conversion_value >= 0, f"Arrow with invalid conversion ({conversion_value}): {self}"
200
+ out = conversion_value
201
+ else:
202
+ out = 1.0
203
+
204
+ if efficiency_value is not None:
205
+ assert efficiency_value > 0, f"Arrow with invalid efficiency ({efficiency_value}): {self}"
206
+ out = out / efficiency_value
207
+
208
+ if loss_value is not None:
209
+ assert loss_value >= 0 or loss_value < 1, f"Arrow with invalid loss ({loss_value}): {self}"
210
+ out = out - out * loss_value
211
+
212
+ if conversion_vector is not None:
213
+ np.multiply(conversion_vector, out, out=conversion_vector)
214
+ out = conversion_vector
215
+
216
+ if efficiency_vector is not None:
217
+ if isinstance(out, float):
218
+ np.divide(out, efficiency_vector, out=efficiency_vector)
219
+ out = efficiency_vector
220
+ else:
221
+ np.divide(out, efficiency_vector, out=out)
222
+
223
+ if loss_vector is not None:
224
+ if isinstance(out, float):
225
+ np.multiply(out, loss_vector, out=loss_vector)
226
+ np.subtract(out, loss_vector, out=loss_vector)
227
+ out = loss_vector
228
+ else:
229
+ np.multiply(out, loss_vector, out=loss_vector)
230
+ np.subtract(out, loss_vector, out=out)
231
+
232
+ if isinstance(out, float):
233
+ num_periods = scenario_horizon.get_num_periods()
234
+ vector = np.ones(num_periods, dtype=np.float32 if is_float32 else np.float64)
235
+ vector.fill(out)
236
+ return vector
237
+
238
+ return out
239
+
240
+ def get_data_value(
241
+ self,
242
+ db: QueryDB | Model,
243
+ scenario_horizon: FixedFrequencyTimeIndex,
244
+ level_period: SinglePeriodTimeIndex,
245
+ unit: str | None,
246
+ is_max_level: bool = False,
247
+ ) -> float:
248
+ """Return float for level_period."""
249
+ conversion_value = None
250
+ efficiency_value = None
251
+ loss_value = None
252
+
253
+ if self._conversion is not None and self._conversion.has_level():
254
+ conversion_value = self._conversion.get_data_value(
255
+ db=db,
256
+ scenario_horizon=scenario_horizon,
257
+ level_period=level_period,
258
+ unit=unit,
259
+ is_max_level=is_max_level,
260
+ )
261
+ conversion_value = float(conversion_value)
262
+
263
+ if self._efficiency is not None and self._efficiency.has_level():
264
+ efficiency_value = self._efficiency.get_data_value(
265
+ db=db,
266
+ scenario_horizon=scenario_horizon,
267
+ level_period=level_period,
268
+ unit=None,
269
+ is_max_level=is_max_level,
270
+ )
271
+ efficiency_value = float(efficiency_value)
272
+
273
+ if self._loss is not None and self._loss.has_level():
274
+ loss_value = self._loss.get_data_value(
275
+ db=db,
276
+ scenario_horizon=scenario_horizon,
277
+ level_period=level_period,
278
+ unit=None,
279
+ is_max_level=is_max_level,
280
+ )
281
+ loss_value = float(loss_value)
282
+
283
+ if conversion_value is not None:
284
+ assert conversion_value >= 0, f"Arrow with invalid conversion ({conversion_value}): {self}"
285
+ out = conversion_value
286
+ else:
287
+ out = 1.0
288
+
289
+ if efficiency_value is not None:
290
+ assert efficiency_value > 0, f"Arrow with invalid efficiency ({efficiency_value}): {self}"
291
+ out = out / efficiency_value
292
+
293
+ if loss_value is not None:
294
+ assert loss_value >= 0 or loss_value < 1, f"Arrow with invalid loss ({loss_value}): {self}"
295
+ out = out - out * loss_value
296
+
297
+ return out
298
+
299
+ def add_loaders(self, loaders: set[Loader]) -> None:
300
+ """Add all loaders stored in attributes to loaders."""
301
+ from framcore.utils import add_loaders_if # noqa: PLC0415
302
+
303
+ add_loaders_if(loaders, self.get_conversion())
304
+ add_loaders_if(loaders, self.get_loss())
305
+ 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
+ """
18
+ ElasticDemand class representing the price elasticity of a demand Component.
19
+
20
+ Attributes:
21
+ _price_elasticity: The price elasticity factor of the demand consumer.
22
+ _min_price: Lower limit for price elasticity.
23
+ _normal_price: Price for which the demand is inelastic. If it deviates from this price, the consumer will adjust
24
+ it's consumption according to the _price_elasticity factor.
25
+ _max_price: Upper limit for price elasticity / reservation price level.
26
+
27
+ """
28
+
29
+ def __init__(
30
+ self,
31
+ price_elasticity: Elasticity,
32
+ min_price: Price,
33
+ normal_price: Price,
34
+ max_price: Price,
35
+ ) -> None:
36
+ """Initialize the ElasticDemand class."""
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 # noqa: PLC0415
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,37 @@
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
+ """
13
+ Represents a reservoir curve attribute.
14
+
15
+ Attributes
16
+ ----------
17
+ _value : str | None
18
+ The value representing the reservoir curve.
19
+
20
+ """
21
+
22
+ def __init__(self, value: str | None) -> None:
23
+ """
24
+ Initialize a ReservoirCurve instance.
25
+
26
+ Parameters
27
+ ----------
28
+ value : str | None
29
+ The value representing the reservoir curve.
30
+
31
+ """
32
+ self._check_type(value, (str, type(None)))
33
+ self._value = value
34
+
35
+ def add_loaders(self, loaders: set[Loader]) -> None:
36
+ """Add all loaders stored in attributes to loaders."""
37
+ return
@@ -0,0 +1,19 @@
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
+ """
11
+ Represents a soft bound attribute.
12
+
13
+ This class can be extended to define soft bounds for various parameters.
14
+
15
+ """
16
+
17
+ def add_loaders(self, loaders: set[Loader]) -> None:
18
+ """Add all loaders stored in attributes to loaders."""
19
+ return
@@ -0,0 +1,54 @@
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
+ """StartUpCost class representing the startup cost of a Component."""
15
+
16
+ def __init__(
17
+ self,
18
+ startup_cost: Cost,
19
+ min_stable_load: Proportion,
20
+ start_hours: Hours,
21
+ part_load_efficiency: Efficiency,
22
+ ) -> None:
23
+ """Initialize the StartUpCost class."""
24
+ self._check_type(startup_cost, Cost)
25
+ self._check_type(min_stable_load, Proportion)
26
+ self._check_type(start_hours, Hours)
27
+ self._check_type(part_load_efficiency, Efficiency)
28
+
29
+ self._startup_cost = startup_cost
30
+ self._min_stable_load = min_stable_load
31
+ self._start_hours = start_hours
32
+ self._part_load_efficiency = part_load_efficiency
33
+
34
+ def get_startupcost(self) -> Cost:
35
+ """Get the startup cost."""
36
+ return self._startup_cost
37
+
38
+ def set_startupcost(self, startupcost: Cost) -> None:
39
+ """Set the startup cost."""
40
+ self._check_type(startupcost, Cost)
41
+ self._startup_cost = startupcost
42
+
43
+ def get_fingerprint(self) -> Fingerprint:
44
+ """Get the fingerprint of the startup cost."""
45
+ return self.get_fingerprint_default()
46
+
47
+ def add_loaders(self, loaders: set[Loader]) -> None:
48
+ """Get all loaders stored in attributes."""
49
+ from framcore.utils import add_loaders_if # noqa: PLC0415
50
+
51
+ add_loaders_if(loaders, self.get_startupcost())
52
+ add_loaders_if(loaders, self._start_hours)
53
+ add_loaders_if(loaders, self._min_stable_load)
54
+ add_loaders_if(loaders, self._part_load_efficiency)
@@ -0,0 +1,146 @@
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
17
+ used, not add more.
18
+ """
19
+
20
+ def __init__(
21
+ self,
22
+ capacity: StockVolume,
23
+ volume: StockVolume | None = None,
24
+ loss: Loss | None = None,
25
+ reservoir_curve: ReservoirCurve | None = None,
26
+ max_soft_bound: SoftBound | None = None,
27
+ min_soft_bound: SoftBound | None = None,
28
+ target_bound: TargetBound | None = None,
29
+ initial_storage_percentage: float | None = None,
30
+ ) -> None:
31
+ """Create new storage."""
32
+ super().__init__()
33
+
34
+ self._check_type(capacity, StockVolume)
35
+ self._check_type(volume, (StockVolume, type(None)))
36
+ self._check_type(loss, (StockVolume, type(None)))
37
+ self._check_type(reservoir_curve, (ReservoirCurve, type(None)))
38
+ self._check_type(max_soft_bound, (SoftBound, type(None)))
39
+ self._check_type(min_soft_bound, (SoftBound, type(None)))
40
+ self._check_type(target_bound, (TargetBound, type(None)))
41
+ self._check_type(initial_storage_percentage, (float, type(None)))
42
+
43
+ if initial_storage_percentage is not None:
44
+ self._check_float(initial_storage_percentage, lower_bound=0.0, upper_bound=1.0)
45
+
46
+ self._capacity = capacity
47
+
48
+ self._loss = loss
49
+ self._reservoir_curve = reservoir_curve
50
+ self._max_soft_bound = max_soft_bound
51
+ self._min_soft_bound = min_soft_bound
52
+ self._target_bound = target_bound
53
+ self._initial_storage_percentage = initial_storage_percentage
54
+
55
+ self._cost_terms: dict[str, ObjectiveCoefficient] = dict()
56
+
57
+ if volume is None:
58
+ volume = StockVolume()
59
+ self._volume = volume
60
+
61
+ def get_capacity(self) -> StockVolume:
62
+ """Get the capacity."""
63
+ return self._capacity
64
+
65
+ def get_volume(self) -> StockVolume:
66
+ """Get the volume."""
67
+ return self._volume
68
+
69
+ def add_cost_term(self, key: str, cost_term: ObjectiveCoefficient) -> None:
70
+ """Add a cost term."""
71
+ self._check_type(key, str)
72
+ self._check_type(cost_term, ObjectiveCoefficient)
73
+ self._cost_terms[key] = cost_term
74
+
75
+ def get_cost_terms(self) -> dict[str, ObjectiveCoefficient]:
76
+ """Get the cost terms."""
77
+ return self._cost_terms
78
+
79
+ def get_loss(self) -> Loss | None:
80
+ """Get the loss."""
81
+ return self._loss
82
+
83
+ def set_loss(self, value: Loss | None) -> None:
84
+ """Set the loss."""
85
+ self._check_type(value, (Loss, type(None)))
86
+ self._loss = value
87
+
88
+ def get_reservoir_curve(self) -> ReservoirCurve | None:
89
+ """Get the reservoir curve."""
90
+ return self._reservoir_curve
91
+
92
+ def set_reservoir_curve(self, value: ReservoirCurve | None) -> None:
93
+ """Set the reservoir curve."""
94
+ self._check_type(value, (ReservoirCurve, type(None)))
95
+ self._reservoir_curve = value
96
+
97
+ def get_max_soft_bound(self) -> SoftBound | None:
98
+ """Get the max soft bound."""
99
+ return self._max_soft_bound
100
+
101
+ def set_max_soft_bound(self, value: SoftBound | None) -> None:
102
+ """Set the max soft bound."""
103
+ self._check_type(value, (SoftBound, type(None)))
104
+ self._max_soft_bound = value
105
+
106
+ def get_min_soft_bound(self) -> SoftBound | None:
107
+ """Get the min soft bound."""
108
+ return self._min_soft_bound
109
+
110
+ def set_min_soft_bound(self, value: SoftBound | None) -> None:
111
+ """Set the min soft bound."""
112
+ self._check_type(value, (SoftBound, type(None)))
113
+ self._min_soft_bound = value
114
+
115
+ def get_target_bound(self) -> TargetBound | None:
116
+ """Get the target bound."""
117
+ return self._target_bound
118
+
119
+ def set_target_bound(self, value: TargetBound | None) -> None:
120
+ """Set the target bound."""
121
+ self._check_type(value, (TargetBound, type(None)))
122
+ self._target_bound = value
123
+
124
+ def get_initial_storage_percentage(self) -> float | None:
125
+ """Get the initial storage percentage (float in [0, 1])."""
126
+ return self._initial_storage_percentage
127
+
128
+ def set_initial_storage_percentage(self, value: float) -> None:
129
+ """Set the initial storage percentage (float in [0, 1])."""
130
+ self._check_float(value, lower_bound=0.0, upper_bound=1.0)
131
+ self._initial_storage_percentage = value
132
+
133
+ def add_loaders(self, loaders: set[Loader]) -> None:
134
+ """Add all loaders stored in attributes to loaders."""
135
+ from framcore.utils import add_loaders_if # noqa: PLC0415
136
+
137
+ add_loaders_if(loaders, self.get_capacity())
138
+ add_loaders_if(loaders, self.get_loss())
139
+ add_loaders_if(loaders, self.get_volume())
140
+ add_loaders_if(loaders, self.get_max_soft_bound())
141
+ add_loaders_if(loaders, self.get_min_soft_bound())
142
+ add_loaders_if(loaders, self.get_reservoir_curve())
143
+ add_loaders_if(loaders, self.get_target_bound())
144
+
145
+ for cost in self.get_cost_terms().values():
146
+ add_loaders_if(loaders, cost)
@@ -0,0 +1,18 @@
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
+ """
11
+ Represents a target bound attribute.
12
+
13
+ This class can be extended to define specific bounds for targets in the energy model.
14
+ """
15
+
16
+ def add_loaders(self, loaders: set[Loader]) -> None:
17
+ """Add all loaders stored in attributes to loaders."""
18
+ return