fram-core 0.0.0__py3-none-any.whl → 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- fram_core-0.1.0.dist-info/METADATA +42 -0
- fram_core-0.1.0.dist-info/RECORD +100 -0
- {fram_core-0.0.0.dist-info → fram_core-0.1.0.dist-info}/WHEEL +1 -2
- fram_core-0.1.0.dist-info/licenses/LICENSE.md +8 -0
- framcore/Base.py +161 -0
- framcore/Model.py +90 -0
- framcore/__init__.py +10 -0
- framcore/aggregators/Aggregator.py +172 -0
- framcore/aggregators/HydroAggregator.py +849 -0
- framcore/aggregators/NodeAggregator.py +530 -0
- framcore/aggregators/WindSolarAggregator.py +315 -0
- framcore/aggregators/__init__.py +13 -0
- framcore/aggregators/_utils.py +184 -0
- framcore/attributes/Arrow.py +307 -0
- framcore/attributes/ElasticDemand.py +90 -0
- framcore/attributes/ReservoirCurve.py +23 -0
- framcore/attributes/SoftBound.py +16 -0
- framcore/attributes/StartUpCost.py +65 -0
- framcore/attributes/Storage.py +158 -0
- framcore/attributes/TargetBound.py +16 -0
- framcore/attributes/__init__.py +63 -0
- framcore/attributes/hydro/HydroBypass.py +49 -0
- framcore/attributes/hydro/HydroGenerator.py +100 -0
- framcore/attributes/hydro/HydroPump.py +178 -0
- framcore/attributes/hydro/HydroReservoir.py +27 -0
- framcore/attributes/hydro/__init__.py +13 -0
- framcore/attributes/level_profile_attributes.py +911 -0
- framcore/components/Component.py +136 -0
- framcore/components/Demand.py +144 -0
- framcore/components/Flow.py +189 -0
- framcore/components/HydroModule.py +371 -0
- framcore/components/Node.py +99 -0
- framcore/components/Thermal.py +208 -0
- framcore/components/Transmission.py +198 -0
- framcore/components/_PowerPlant.py +81 -0
- framcore/components/__init__.py +22 -0
- framcore/components/wind_solar.py +82 -0
- framcore/curves/Curve.py +44 -0
- framcore/curves/LoadedCurve.py +146 -0
- framcore/curves/__init__.py +9 -0
- framcore/events/__init__.py +21 -0
- framcore/events/events.py +51 -0
- framcore/expressions/Expr.py +591 -0
- framcore/expressions/__init__.py +30 -0
- framcore/expressions/_get_constant_from_expr.py +477 -0
- framcore/expressions/_utils.py +73 -0
- framcore/expressions/queries.py +416 -0
- framcore/expressions/units.py +227 -0
- framcore/fingerprints/__init__.py +11 -0
- framcore/fingerprints/fingerprint.py +292 -0
- framcore/juliamodels/JuliaModel.py +171 -0
- framcore/juliamodels/__init__.py +7 -0
- framcore/loaders/__init__.py +10 -0
- framcore/loaders/loaders.py +405 -0
- framcore/metadata/Div.py +73 -0
- framcore/metadata/ExprMeta.py +56 -0
- framcore/metadata/LevelExprMeta.py +32 -0
- framcore/metadata/Member.py +55 -0
- framcore/metadata/Meta.py +44 -0
- framcore/metadata/__init__.py +15 -0
- framcore/populators/Populator.py +108 -0
- framcore/populators/__init__.py +7 -0
- framcore/querydbs/CacheDB.py +50 -0
- framcore/querydbs/ModelDB.py +34 -0
- framcore/querydbs/QueryDB.py +45 -0
- framcore/querydbs/__init__.py +11 -0
- framcore/solvers/Solver.py +63 -0
- framcore/solvers/SolverConfig.py +272 -0
- framcore/solvers/__init__.py +9 -0
- framcore/timeindexes/AverageYearRange.py +27 -0
- framcore/timeindexes/ConstantTimeIndex.py +22 -0
- framcore/timeindexes/DailyIndex.py +33 -0
- framcore/timeindexes/FixedFrequencyTimeIndex.py +814 -0
- framcore/timeindexes/HourlyIndex.py +33 -0
- framcore/timeindexes/IsoCalendarDay.py +33 -0
- framcore/timeindexes/ListTimeIndex.py +277 -0
- framcore/timeindexes/ModelYear.py +23 -0
- framcore/timeindexes/ModelYears.py +27 -0
- framcore/timeindexes/OneYearProfileTimeIndex.py +29 -0
- framcore/timeindexes/ProfileTimeIndex.py +43 -0
- framcore/timeindexes/SinglePeriodTimeIndex.py +37 -0
- framcore/timeindexes/TimeIndex.py +103 -0
- framcore/timeindexes/WeeklyIndex.py +33 -0
- framcore/timeindexes/__init__.py +36 -0
- framcore/timeindexes/_time_vector_operations.py +689 -0
- framcore/timevectors/ConstantTimeVector.py +131 -0
- framcore/timevectors/LinearTransformTimeVector.py +131 -0
- framcore/timevectors/ListTimeVector.py +127 -0
- framcore/timevectors/LoadedTimeVector.py +97 -0
- framcore/timevectors/ReferencePeriod.py +51 -0
- framcore/timevectors/TimeVector.py +108 -0
- framcore/timevectors/__init__.py +17 -0
- framcore/utils/__init__.py +35 -0
- framcore/utils/get_regional_volumes.py +387 -0
- framcore/utils/get_supported_components.py +60 -0
- framcore/utils/global_energy_equivalent.py +63 -0
- framcore/utils/isolate_subnodes.py +172 -0
- framcore/utils/loaders.py +97 -0
- framcore/utils/node_flow_utils.py +236 -0
- framcore/utils/storage_subsystems.py +106 -0
- fram_core-0.0.0.dist-info/METADATA +0 -5
- fram_core-0.0.0.dist-info/RECORD +0 -4
- fram_core-0.0.0.dist-info/top_level.txt +0 -1
|
@@ -0,0 +1,591 @@
|
|
|
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.curves import Curve, LoadedCurve
|
|
8
|
+
from framcore.fingerprints import Fingerprint, FingerprintRef
|
|
9
|
+
from framcore.timevectors import ConstantTimeVector, TimeVector
|
|
10
|
+
|
|
11
|
+
if TYPE_CHECKING:
|
|
12
|
+
from framcore.loaders import Loader
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
# TODO: Add Expr.add_many to support faster aggregation expressions.
|
|
16
|
+
class Expr(Base):
|
|
17
|
+
"""
|
|
18
|
+
Mathematical expression with TimeVectors and Curves to represent Levels and Profiles in LevelProfiles.
|
|
19
|
+
|
|
20
|
+
The simplest Expr is a single TimeVector, while a more complicated expression could be a weighted average of several TimeVectors or Expressions.
|
|
21
|
+
Expr can also have string references to Expr, TimeVector or Curve in a database (often Model).
|
|
22
|
+
|
|
23
|
+
Expr are classified as Stock, Flow or None of them. See https://en.wikipedia.org/wiki/Stock_and_flow. In FRAM we only support Flow data as a rate of change.
|
|
24
|
+
So, for example, a production timeseries has to be in MW, and not in MWh. Converting between the two versions of Flow would add another
|
|
25
|
+
level of complexity both in Expr and in TimeVector operations.
|
|
26
|
+
|
|
27
|
+
Expr are also classified as Level, Profile or none of them. This classification, together with Stock or Flow,
|
|
28
|
+
is used to check if the built Expr are legal operations.
|
|
29
|
+
- Expr that are Level can contain its connected Profile Expr. This is used in the queries to evaluate Levels according to their ReferencePeriod, and
|
|
30
|
+
convert between Level formats (max level or average level, see LevelProfile for more details).
|
|
31
|
+
|
|
32
|
+
Calculations using Expr are evaluated lazily, reducing unnecessary numerical operations during data manipulation.
|
|
33
|
+
Computations involving values and units occur only when the Expr is queried.
|
|
34
|
+
|
|
35
|
+
We only support calculations using +, -, *, and / in Expr, and we have no plans to change this.
|
|
36
|
+
Expanding beyond these would turn Expr into a complex programming language rather than keeping it as a simple
|
|
37
|
+
and efficient system for common time-series calculations. More advanced operations are still possible through eager evaluation, so this is not a limitation.
|
|
38
|
+
It simply distributes responsibilities across system components in a way that is practical from a maintenance perspective.
|
|
39
|
+
|
|
40
|
+
We use SymPy to support unit conversions. Already computed unit conversion factors are cached to minimize redundant calculations.
|
|
41
|
+
|
|
42
|
+
At the moment we support these queries for Expr (see Aggregators for more about how they are used):
|
|
43
|
+
- get_level_value(expr, db, unit, data_dim, scen_dim, is_max)
|
|
44
|
+
- Supports all expressions. Will evaluate level Exprs at data_dim (with reference period of scen_dim),
|
|
45
|
+
and profile Exprs as an average over scen_dim (both as constants).
|
|
46
|
+
- Has optimized fastpath methods for sums, products and aggregations. The rest uses a fallback method with SymPy.
|
|
47
|
+
- get_profile_vector(expr, db, data_dim, scen_dim, is_zero_one, is_float32)
|
|
48
|
+
- Supports expr = sum(weight[i] * profile[i]) where weight[i] is a unitless constant Expr with value >= 0, and profile[i] is a unitless profile Expr.
|
|
49
|
+
|
|
50
|
+
"""
|
|
51
|
+
|
|
52
|
+
def __init__(
|
|
53
|
+
self,
|
|
54
|
+
src: str | Curve | TimeVector | None = None,
|
|
55
|
+
is_stock: bool = False,
|
|
56
|
+
is_flow: bool = False,
|
|
57
|
+
is_profile: bool = False,
|
|
58
|
+
is_level: bool = False,
|
|
59
|
+
profile: Expr | None = None,
|
|
60
|
+
operations: tuple[str, list[Expr]] | None = None,
|
|
61
|
+
) -> None:
|
|
62
|
+
"""
|
|
63
|
+
Create new (immutable) Expression.
|
|
64
|
+
|
|
65
|
+
Args:
|
|
66
|
+
src (str | Curve | TimeVector | None, optional): Source of the values to be used in the Expression. Either a Curve or TimeVector object,
|
|
67
|
+
or a reference to one of them. Defaults to None.
|
|
68
|
+
is_stock (bool, optional): Flag to signify if the Expr represents a stock type variable. Defaults to False.
|
|
69
|
+
is_flow (bool, optional): Flag to signify if the Expr represents a flow type variable. Defaults to False.
|
|
70
|
+
is_profile (bool, optional): Flag to signify if the Expr represents a profile. Defaults to False.
|
|
71
|
+
is_level (bool, optional): Flag to signify if the Expr represents a level. Defaults to False.
|
|
72
|
+
profile (Expr | None, optional): Expr that are Level can contain its connected Profile Expr. This is used in the queries to evaluate
|
|
73
|
+
Levels according to their ReferencePeriod, and convert between Level formats (max level or average level, see LevelProfile for more details).
|
|
74
|
+
operations (tuple[str, list[Expr]] | None, optional): Operations to apply to the expression. Defaults to None.
|
|
75
|
+
|
|
76
|
+
"""
|
|
77
|
+
if is_level and is_profile:
|
|
78
|
+
message = "Expr cannot be both level and a profile. Set either is_level or is_profile True or both False."
|
|
79
|
+
raise ValueError(message)
|
|
80
|
+
|
|
81
|
+
if is_flow and is_stock:
|
|
82
|
+
message = "Expr cannot be both flow and stock. Set either is_flow or is_stock True or both False."
|
|
83
|
+
raise ValueError(message)
|
|
84
|
+
|
|
85
|
+
if is_profile and (is_flow or is_stock):
|
|
86
|
+
message = "Expr cannot be both a profile and a flow/stock. Profiles must be coefficients."
|
|
87
|
+
raise ValueError(message)
|
|
88
|
+
|
|
89
|
+
self._src: str | Curve | TimeVector | None = src
|
|
90
|
+
self._is_stock = is_stock
|
|
91
|
+
self._is_flow = is_flow
|
|
92
|
+
self._is_profile = is_profile
|
|
93
|
+
self._is_level = is_level
|
|
94
|
+
self._profile = profile
|
|
95
|
+
|
|
96
|
+
# have to come after setting fields
|
|
97
|
+
# because fields are used to create
|
|
98
|
+
# error messages e.g. in __repr__
|
|
99
|
+
|
|
100
|
+
self._check_type(src, (str, Curve, TimeVector, type(None)))
|
|
101
|
+
self._check_type(is_stock, (bool, type(None)))
|
|
102
|
+
self._check_type(is_flow, (bool, type(None)))
|
|
103
|
+
self._check_type(is_level, (bool, type(None)))
|
|
104
|
+
self._check_type(is_profile, (bool, type(None)))
|
|
105
|
+
self._check_type(profile, (Expr, type(None)))
|
|
106
|
+
|
|
107
|
+
self._check_operations(operations)
|
|
108
|
+
if operations is None:
|
|
109
|
+
operations = "", []
|
|
110
|
+
self._operations: tuple[str, list[Expr]] = operations
|
|
111
|
+
|
|
112
|
+
def _check_operations(self, operations: tuple[str, list[Expr]] | None, expect_ops: bool = False) -> None:
|
|
113
|
+
if operations is None:
|
|
114
|
+
return
|
|
115
|
+
self._check_type(operations, tuple)
|
|
116
|
+
if len(operations) != 2: # noqa: PLR2004
|
|
117
|
+
message = f"Expected len(operations) == 2. Got: {operations}"
|
|
118
|
+
raise ValueError(message)
|
|
119
|
+
ops, args = operations
|
|
120
|
+
self._check_type(ops, str)
|
|
121
|
+
self._check_type(args, list)
|
|
122
|
+
if ops == "":
|
|
123
|
+
if expect_ops:
|
|
124
|
+
message = f"Expected ops, but got {operations}"
|
|
125
|
+
raise ValueError(message)
|
|
126
|
+
if len(args) > 0:
|
|
127
|
+
message = f"Expected ops to have length. Got {operations}"
|
|
128
|
+
raise ValueError(message)
|
|
129
|
+
return
|
|
130
|
+
if len(ops) != len(args) - 1:
|
|
131
|
+
message = f"Expected len(ops) == len(args) - 1. Got {operations}"
|
|
132
|
+
raise ValueError(message)
|
|
133
|
+
for op in ops:
|
|
134
|
+
if op not in "+-/*":
|
|
135
|
+
message = f"Expected all op in ops in +-*/. Got {operations}"
|
|
136
|
+
raise ValueError(message)
|
|
137
|
+
for ex in args:
|
|
138
|
+
self._check_type(ex, Expr)
|
|
139
|
+
|
|
140
|
+
def get_fingerprint(self) -> Fingerprint:
|
|
141
|
+
"""Return fingerprint."""
|
|
142
|
+
fingerprint = Fingerprint(self)
|
|
143
|
+
fingerprint.add("is_stock", self._is_stock)
|
|
144
|
+
fingerprint.add("is_flow", self._is_flow)
|
|
145
|
+
fingerprint.add("is_profile", self._is_profile)
|
|
146
|
+
fingerprint.add("is_level", self._is_level)
|
|
147
|
+
fingerprint.add("profile", self._profile)
|
|
148
|
+
if self._src:
|
|
149
|
+
fingerprint.add("src", self._src.get_fingerprint() if isinstance(self._src, TimeVector) else FingerprintRef(self._src))
|
|
150
|
+
fingerprint.add("operations", self._operations)
|
|
151
|
+
return fingerprint
|
|
152
|
+
|
|
153
|
+
def is_leaf(self) -> bool:
|
|
154
|
+
"""Return True if self is not an operation expression."""
|
|
155
|
+
return self._src is not None
|
|
156
|
+
|
|
157
|
+
def get_src(self) -> str | Curve | TimeVector | None:
|
|
158
|
+
"""Return str, Curve or TimeVector (either reference to Curve/TimeVector or Curve/TimeVector itself) or None if self is an operation expression."""
|
|
159
|
+
return self._src
|
|
160
|
+
|
|
161
|
+
def get_operations(self, expect_ops: bool, copy_list: bool) -> tuple[str, list[Expr]]:
|
|
162
|
+
"""Return ops, args. Users of this (low level) API must supply expect_ops and copy_list args."""
|
|
163
|
+
self._check_type(copy_list, bool)
|
|
164
|
+
self._verify_operations(expect_ops)
|
|
165
|
+
if copy_list:
|
|
166
|
+
ops, args = self._operations
|
|
167
|
+
return ops, copy(args)
|
|
168
|
+
return self._operations
|
|
169
|
+
|
|
170
|
+
def _verify_operations(self, expect_ops: bool = False) -> None:
|
|
171
|
+
self._check_operations(self._operations, expect_ops)
|
|
172
|
+
ops = self._operations[0]
|
|
173
|
+
|
|
174
|
+
if not ops:
|
|
175
|
+
return
|
|
176
|
+
|
|
177
|
+
has_add = "+" in ops
|
|
178
|
+
has_sub = "-" in ops
|
|
179
|
+
has_mul = "*" in ops
|
|
180
|
+
has_div = "/" in ops
|
|
181
|
+
|
|
182
|
+
if (has_add or has_sub) and (has_mul or has_div):
|
|
183
|
+
message = f"Found +- in same operation level as */ in operations {self._operations} "
|
|
184
|
+
raise ValueError(message)
|
|
185
|
+
|
|
186
|
+
if has_div:
|
|
187
|
+
seen_div = False
|
|
188
|
+
for op in ops:
|
|
189
|
+
if op == "/":
|
|
190
|
+
seen_div = True
|
|
191
|
+
continue
|
|
192
|
+
if seen_div and op != "/":
|
|
193
|
+
message = f"Found +-* after / in operations {self._operations}"
|
|
194
|
+
raise ValueError(message)
|
|
195
|
+
|
|
196
|
+
def is_flow(self) -> bool:
|
|
197
|
+
"""Return True if flow. Cannot be stock and flow."""
|
|
198
|
+
return self._is_flow
|
|
199
|
+
|
|
200
|
+
def is_stock(self) -> bool:
|
|
201
|
+
"""Return True if stock. Cannot be stock and flow."""
|
|
202
|
+
return self._is_stock
|
|
203
|
+
|
|
204
|
+
def is_level(self) -> bool:
|
|
205
|
+
"""Return True if level. Cannot be level and profile."""
|
|
206
|
+
return self._is_level
|
|
207
|
+
|
|
208
|
+
def is_profile(self) -> bool:
|
|
209
|
+
"""Return True if profile. Cannot be level and profile."""
|
|
210
|
+
return self._is_profile
|
|
211
|
+
|
|
212
|
+
def get_profile(self) -> Expr | None:
|
|
213
|
+
"""Return Expr representing profile. Implies self.is_level() is True."""
|
|
214
|
+
return self._profile
|
|
215
|
+
|
|
216
|
+
def set_profile(self, profile: Expr | None) -> None:
|
|
217
|
+
"""Set the profile of the Expr. Implies self.is_level() is True."""
|
|
218
|
+
if not self.is_level():
|
|
219
|
+
raise ValueError("Cannot set profile on Expr that is not a level.")
|
|
220
|
+
self._profile = profile
|
|
221
|
+
|
|
222
|
+
def _analyze_op(self, op: str, other: Expr) -> tuple[bool, bool, bool, bool, Expr | None]:
|
|
223
|
+
flow = (True, False)
|
|
224
|
+
stock = (False, True)
|
|
225
|
+
level = (True, False)
|
|
226
|
+
profile = (False, True)
|
|
227
|
+
none = (False, False)
|
|
228
|
+
|
|
229
|
+
supported_cases = {
|
|
230
|
+
# all op supported for none
|
|
231
|
+
("+", none, none, none, none): (none, none, None),
|
|
232
|
+
("-", none, none, none, none): (none, none, None),
|
|
233
|
+
("*", none, none, none, none): (none, none, None),
|
|
234
|
+
("/", none, none, none, none): (none, none, None),
|
|
235
|
+
# + flow level
|
|
236
|
+
("+", flow, level, flow, level): (flow, level, None),
|
|
237
|
+
# * flow level
|
|
238
|
+
("*", flow, level, none, none): (flow, level, self.get_profile()),
|
|
239
|
+
("*", none, none, flow, level): (flow, level, other.get_profile()),
|
|
240
|
+
# / flow level
|
|
241
|
+
("/", flow, level, none, none): (flow, level, self.get_profile()),
|
|
242
|
+
("/", flow, level, flow, level): (none, none, None),
|
|
243
|
+
# + stock level
|
|
244
|
+
("+", stock, level, stock, level): (stock, level, None),
|
|
245
|
+
# * stock level
|
|
246
|
+
("*", stock, level, none, level): (stock, level, None),
|
|
247
|
+
("*", none, level, stock, level): (stock, level, None),
|
|
248
|
+
("*", stock, level, none, none): (stock, level, self.get_profile()),
|
|
249
|
+
("*", none, none, stock, level): (stock, level, other.get_profile()),
|
|
250
|
+
# / stock level
|
|
251
|
+
("/", stock, level, none, level): (stock, level, None),
|
|
252
|
+
("/", stock, level, none, none): (stock, level, self.get_profile()),
|
|
253
|
+
("/", stock, level, stock, level): (none, none, None),
|
|
254
|
+
# level * level ok if one is flow (i.e. price * volume) or none (co2_eff / eff)
|
|
255
|
+
("*", flow, level, none, level): (flow, level, None),
|
|
256
|
+
("*", none, level, flow, level): (flow, level, None),
|
|
257
|
+
("/", flow, level, none, level): (flow, level, None),
|
|
258
|
+
("/", none, level, none, level): (none, level, None),
|
|
259
|
+
("*", none, level, none, level): (none, level, None),
|
|
260
|
+
# profile
|
|
261
|
+
("+", none, profile, none, profile): (none, profile, None),
|
|
262
|
+
("-", none, profile, none, profile): (none, profile, None),
|
|
263
|
+
("/", none, profile, none, none): (none, profile, None),
|
|
264
|
+
("*", none, profile, none, none): (none, profile, None),
|
|
265
|
+
("*", none, none, none, profile): (none, profile, None),
|
|
266
|
+
("/", none, none, none, profile): (none, profile, None),
|
|
267
|
+
# level
|
|
268
|
+
("+", none, level, none, level): (none, level, None),
|
|
269
|
+
("-", none, level, none, level): (none, level, None),
|
|
270
|
+
("/", none, level, none, none): (none, level, self.get_profile()),
|
|
271
|
+
("*", none, level, none, none): (none, level, self.get_profile()),
|
|
272
|
+
("*", none, none, none, level): (none, level, other.get_profile()),
|
|
273
|
+
("/", none, none, none, level): (none, level, other.get_profile()),
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
case = (
|
|
277
|
+
op,
|
|
278
|
+
(self.is_flow(), self.is_stock()),
|
|
279
|
+
(self.is_level(), self.is_profile()),
|
|
280
|
+
(other.is_flow(), other.is_stock()),
|
|
281
|
+
(other.is_level(), other.is_profile()),
|
|
282
|
+
)
|
|
283
|
+
|
|
284
|
+
if case not in supported_cases:
|
|
285
|
+
printable_case = {
|
|
286
|
+
"op": case[0],
|
|
287
|
+
"self_is_flow": case[1][0],
|
|
288
|
+
"self_is_stock": case[1][1],
|
|
289
|
+
"self_is_level": case[2][0],
|
|
290
|
+
"self_is_profile": case[2][1],
|
|
291
|
+
"other_is_flow": case[3][0],
|
|
292
|
+
"other_is_stock": case[3][1],
|
|
293
|
+
"other_is_level": case[4][0],
|
|
294
|
+
"other_is_profile": case[4][1],
|
|
295
|
+
}
|
|
296
|
+
message = f"Unsupported case:\n{printable_case}\nexpression:\n{self} {op} {other}."
|
|
297
|
+
raise ValueError(message)
|
|
298
|
+
|
|
299
|
+
((is_flow, is_stock), (is_level, is_profile), profile) = supported_cases[case]
|
|
300
|
+
|
|
301
|
+
return is_stock, is_flow, is_level, is_profile, profile
|
|
302
|
+
|
|
303
|
+
@staticmethod
|
|
304
|
+
def _is_number(src: str) -> bool:
|
|
305
|
+
try:
|
|
306
|
+
float(src)
|
|
307
|
+
return True
|
|
308
|
+
except ValueError:
|
|
309
|
+
return False
|
|
310
|
+
|
|
311
|
+
def _create_op_expr( # noqa: C901
|
|
312
|
+
self,
|
|
313
|
+
op: str,
|
|
314
|
+
other: Expr | int | float,
|
|
315
|
+
is_rhs: bool,
|
|
316
|
+
) -> Expr:
|
|
317
|
+
if isinstance(other, Expr):
|
|
318
|
+
is_stock, is_flow, is_level, is_profile, profile = self._analyze_op(op, other)
|
|
319
|
+
|
|
320
|
+
x, y = (other, self) if is_rhs else (self, other)
|
|
321
|
+
|
|
322
|
+
xisconst = isinstance(x.get_src(), ConstantTimeVector)
|
|
323
|
+
yisconst = isinstance(y.get_src(), ConstantTimeVector)
|
|
324
|
+
if xisconst and yisconst:
|
|
325
|
+
xtv = x.get_src()
|
|
326
|
+
ytv = y.get_src()
|
|
327
|
+
is_combinable_tv = (
|
|
328
|
+
xtv.get_unit() == ytv.get_unit()
|
|
329
|
+
and xtv.is_max_level() == ytv.is_max_level()
|
|
330
|
+
and xtv.is_zero_one_profile() == ytv.is_zero_one_profile()
|
|
331
|
+
and xtv.get_reference_period() == ytv.get_reference_period()
|
|
332
|
+
)
|
|
333
|
+
if is_combinable_tv:
|
|
334
|
+
is_combinable_expr = (
|
|
335
|
+
x.is_level() == y.is_level()
|
|
336
|
+
and x.is_profile() == y.is_profile()
|
|
337
|
+
and x.is_flow() == y.is_flow()
|
|
338
|
+
and x.is_stock() == y.is_stock()
|
|
339
|
+
and x.get_profile() == y.get_profile()
|
|
340
|
+
)
|
|
341
|
+
if is_combinable_expr:
|
|
342
|
+
xscalar = xtv.get_vector(is_float32=True)[0]
|
|
343
|
+
yscalar = ytv.get_vector(is_float32=True)[0]
|
|
344
|
+
if op == "+":
|
|
345
|
+
scalar = xscalar + yscalar
|
|
346
|
+
elif op == "-":
|
|
347
|
+
scalar = xscalar - yscalar
|
|
348
|
+
elif op == "*":
|
|
349
|
+
scalar = xscalar * yscalar
|
|
350
|
+
elif op == "/":
|
|
351
|
+
scalar = xscalar / yscalar
|
|
352
|
+
return Expr(
|
|
353
|
+
src=ConstantTimeVector(
|
|
354
|
+
scalar=scalar,
|
|
355
|
+
unit=xtv.get_unit(),
|
|
356
|
+
is_max_level=xtv.is_max_level(),
|
|
357
|
+
is_zero_one_profile=xtv.is_zero_one_profile(),
|
|
358
|
+
reference_period=xtv.get_reference_period(),
|
|
359
|
+
),
|
|
360
|
+
is_stock=x.is_stock(),
|
|
361
|
+
is_flow=x.is_flow(),
|
|
362
|
+
is_profile=x.is_profile(),
|
|
363
|
+
is_level=x.is_level(),
|
|
364
|
+
profile=x.get_profile(),
|
|
365
|
+
operations=None,
|
|
366
|
+
)
|
|
367
|
+
|
|
368
|
+
ops, args = x.get_operations(expect_ops=False, copy_list=True)
|
|
369
|
+
|
|
370
|
+
if not ops:
|
|
371
|
+
ops = op
|
|
372
|
+
args = [x, y]
|
|
373
|
+
else:
|
|
374
|
+
last_op = ops[-1]
|
|
375
|
+
if last_op == op or (op in "+-" and last_op in "+-") or (last_op == "*" and op == "/"):
|
|
376
|
+
ops = f"{ops}{op}"
|
|
377
|
+
args.append(y)
|
|
378
|
+
else:
|
|
379
|
+
ops = op
|
|
380
|
+
args = [x, y]
|
|
381
|
+
|
|
382
|
+
return Expr(
|
|
383
|
+
src=None,
|
|
384
|
+
is_flow=is_flow,
|
|
385
|
+
is_stock=is_stock,
|
|
386
|
+
is_level=is_level,
|
|
387
|
+
is_profile=is_profile,
|
|
388
|
+
profile=profile,
|
|
389
|
+
operations=(ops, args),
|
|
390
|
+
)
|
|
391
|
+
|
|
392
|
+
if self._is_number(other):
|
|
393
|
+
if op in "*/":
|
|
394
|
+
other_expr = Expr(src=ConstantTimeVector(float(other), is_max_level=False))
|
|
395
|
+
return self._create_op_expr(op=op, other=other_expr, is_rhs=is_rhs)
|
|
396
|
+
|
|
397
|
+
if op in "+" and other == 0: # Comes from sum(expr_list). See sum() noqa
|
|
398
|
+
return self # TODO: Also accept 0 - Expr and -Expr?
|
|
399
|
+
|
|
400
|
+
message = f"Only support multiplication and division with numbers, got {op} and {other}."
|
|
401
|
+
raise ValueError(message)
|
|
402
|
+
|
|
403
|
+
message = f"Only support Expr, int, float. Got unsupported type {type(other).__name__}."
|
|
404
|
+
raise TypeError(message)
|
|
405
|
+
|
|
406
|
+
def __add__(self, other: object) -> Expr: # noqa: D105
|
|
407
|
+
return self._create_op_expr("+", other, is_rhs=False)
|
|
408
|
+
|
|
409
|
+
def __sub__(self, other: object) -> Expr: # noqa: D105
|
|
410
|
+
return self._create_op_expr("-", other, is_rhs=False)
|
|
411
|
+
|
|
412
|
+
def __mul__(self, other: object) -> Expr: # noqa: D105
|
|
413
|
+
return self._create_op_expr("*", other, is_rhs=False)
|
|
414
|
+
|
|
415
|
+
def __truediv__(self, other: object) -> Expr: # noqa: D105
|
|
416
|
+
return self._create_op_expr("/", other, is_rhs=False)
|
|
417
|
+
|
|
418
|
+
def __radd__(self, other: object) -> Expr: # noqa: D105
|
|
419
|
+
return self._create_op_expr("+", other, is_rhs=True)
|
|
420
|
+
|
|
421
|
+
def __rsub__(self, other: object) -> Expr: # noqa: D105
|
|
422
|
+
return self._create_op_expr("-", other, is_rhs=True)
|
|
423
|
+
|
|
424
|
+
def __rmul__(self, other: object) -> Expr: # noqa: D105
|
|
425
|
+
return self._create_op_expr("*", other, is_rhs=True)
|
|
426
|
+
|
|
427
|
+
def __rtruediv__(self, other: object) -> Expr: # noqa: D105
|
|
428
|
+
return self._create_op_expr("/", other, is_rhs=True)
|
|
429
|
+
|
|
430
|
+
def __repr__(self) -> str:
|
|
431
|
+
"""Represent Expr as str."""
|
|
432
|
+
if self._src is not None:
|
|
433
|
+
return f"Expr({self._src})"
|
|
434
|
+
ops, args = self.get_operations(expect_ops=True, copy_list=False)
|
|
435
|
+
out = f"{args[0]}"
|
|
436
|
+
for op, arg in zip(ops, args[1:], strict=True):
|
|
437
|
+
out = f"{out} {op} {arg}"
|
|
438
|
+
return f"Expr({out})"
|
|
439
|
+
|
|
440
|
+
def __eq__(self, other) -> bool: # noqa: ANN001
|
|
441
|
+
"""Check if self and other are equal."""
|
|
442
|
+
if not isinstance(other, type(self)):
|
|
443
|
+
return False
|
|
444
|
+
return (
|
|
445
|
+
self._is_flow == other._is_flow
|
|
446
|
+
and self._is_level == other._is_level
|
|
447
|
+
and self._src == other._src
|
|
448
|
+
and self._is_stock == other._is_stock
|
|
449
|
+
and self._is_profile == other._is_profile
|
|
450
|
+
and self._profile == other._profile
|
|
451
|
+
and self._operations[0] == other._operations[0]
|
|
452
|
+
and len(self._operations[1]) == len(other._operations[1])
|
|
453
|
+
and all([self._operations[1][i] == other._operations[1][i] for i in range(len(self._operations[1]))]) # noqa: SLF001
|
|
454
|
+
)
|
|
455
|
+
|
|
456
|
+
def __hash__(self) -> int:
|
|
457
|
+
"""Compute hash value.."""
|
|
458
|
+
return hash(
|
|
459
|
+
(
|
|
460
|
+
self._is_flow,
|
|
461
|
+
self._is_stock,
|
|
462
|
+
self._is_level,
|
|
463
|
+
self._is_profile,
|
|
464
|
+
self._src,
|
|
465
|
+
self._profile,
|
|
466
|
+
self._operations[0],
|
|
467
|
+
tuple(self._operations[1]),
|
|
468
|
+
),
|
|
469
|
+
)
|
|
470
|
+
|
|
471
|
+
def add_loaders(self, loaders: set[Loader]) -> None:
|
|
472
|
+
"""Add all loaders stored in TimeVector or Curve within Expr to loaders."""
|
|
473
|
+
if self.is_leaf():
|
|
474
|
+
src = self.get_src()
|
|
475
|
+
if isinstance(src, TimeVector | LoadedCurve):
|
|
476
|
+
loader = src.get_loader()
|
|
477
|
+
if loader is not None:
|
|
478
|
+
loaders.add(loader)
|
|
479
|
+
return
|
|
480
|
+
__, args = self.get_operations(expect_ops=True, copy_list=False)
|
|
481
|
+
for arg in args:
|
|
482
|
+
arg.add_loaders(loaders)
|
|
483
|
+
|
|
484
|
+
|
|
485
|
+
# Proposed new way of creating Expr in classes.
|
|
486
|
+
def ensure_expr(
|
|
487
|
+
value: Expr | str | Curve | TimeVector | None, # technically anything that can be converted to float. Typehint for this?
|
|
488
|
+
is_flow: bool = False,
|
|
489
|
+
is_stock: bool = False,
|
|
490
|
+
is_level: bool = False,
|
|
491
|
+
is_profile: bool = False,
|
|
492
|
+
profile: Expr | None = None,
|
|
493
|
+
) -> Expr | None:
|
|
494
|
+
"""
|
|
495
|
+
Ensure that the value is an expression of the expected type or create one if possible.
|
|
496
|
+
|
|
497
|
+
Args:
|
|
498
|
+
value (Expr | str | None): The value to check.
|
|
499
|
+
is_flow (str): If the Expr is a flow. Cannot be True if is_stock is True.
|
|
500
|
+
is_stock (str): If the Expr is a stock. Cannot be True if is_flow is True.
|
|
501
|
+
is_level (bool): Wether the Expr represents a level. Cannot be True if is_profile is True.
|
|
502
|
+
is_profile (bool): Wether the Expr represents a profile. Cannot be True if is_level is True.
|
|
503
|
+
profile (Expr | None): If the Expr is a level, this should be its profile.
|
|
504
|
+
|
|
505
|
+
Returns:
|
|
506
|
+
value (Expr | str): The value as an expression of the expected type or None.
|
|
507
|
+
|
|
508
|
+
"""
|
|
509
|
+
if not isinstance(value, (str, Expr, Curve, TimeVector)) and value is not None:
|
|
510
|
+
msg = f"Expected value to be of type Expr, str, Curve, TimeVector or None. Got {type(value).__name__}."
|
|
511
|
+
raise TypeError(msg)
|
|
512
|
+
|
|
513
|
+
if value is None:
|
|
514
|
+
return None
|
|
515
|
+
|
|
516
|
+
if isinstance(value, Expr):
|
|
517
|
+
# Check wether given Expr matches expected flow, stock, profile and level status.
|
|
518
|
+
if value.is_flow() != is_flow or value.is_stock() != is_stock or value.is_level() != is_level or value.is_profile() != is_profile:
|
|
519
|
+
message = (
|
|
520
|
+
"Given Expr has a mismatch between expected and actual flow/stock or level/profile status:\nExpected: "
|
|
521
|
+
f"is_flow - {is_flow}, is_stock - {is_stock}, is_level - {is_level}, is_profile - {is_profile}\n"
|
|
522
|
+
f"Actual: is_flow - {value.is_flow()}, is_stock - {value.is_stock()}, "
|
|
523
|
+
f"is_level - {value.is_level()}, is_profile - {value.is_profile()}"
|
|
524
|
+
)
|
|
525
|
+
raise ValueError(message)
|
|
526
|
+
return value
|
|
527
|
+
|
|
528
|
+
return Expr(
|
|
529
|
+
src=value,
|
|
530
|
+
is_flow=is_flow,
|
|
531
|
+
is_stock=is_stock,
|
|
532
|
+
is_level=is_level,
|
|
533
|
+
is_profile=is_profile,
|
|
534
|
+
profile=profile,
|
|
535
|
+
)
|
|
536
|
+
|
|
537
|
+
|
|
538
|
+
def get_profile_exprs_from_leaf_levels(expr: Expr) -> list[Expr]:
|
|
539
|
+
"""
|
|
540
|
+
Get all profile expressions from leaf-level Expr objects that are marked as levels.
|
|
541
|
+
|
|
542
|
+
Args:
|
|
543
|
+
expr (Expr): The starting Expr object.
|
|
544
|
+
|
|
545
|
+
Returns:
|
|
546
|
+
list[Expr]: A list of profile expressions from leaf-level Expr objects.
|
|
547
|
+
|
|
548
|
+
"""
|
|
549
|
+
profile_exprs = []
|
|
550
|
+
|
|
551
|
+
def _traverse(expr: Expr) -> None:
|
|
552
|
+
if expr.is_leaf():
|
|
553
|
+
if expr.is_level() and expr.get_profile() is not None:
|
|
554
|
+
profile_exprs.append(expr.get_profile())
|
|
555
|
+
return
|
|
556
|
+
|
|
557
|
+
# Recursively traverse the arguments of the expression
|
|
558
|
+
_, args = expr.get_operations(expect_ops=False, copy_list=False)
|
|
559
|
+
for arg in args:
|
|
560
|
+
_traverse(arg)
|
|
561
|
+
|
|
562
|
+
_traverse(expr)
|
|
563
|
+
return profile_exprs
|
|
564
|
+
|
|
565
|
+
|
|
566
|
+
def get_leaf_profiles(expr: Expr) -> list[Expr]:
|
|
567
|
+
"""
|
|
568
|
+
Get all leaf profile expressions from an Expr object.
|
|
569
|
+
|
|
570
|
+
Args:
|
|
571
|
+
expr (Expr): The starting Expr object.
|
|
572
|
+
|
|
573
|
+
Returns:
|
|
574
|
+
list[Expr]: A list of leaf profile expressions.
|
|
575
|
+
|
|
576
|
+
"""
|
|
577
|
+
leaf_profiles = []
|
|
578
|
+
|
|
579
|
+
def _traverse(expr: Expr) -> None:
|
|
580
|
+
if expr.is_leaf():
|
|
581
|
+
if expr.is_profile():
|
|
582
|
+
leaf_profiles.append(expr)
|
|
583
|
+
return
|
|
584
|
+
|
|
585
|
+
# Recursively traverse the arguments of the expression
|
|
586
|
+
_, args = expr.get_operations(expect_ops=False, copy_list=False)
|
|
587
|
+
for arg in args:
|
|
588
|
+
_traverse(arg)
|
|
589
|
+
|
|
590
|
+
_traverse(expr)
|
|
591
|
+
return leaf_profiles
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# framcore/expressions/__init__.py
|
|
2
|
+
|
|
3
|
+
from framcore.expressions.Expr import Expr, ensure_expr, get_leaf_profiles, get_profile_exprs_from_leaf_levels
|
|
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_leaf_profiles",
|
|
22
|
+
"get_level_value",
|
|
23
|
+
"get_profile_exprs_from_leaf_levels",
|
|
24
|
+
"get_profile_vector",
|
|
25
|
+
"get_timeindexes_from_expr",
|
|
26
|
+
"get_unit_conversion_factor",
|
|
27
|
+
"get_units_from_expr",
|
|
28
|
+
"is_convertable",
|
|
29
|
+
"validate_unit_conversion_fastpaths",
|
|
30
|
+
]
|