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.
- fram_core-0.1.0a1.dist-info/METADATA +41 -0
- fram_core-0.1.0a1.dist-info/RECORD +100 -0
- {fram_core-0.0.0.dist-info → fram_core-0.1.0a1.dist-info}/WHEEL +1 -2
- fram_core-0.1.0a1.dist-info/licenses/LICENSE.md +8 -0
- framcore/Base.py +142 -0
- framcore/Model.py +73 -0
- framcore/__init__.py +9 -0
- framcore/aggregators/Aggregator.py +153 -0
- framcore/aggregators/HydroAggregator.py +837 -0
- framcore/aggregators/NodeAggregator.py +495 -0
- framcore/aggregators/WindSolarAggregator.py +323 -0
- framcore/aggregators/__init__.py +13 -0
- framcore/aggregators/_utils.py +184 -0
- framcore/attributes/Arrow.py +305 -0
- framcore/attributes/ElasticDemand.py +90 -0
- framcore/attributes/ReservoirCurve.py +37 -0
- framcore/attributes/SoftBound.py +19 -0
- framcore/attributes/StartUpCost.py +54 -0
- framcore/attributes/Storage.py +146 -0
- framcore/attributes/TargetBound.py +18 -0
- framcore/attributes/__init__.py +65 -0
- framcore/attributes/hydro/HydroBypass.py +42 -0
- framcore/attributes/hydro/HydroGenerator.py +83 -0
- framcore/attributes/hydro/HydroPump.py +156 -0
- framcore/attributes/hydro/HydroReservoir.py +27 -0
- framcore/attributes/hydro/__init__.py +13 -0
- framcore/attributes/level_profile_attributes.py +714 -0
- framcore/components/Component.py +112 -0
- framcore/components/Demand.py +130 -0
- framcore/components/Flow.py +167 -0
- framcore/components/HydroModule.py +330 -0
- framcore/components/Node.py +76 -0
- framcore/components/Thermal.py +204 -0
- framcore/components/Transmission.py +183 -0
- framcore/components/_PowerPlant.py +81 -0
- framcore/components/__init__.py +22 -0
- framcore/components/wind_solar.py +67 -0
- framcore/curves/Curve.py +44 -0
- framcore/curves/LoadedCurve.py +155 -0
- framcore/curves/__init__.py +9 -0
- framcore/events/__init__.py +21 -0
- framcore/events/events.py +51 -0
- framcore/expressions/Expr.py +490 -0
- framcore/expressions/__init__.py +28 -0
- framcore/expressions/_get_constant_from_expr.py +483 -0
- framcore/expressions/_time_vector_operations.py +615 -0
- framcore/expressions/_utils.py +73 -0
- framcore/expressions/queries.py +423 -0
- framcore/expressions/units.py +207 -0
- framcore/fingerprints/__init__.py +11 -0
- framcore/fingerprints/fingerprint.py +293 -0
- framcore/juliamodels/JuliaModel.py +161 -0
- framcore/juliamodels/__init__.py +7 -0
- framcore/loaders/__init__.py +10 -0
- framcore/loaders/loaders.py +407 -0
- framcore/metadata/Div.py +73 -0
- framcore/metadata/ExprMeta.py +50 -0
- framcore/metadata/LevelExprMeta.py +17 -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 +48 -0
- framcore/solvers/SolverConfig.py +272 -0
- framcore/solvers/__init__.py +9 -0
- framcore/timeindexes/AverageYearRange.py +20 -0
- framcore/timeindexes/ConstantTimeIndex.py +17 -0
- framcore/timeindexes/DailyIndex.py +21 -0
- framcore/timeindexes/FixedFrequencyTimeIndex.py +762 -0
- framcore/timeindexes/HourlyIndex.py +21 -0
- framcore/timeindexes/IsoCalendarDay.py +31 -0
- framcore/timeindexes/ListTimeIndex.py +197 -0
- framcore/timeindexes/ModelYear.py +17 -0
- framcore/timeindexes/ModelYears.py +18 -0
- framcore/timeindexes/OneYearProfileTimeIndex.py +21 -0
- framcore/timeindexes/ProfileTimeIndex.py +32 -0
- framcore/timeindexes/SinglePeriodTimeIndex.py +37 -0
- framcore/timeindexes/TimeIndex.py +90 -0
- framcore/timeindexes/WeeklyIndex.py +21 -0
- framcore/timeindexes/__init__.py +36 -0
- framcore/timevectors/ConstantTimeVector.py +135 -0
- framcore/timevectors/LinearTransformTimeVector.py +114 -0
- framcore/timevectors/ListTimeVector.py +123 -0
- framcore/timevectors/LoadedTimeVector.py +104 -0
- framcore/timevectors/ReferencePeriod.py +41 -0
- framcore/timevectors/TimeVector.py +94 -0
- framcore/timevectors/__init__.py +17 -0
- framcore/utils/__init__.py +36 -0
- framcore/utils/get_regional_volumes.py +369 -0
- framcore/utils/get_supported_components.py +60 -0
- framcore/utils/global_energy_equivalent.py +46 -0
- framcore/utils/isolate_subnodes.py +163 -0
- framcore/utils/loaders.py +97 -0
- framcore/utils/node_flow_utils.py +236 -0
- framcore/utils/storage_subsystems.py +107 -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,490 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from copy import copy
|
|
4
|
+
from typing import TYPE_CHECKING
|
|
5
|
+
|
|
6
|
+
from framcore import Base
|
|
7
|
+
from framcore.fingerprints import Fingerprint, FingerprintRef
|
|
8
|
+
from framcore.timevectors import ConstantTimeVector, TimeVector
|
|
9
|
+
|
|
10
|
+
if TYPE_CHECKING:
|
|
11
|
+
from framcore.loaders import Loader
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
# TODO: Add Expr.add_many to support faster aggregation expressions.
|
|
15
|
+
class Expr(Base):
|
|
16
|
+
"""Expressions for data manipulation of curves and timevectors."""
|
|
17
|
+
|
|
18
|
+
def __init__(
|
|
19
|
+
self,
|
|
20
|
+
src: str | TimeVector | None = None,
|
|
21
|
+
is_stock: bool = False,
|
|
22
|
+
is_flow: bool = False,
|
|
23
|
+
is_profile: bool = False,
|
|
24
|
+
is_level: bool = False,
|
|
25
|
+
profile: Expr | None = None,
|
|
26
|
+
operations: tuple[str, list[Expr]] | None = None,
|
|
27
|
+
) -> None:
|
|
28
|
+
"""
|
|
29
|
+
Create new (immutable) expression.
|
|
30
|
+
|
|
31
|
+
Args:
|
|
32
|
+
src (str | None, optional): _description_. Defaults to None.
|
|
33
|
+
is_stock (bool, optional): _description_. Defaults to False.
|
|
34
|
+
is_flow (bool, optional): _description_. Defaults to False.
|
|
35
|
+
is_profile (bool, optional): _description_. Defaults to False.
|
|
36
|
+
is_level (bool, optional): _description_. Defaults to False.
|
|
37
|
+
profile (Expr | None, optional): _description_. Defaults to None.
|
|
38
|
+
operations (tuple[str, list[Expr]] | None, optional): Operations to apply to the expression. Defaults to None.
|
|
39
|
+
|
|
40
|
+
""" # TODO: write detailed description of what the labels of Expr means (level, profile, flow, stock)
|
|
41
|
+
self._src: str | TimeVector | None = src
|
|
42
|
+
self._is_stock = is_stock
|
|
43
|
+
self._is_flow = is_flow
|
|
44
|
+
self._is_profile = is_profile
|
|
45
|
+
self._is_level = is_level
|
|
46
|
+
self._profile = profile
|
|
47
|
+
|
|
48
|
+
# have to come after setting fields
|
|
49
|
+
# because fields are used to create
|
|
50
|
+
# error messages e.g. in __repr__
|
|
51
|
+
|
|
52
|
+
self._check_type(src, (str, TimeVector, type(None)))
|
|
53
|
+
self._check_type(is_stock, (bool, type(None)))
|
|
54
|
+
self._check_type(is_flow, (bool, type(None)))
|
|
55
|
+
self._check_type(is_level, (bool, type(None)))
|
|
56
|
+
self._check_type(is_profile, (bool, type(None)))
|
|
57
|
+
self._check_type(profile, (Expr, type(None)))
|
|
58
|
+
|
|
59
|
+
self._check_operations(operations)
|
|
60
|
+
if operations is None:
|
|
61
|
+
operations = "", []
|
|
62
|
+
self._operations: tuple[str, list[Expr]] = operations
|
|
63
|
+
|
|
64
|
+
def _check_operations(self, operations: tuple[str, list[Expr]] | None, expect_ops: bool = False) -> None:
|
|
65
|
+
if operations is None:
|
|
66
|
+
return
|
|
67
|
+
self._check_type(operations, tuple)
|
|
68
|
+
if len(operations) != 2: # noqa: PLR2004
|
|
69
|
+
message = f"Expected len(operations) == 2. Got: {operations}"
|
|
70
|
+
raise ValueError(message)
|
|
71
|
+
ops, args = operations
|
|
72
|
+
self._check_type(ops, str)
|
|
73
|
+
self._check_type(args, list)
|
|
74
|
+
if ops == "":
|
|
75
|
+
if expect_ops:
|
|
76
|
+
message = f"Expected ops, but got {operations}"
|
|
77
|
+
raise ValueError(message)
|
|
78
|
+
if len(args) > 0:
|
|
79
|
+
message = f"Expected ops to have length. Got {operations}"
|
|
80
|
+
raise ValueError(message)
|
|
81
|
+
return
|
|
82
|
+
if len(ops) != len(args) - 1:
|
|
83
|
+
message = f"Expected len(ops) == len(args). Got {operations}"
|
|
84
|
+
raise ValueError(message)
|
|
85
|
+
for op in ops:
|
|
86
|
+
if op not in "+-/*":
|
|
87
|
+
message = f"Expected all op in ops in +-*/. Got {operations}"
|
|
88
|
+
raise ValueError(message)
|
|
89
|
+
for ex in args:
|
|
90
|
+
self._check_type(ex, Expr)
|
|
91
|
+
|
|
92
|
+
def get_fingerprint(self) -> Fingerprint:
|
|
93
|
+
"""Return fingerprint."""
|
|
94
|
+
fingerprint = Fingerprint(self)
|
|
95
|
+
fingerprint.add("is_stock", self._is_stock)
|
|
96
|
+
fingerprint.add("is_flow", self._is_flow)
|
|
97
|
+
fingerprint.add("is_profile", self._is_profile)
|
|
98
|
+
fingerprint.add("is_level", self._is_level)
|
|
99
|
+
fingerprint.add("profile", self._profile)
|
|
100
|
+
if self._src:
|
|
101
|
+
fingerprint.add("src", self._src.get_fingerprint() if isinstance(self._src, TimeVector) else FingerprintRef(self._src))
|
|
102
|
+
fingerprint.add("operations", self._operations)
|
|
103
|
+
return fingerprint
|
|
104
|
+
|
|
105
|
+
def is_leaf(self) -> bool:
|
|
106
|
+
"""Return True if self is not an operation expression."""
|
|
107
|
+
return self._src is not None
|
|
108
|
+
|
|
109
|
+
def get_src(self) -> str | TimeVector | None:
|
|
110
|
+
"""Return str (either usercode or key in model) or None if self is an operation expression."""
|
|
111
|
+
return self._src
|
|
112
|
+
|
|
113
|
+
def get_operations(self, expect_ops: bool, copy_list: bool) -> tuple[str, list[Expr]]:
|
|
114
|
+
"""Return ops, args. Users of this (low level) API must supply expect_ops and copy_list args."""
|
|
115
|
+
self._check_type(copy_list, bool)
|
|
116
|
+
self._verify_operations(expect_ops)
|
|
117
|
+
if copy_list:
|
|
118
|
+
ops, args = self._operations
|
|
119
|
+
return ops, copy(args)
|
|
120
|
+
return self._operations
|
|
121
|
+
|
|
122
|
+
def _verify_operations(self, expect_ops: bool = False) -> None:
|
|
123
|
+
self._check_operations(self._operations, expect_ops)
|
|
124
|
+
ops = self._operations[0]
|
|
125
|
+
if not ops:
|
|
126
|
+
return
|
|
127
|
+
has_add = "+" in ops
|
|
128
|
+
has_sub = "-" in ops
|
|
129
|
+
has_mul = "*" in ops
|
|
130
|
+
has_div = "/" in ops
|
|
131
|
+
if (has_add or has_sub) and (has_mul or has_div):
|
|
132
|
+
message = f"+- in same operation level as */ in operations {self._operations} "
|
|
133
|
+
raise ValueError(message)
|
|
134
|
+
if has_div:
|
|
135
|
+
seen_div = False
|
|
136
|
+
for op in ops:
|
|
137
|
+
if op == "/":
|
|
138
|
+
seen_div = True
|
|
139
|
+
break
|
|
140
|
+
if seen_div and op != "/":
|
|
141
|
+
message = f"+-* after / in operations {self._operations}"
|
|
142
|
+
|
|
143
|
+
def is_flow(self) -> bool:
|
|
144
|
+
"""Return True if flow. Cannot be stock and flow."""
|
|
145
|
+
return self._is_flow
|
|
146
|
+
|
|
147
|
+
def is_stock(self) -> bool:
|
|
148
|
+
"""Return True if stock. Cannot be stock and flow."""
|
|
149
|
+
return self._is_stock
|
|
150
|
+
|
|
151
|
+
def is_level(self) -> bool:
|
|
152
|
+
"""Return True if level. Cannot be level and profile."""
|
|
153
|
+
return self._is_level
|
|
154
|
+
|
|
155
|
+
def is_profile(self) -> bool:
|
|
156
|
+
"""Return True if profile. Cannot be level and profile."""
|
|
157
|
+
return self._is_profile
|
|
158
|
+
|
|
159
|
+
def get_profile(self) -> Expr | None:
|
|
160
|
+
"""Return Expr representing profile. Implies self.is_level() is True."""
|
|
161
|
+
return self._profile
|
|
162
|
+
|
|
163
|
+
def set_profile(self, profile: Expr | None) -> None:
|
|
164
|
+
"""Set the profile of the Expr. Implies self.is_level() is True."""
|
|
165
|
+
if not self.is_level():
|
|
166
|
+
raise ValueError("Cannot set profile on Expr that is not a level.")
|
|
167
|
+
self._profile = profile
|
|
168
|
+
|
|
169
|
+
def _analyze_op(self, op: str, other: Expr) -> tuple[bool, bool, bool, bool, Expr | None]:
|
|
170
|
+
flow = (True, False)
|
|
171
|
+
stock = (False, True)
|
|
172
|
+
level = (True, False)
|
|
173
|
+
profile = (False, True)
|
|
174
|
+
none = (False, False)
|
|
175
|
+
|
|
176
|
+
supported_cases = {
|
|
177
|
+
# all op supported for none
|
|
178
|
+
("+", none, none, none, none): (none, none, None),
|
|
179
|
+
("-", none, none, none, none): (none, none, None),
|
|
180
|
+
("*", none, none, none, none): (none, none, None),
|
|
181
|
+
("/", none, none, none, none): (none, none, None),
|
|
182
|
+
# + flow level
|
|
183
|
+
("+", flow, level, flow, level): (flow, level, None),
|
|
184
|
+
# * flow level
|
|
185
|
+
("*", flow, level, none, none): (flow, level, self.get_profile()),
|
|
186
|
+
("*", none, none, flow, level): (flow, level, other.get_profile()),
|
|
187
|
+
# / flow level
|
|
188
|
+
("/", flow, level, none, none): (flow, level, self.get_profile()),
|
|
189
|
+
("/", flow, level, flow, level): (none, none, None),
|
|
190
|
+
# + stock level
|
|
191
|
+
("+", stock, level, stock, level): (stock, level, None),
|
|
192
|
+
# * stock level
|
|
193
|
+
("*", stock, level, none, level): (stock, level, None),
|
|
194
|
+
("*", none, level, stock, level): (stock, level, None),
|
|
195
|
+
("*", stock, level, none, none): (stock, level, self.get_profile()),
|
|
196
|
+
("*", none, none, stock, level): (stock, level, other.get_profile()),
|
|
197
|
+
# / stock level
|
|
198
|
+
("/", stock, level, none, level): (stock, level, None),
|
|
199
|
+
("/", stock, level, none, none): (stock, level, self.get_profile()),
|
|
200
|
+
("/", stock, level, stock, level): (none, none, None),
|
|
201
|
+
# level * level ok if one is flow (i.e. price * volume) or none (co2_eff / eff)
|
|
202
|
+
("*", flow, level, none, level): (flow, level, None),
|
|
203
|
+
("*", none, level, flow, level): (flow, level, None),
|
|
204
|
+
("/", flow, level, none, level): (flow, level, None),
|
|
205
|
+
("/", none, level, none, level): (none, level, None),
|
|
206
|
+
("*", none, level, none, level): (none, level, None),
|
|
207
|
+
# profile
|
|
208
|
+
("+", none, profile, none, profile): (none, profile, None),
|
|
209
|
+
("-", none, profile, none, profile): (none, profile, None),
|
|
210
|
+
("/", none, profile, none, none): (none, profile, None),
|
|
211
|
+
("*", none, profile, none, none): (none, profile, None),
|
|
212
|
+
("*", none, none, none, profile): (none, profile, None),
|
|
213
|
+
("/", none, none, none, profile): (none, profile, None),
|
|
214
|
+
# level
|
|
215
|
+
("+", none, level, none, level): (none, level, None),
|
|
216
|
+
("-", none, level, none, level): (none, level, None),
|
|
217
|
+
("/", none, level, none, none): (none, level, self.get_profile()),
|
|
218
|
+
("*", none, level, none, none): (none, level, self.get_profile()),
|
|
219
|
+
("*", none, none, none, level): (none, level, other.get_profile()),
|
|
220
|
+
("/", none, none, none, level): (none, level, other.get_profile()),
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
case = (
|
|
224
|
+
op,
|
|
225
|
+
(self.is_flow(), self.is_stock()),
|
|
226
|
+
(self.is_level(), self.is_profile()),
|
|
227
|
+
(other.is_flow(), other.is_stock()),
|
|
228
|
+
(other.is_level(), other.is_profile()),
|
|
229
|
+
)
|
|
230
|
+
|
|
231
|
+
if case not in supported_cases:
|
|
232
|
+
printable_case = {
|
|
233
|
+
"op": case[0],
|
|
234
|
+
"self_is_flow": case[1][0],
|
|
235
|
+
"self_is_stock": case[1][1],
|
|
236
|
+
"self_is_level": case[2][0],
|
|
237
|
+
"self_is_profile": case[2][1],
|
|
238
|
+
"other_is_flow": case[3][0],
|
|
239
|
+
"other_is_stock": case[3][1],
|
|
240
|
+
"other_is_level": case[4][0],
|
|
241
|
+
"other_is_profile": case[4][1],
|
|
242
|
+
}
|
|
243
|
+
message = f"Unsupported case:\n{printable_case}\nexpression:\n{self} {op} {other}."
|
|
244
|
+
raise ValueError(message)
|
|
245
|
+
|
|
246
|
+
((is_flow, is_stock), (is_level, is_profile), profile) = supported_cases[case]
|
|
247
|
+
|
|
248
|
+
return is_stock, is_flow, is_level, is_profile, profile
|
|
249
|
+
|
|
250
|
+
@staticmethod
|
|
251
|
+
def _is_number(src: str) -> bool:
|
|
252
|
+
try:
|
|
253
|
+
float(src)
|
|
254
|
+
return True
|
|
255
|
+
except ValueError:
|
|
256
|
+
return False
|
|
257
|
+
|
|
258
|
+
def _create_op_expr( # noqa: C901
|
|
259
|
+
self,
|
|
260
|
+
op: str,
|
|
261
|
+
other: object,
|
|
262
|
+
is_rhs: bool,
|
|
263
|
+
) -> Expr:
|
|
264
|
+
if isinstance(other, Expr):
|
|
265
|
+
is_stock, is_flow, is_level, is_profile, profile = self._analyze_op(op, other)
|
|
266
|
+
|
|
267
|
+
x, y = (other, self) if is_rhs else (self, other)
|
|
268
|
+
|
|
269
|
+
xisconst = isinstance(x.get_src(), ConstantTimeVector)
|
|
270
|
+
yisconst = isinstance(y.get_src(), ConstantTimeVector)
|
|
271
|
+
if xisconst and yisconst:
|
|
272
|
+
xtv = x.get_src()
|
|
273
|
+
ytv = y.get_src()
|
|
274
|
+
is_combinable_tv = (
|
|
275
|
+
xtv.get_unit() == ytv.get_unit()
|
|
276
|
+
and xtv.is_max_level() == ytv.is_max_level()
|
|
277
|
+
and xtv.is_zero_one_profile() == ytv.is_zero_one_profile()
|
|
278
|
+
and xtv.get_reference_period() == ytv.get_reference_period()
|
|
279
|
+
)
|
|
280
|
+
if is_combinable_tv:
|
|
281
|
+
is_combinable_expr = (
|
|
282
|
+
x.is_level() == y.is_level()
|
|
283
|
+
and x.is_profile() == y.is_profile()
|
|
284
|
+
and x.is_flow() == y.is_flow()
|
|
285
|
+
and x.is_stock() == y.is_stock()
|
|
286
|
+
and x.get_profile() == y.get_profile()
|
|
287
|
+
)
|
|
288
|
+
if is_combinable_expr:
|
|
289
|
+
xscalar = xtv.get_vector(is_float32=True)[0]
|
|
290
|
+
yscalar = ytv.get_vector(is_float32=True)[0]
|
|
291
|
+
if op == "+":
|
|
292
|
+
scalar = xscalar + yscalar
|
|
293
|
+
elif op == "-":
|
|
294
|
+
scalar = xscalar - yscalar
|
|
295
|
+
elif op == "*":
|
|
296
|
+
scalar = xscalar * yscalar
|
|
297
|
+
elif op == "/":
|
|
298
|
+
scalar = xscalar / yscalar
|
|
299
|
+
return Expr(
|
|
300
|
+
src=ConstantTimeVector(
|
|
301
|
+
scalar=scalar,
|
|
302
|
+
unit=xtv.get_unit(),
|
|
303
|
+
is_max_level=xtv.is_max_level(),
|
|
304
|
+
is_zero_one_profile=xtv.is_zero_one_profile(),
|
|
305
|
+
reference_period=xtv.get_reference_period(),
|
|
306
|
+
),
|
|
307
|
+
is_stock=x.is_stock(),
|
|
308
|
+
is_flow=x.is_flow(),
|
|
309
|
+
is_profile=x.is_profile(),
|
|
310
|
+
is_level=x.is_level(),
|
|
311
|
+
profile=x.get_profile(),
|
|
312
|
+
operations=None,
|
|
313
|
+
)
|
|
314
|
+
|
|
315
|
+
ops, args = x.get_operations(expect_ops=False, copy_list=True)
|
|
316
|
+
|
|
317
|
+
if not ops:
|
|
318
|
+
ops = op
|
|
319
|
+
args = [x, y]
|
|
320
|
+
else:
|
|
321
|
+
last_op = ops[-1]
|
|
322
|
+
if last_op == op or (op in "+-" and last_op in "+-") or (last_op == "*" and op == "/"):
|
|
323
|
+
ops = f"{ops}{op}"
|
|
324
|
+
args.append(y)
|
|
325
|
+
else:
|
|
326
|
+
ops = op
|
|
327
|
+
args = [x, y]
|
|
328
|
+
|
|
329
|
+
return Expr(
|
|
330
|
+
src=None,
|
|
331
|
+
is_flow=is_flow,
|
|
332
|
+
is_stock=is_stock,
|
|
333
|
+
is_level=is_level,
|
|
334
|
+
is_profile=is_profile,
|
|
335
|
+
profile=profile,
|
|
336
|
+
operations=(ops, args),
|
|
337
|
+
)
|
|
338
|
+
|
|
339
|
+
if self._is_number(other):
|
|
340
|
+
if op in "*/":
|
|
341
|
+
other_expr = Expr(src=ConstantTimeVector(float(other), is_max_level=False))
|
|
342
|
+
return self._create_op_expr(op=op, other=other_expr, is_rhs=is_rhs)
|
|
343
|
+
|
|
344
|
+
if op in "+" and other == 0: # Comes from sum(expr_list). See sum() noqa
|
|
345
|
+
return self # TODO: Also accept 0 - Expr and -Expr?
|
|
346
|
+
|
|
347
|
+
message = f"Only support multiplication and division with numbers, got {op} and {other}."
|
|
348
|
+
raise ValueError(message)
|
|
349
|
+
|
|
350
|
+
message = f"Only support Expr, int, float. Got unsupported type {type(other).__name__}."
|
|
351
|
+
raise TypeError(message)
|
|
352
|
+
|
|
353
|
+
def __add__(self, other: object) -> Expr: # noqa: D105
|
|
354
|
+
return self._create_op_expr("+", other, is_rhs=False)
|
|
355
|
+
|
|
356
|
+
def __sub__(self, other: object) -> Expr: # noqa: D105
|
|
357
|
+
return self._create_op_expr("-", other, is_rhs=False)
|
|
358
|
+
|
|
359
|
+
def __mul__(self, other: object) -> Expr: # noqa: D105
|
|
360
|
+
return self._create_op_expr("*", other, is_rhs=False)
|
|
361
|
+
|
|
362
|
+
def __truediv__(self, other: object) -> Expr: # noqa: D105
|
|
363
|
+
return self._create_op_expr("/", other, is_rhs=False)
|
|
364
|
+
|
|
365
|
+
def __radd__(self, other: object) -> Expr: # noqa: D105
|
|
366
|
+
return self._create_op_expr("+", other, is_rhs=True)
|
|
367
|
+
|
|
368
|
+
def __rsub__(self, other: object) -> Expr: # noqa: D105
|
|
369
|
+
return self._create_op_expr("-", other, is_rhs=True)
|
|
370
|
+
|
|
371
|
+
def __rmul__(self, other: object) -> Expr: # noqa: D105
|
|
372
|
+
return self._create_op_expr("*", other, is_rhs=True)
|
|
373
|
+
|
|
374
|
+
def __rtruediv__(self, other: object) -> Expr: # noqa: D105
|
|
375
|
+
return self._create_op_expr("/", other, is_rhs=True)
|
|
376
|
+
|
|
377
|
+
def __repr__(self) -> str:
|
|
378
|
+
"""Represent Expr as str."""
|
|
379
|
+
if self._src is not None:
|
|
380
|
+
return f"{self._src}"
|
|
381
|
+
ops, args = self.get_operations(expect_ops=True, copy_list=False)
|
|
382
|
+
out = f"{args[0]}"
|
|
383
|
+
for op, arg in zip(ops, args[1:], strict=True):
|
|
384
|
+
out = f"{out} {op} {arg}"
|
|
385
|
+
return f"({out})"
|
|
386
|
+
|
|
387
|
+
def __eq__(self, other) -> bool: # noqa: ANN001
|
|
388
|
+
"""Check if self and other are equal."""
|
|
389
|
+
if not isinstance(other, type(self)):
|
|
390
|
+
return False
|
|
391
|
+
return (
|
|
392
|
+
self._is_flow == other._is_flow
|
|
393
|
+
and self._is_level == other._is_level
|
|
394
|
+
and self._src == other._src
|
|
395
|
+
and self._is_stock == other._is_stock
|
|
396
|
+
and self._is_profile == other._is_profile
|
|
397
|
+
and self._profile == other._profile
|
|
398
|
+
and self._operations[0] == other._operations[0]
|
|
399
|
+
and len(self._operations[1]) == len(other._operations[1])
|
|
400
|
+
and all([self._operations[1][i] == other._operations[1][i] for i in range(len(self._operations[1]))])
|
|
401
|
+
)
|
|
402
|
+
|
|
403
|
+
def __hash__(self) -> int:
|
|
404
|
+
"""Compute hash value.."""
|
|
405
|
+
return hash(
|
|
406
|
+
(
|
|
407
|
+
self._is_flow,
|
|
408
|
+
self._is_stock,
|
|
409
|
+
self._is_level,
|
|
410
|
+
self._is_profile,
|
|
411
|
+
self._src,
|
|
412
|
+
self._profile,
|
|
413
|
+
self._operations[0],
|
|
414
|
+
tuple(self._operations[1]),
|
|
415
|
+
),
|
|
416
|
+
)
|
|
417
|
+
|
|
418
|
+
def add_loaders(self, loaders: set[Loader]) -> None:
|
|
419
|
+
"""Add all loaders stored in TimeVector or Curve within Expr to loaders."""
|
|
420
|
+
from framcore.curves import Curve
|
|
421
|
+
|
|
422
|
+
if self.is_leaf():
|
|
423
|
+
src = self.get_src()
|
|
424
|
+
if isinstance(src, TimeVector | Curve):
|
|
425
|
+
loader = src.get_loader()
|
|
426
|
+
if loader is not None:
|
|
427
|
+
loaders.add(loader)
|
|
428
|
+
return
|
|
429
|
+
__, args = self.get_operations(expect_ops=True, copy_list=False)
|
|
430
|
+
for arg in args:
|
|
431
|
+
arg.add_loaders(loaders)
|
|
432
|
+
|
|
433
|
+
|
|
434
|
+
# Proposed new way of creating Expr in classes.
|
|
435
|
+
def ensure_expr(
|
|
436
|
+
value: Expr | str | TimeVector | None, # technically anything that can be converted to float. Typehint for this?
|
|
437
|
+
is_flow: bool = False,
|
|
438
|
+
is_stock: bool = False,
|
|
439
|
+
is_level: bool = False,
|
|
440
|
+
is_profile: bool = False,
|
|
441
|
+
profile: Expr | None = None,
|
|
442
|
+
) -> Expr | None:
|
|
443
|
+
"""
|
|
444
|
+
Ensure that the value is an expression of the expected type or create one if possible.
|
|
445
|
+
|
|
446
|
+
Args:
|
|
447
|
+
value (Expr | str | None): The value to check.
|
|
448
|
+
is_flow (str): If the Expr is a flow. Cannot be True if is_stock is True.
|
|
449
|
+
is_stock (str): If the Expr is a stock. Cannot be True if is_flow is True.
|
|
450
|
+
is_level (bool): Wether the Expr represents a level. Cannot be True if is_profile is True.
|
|
451
|
+
is_profile (bool): Wether the Expr represents a profile. Cannot be True if is_level is True.
|
|
452
|
+
profile (Expr | None): If the Expr is a level, this should be its profile.
|
|
453
|
+
|
|
454
|
+
Returns:
|
|
455
|
+
value (Expr | str): The value as an expression of the expected type or None.
|
|
456
|
+
|
|
457
|
+
"""
|
|
458
|
+
# Following checks could be moved to Expr.
|
|
459
|
+
if is_level and is_profile:
|
|
460
|
+
message = "Expr cannot be both level and a profile. Set either is_level or is_profile True or both False."
|
|
461
|
+
raise ValueError(message)
|
|
462
|
+
if is_flow and is_stock:
|
|
463
|
+
message = "Expr cannot be both flow and stock. Set either is_flow or is_stock True or both False."
|
|
464
|
+
raise ValueError(message)
|
|
465
|
+
if is_profile and (is_flow or is_stock):
|
|
466
|
+
message = "Expr cannot be both a profile and a flow/stock. Profiles must be coefficients."
|
|
467
|
+
|
|
468
|
+
if value is None:
|
|
469
|
+
return None
|
|
470
|
+
if isinstance(value, Expr):
|
|
471
|
+
# Check wether given Expr matches expected flow, stock, profile and level status.
|
|
472
|
+
# Alternatively we could just create a new Expr with updated status.
|
|
473
|
+
if value.is_flow() != is_flow or value.is_stock() != is_stock or value.is_level() != is_level or value.is_profile() != is_profile:
|
|
474
|
+
message = (
|
|
475
|
+
"Given Expr has a mismatch between expected and actual flow/stock or level/profile status:\nExpected: "
|
|
476
|
+
f"is_flow - {is_flow}, is_stock - {is_stock}, is_level - {is_level}, is_profile - {is_profile}\n"
|
|
477
|
+
f"Actual: is_flow - {value.is_flow()}, is_stock - {value.is_stock()}, "
|
|
478
|
+
f"is_level - {value.is_level()}, is_profile - {value.is_profile()}"
|
|
479
|
+
)
|
|
480
|
+
raise ValueError(message)
|
|
481
|
+
return value
|
|
482
|
+
|
|
483
|
+
return Expr(
|
|
484
|
+
src=value,
|
|
485
|
+
is_flow=is_flow,
|
|
486
|
+
is_stock=is_stock,
|
|
487
|
+
is_level=is_level,
|
|
488
|
+
is_profile=is_profile,
|
|
489
|
+
profile=profile,
|
|
490
|
+
)
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
# framcore/expressions/__init__.py
|
|
2
|
+
|
|
3
|
+
from framcore.expressions.Expr import Expr, ensure_expr
|
|
4
|
+
|
|
5
|
+
from framcore.expressions.units import (
|
|
6
|
+
get_unit_conversion_factor,
|
|
7
|
+
is_convertable,
|
|
8
|
+
validate_unit_conversion_fastpaths,
|
|
9
|
+
)
|
|
10
|
+
|
|
11
|
+
from framcore.expressions.queries import (
|
|
12
|
+
get_level_value,
|
|
13
|
+
get_profile_vector,
|
|
14
|
+
get_units_from_expr,
|
|
15
|
+
get_timeindexes_from_expr,
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
__all__ = [
|
|
19
|
+
"Expr",
|
|
20
|
+
"ensure_expr",
|
|
21
|
+
"get_level_value",
|
|
22
|
+
"get_profile_vector",
|
|
23
|
+
"get_timeindexes_from_expr",
|
|
24
|
+
"get_unit_conversion_factor",
|
|
25
|
+
"get_units_from_expr",
|
|
26
|
+
"is_convertable",
|
|
27
|
+
"validate_unit_conversion_fastpaths",
|
|
28
|
+
]
|