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,483 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Implementation of _get_constant_from_expr.
|
|
3
|
+
|
|
4
|
+
The first implementation used the _sympy_fallback function in all cases.
|
|
5
|
+
This turned out to be very slow for large expressions. Therefore,
|
|
6
|
+
we collected data on common expressions that turn up in aggregation,
|
|
7
|
+
and added fast paths for these cases.
|
|
8
|
+
|
|
9
|
+
Since this results in more code than the original,
|
|
10
|
+
we put this function in its own file.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
from time import time
|
|
16
|
+
from typing import TYPE_CHECKING
|
|
17
|
+
|
|
18
|
+
import sympy
|
|
19
|
+
|
|
20
|
+
from framcore.curves import Curve
|
|
21
|
+
from framcore.events import send_warning_event
|
|
22
|
+
from framcore.expressions import Expr
|
|
23
|
+
from framcore.expressions._utils import _ensure_real_expr, _load_model_and_create_model_db, _lookup_expr_from_constants_with_units
|
|
24
|
+
from framcore.expressions.units import _get_scalar_from_expr, _unit_str_to_sym, get_unit_conversion_factor
|
|
25
|
+
from framcore.querydbs import QueryDB
|
|
26
|
+
from framcore.timeindexes import FixedFrequencyTimeIndex, SinglePeriodTimeIndex
|
|
27
|
+
from framcore.timevectors import ConstantTimeVector, TimeVector
|
|
28
|
+
|
|
29
|
+
if TYPE_CHECKING:
|
|
30
|
+
from framcore import Model
|
|
31
|
+
|
|
32
|
+
_DEBUG = False
|
|
33
|
+
_DEBUG_ROUND_DECIMALS = 5
|
|
34
|
+
_WARN_IF_FALLBACK = True
|
|
35
|
+
_WARN_MAX_ELAPSED_SECONDS = 0.1
|
|
36
|
+
|
|
37
|
+
_NUM_LEAF = 0
|
|
38
|
+
_NUM_FALLBACK = 0
|
|
39
|
+
_NUM_FASTPATH_PRODUCT = 0
|
|
40
|
+
_NUM_FASTPATH_AGGREGATION = 0
|
|
41
|
+
_NUM_FASTPATH_SUM = 0
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def _get_case_counts() -> dict[str, int]:
|
|
45
|
+
"""
|
|
46
|
+
Return dict of counts for different cases of _get_constant_from_expr.
|
|
47
|
+
|
|
48
|
+
Useful for fastpath development.
|
|
49
|
+
"""
|
|
50
|
+
return {
|
|
51
|
+
"fastpath_leaf": _NUM_LEAF,
|
|
52
|
+
"fallback": _NUM_FALLBACK,
|
|
53
|
+
"fastpath_sum": _NUM_FASTPATH_SUM,
|
|
54
|
+
"fastpath_product": _NUM_FASTPATH_PRODUCT,
|
|
55
|
+
"fastpath_aggregation": _NUM_FASTPATH_AGGREGATION,
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def _get_constant_from_expr(
|
|
60
|
+
expr: Expr,
|
|
61
|
+
db: QueryDB | Model,
|
|
62
|
+
unit: str | None,
|
|
63
|
+
data_dim: SinglePeriodTimeIndex,
|
|
64
|
+
scen_dim: FixedFrequencyTimeIndex,
|
|
65
|
+
is_max: bool,
|
|
66
|
+
) -> float:
|
|
67
|
+
if not isinstance(expr, Expr):
|
|
68
|
+
message = f"Expected Expr, got {expr}"
|
|
69
|
+
raise ValueError(message)
|
|
70
|
+
|
|
71
|
+
db = _load_model_and_create_model_db(db)
|
|
72
|
+
|
|
73
|
+
real_expr = _ensure_real_expr(expr, db)
|
|
74
|
+
|
|
75
|
+
constants_with_units = dict()
|
|
76
|
+
|
|
77
|
+
expr_str = _update_constants_with_units(
|
|
78
|
+
constants_with_units,
|
|
79
|
+
real_expr,
|
|
80
|
+
db,
|
|
81
|
+
data_dim,
|
|
82
|
+
scen_dim,
|
|
83
|
+
is_max,
|
|
84
|
+
)
|
|
85
|
+
|
|
86
|
+
# counts for debug and optimization
|
|
87
|
+
global _NUM_LEAF # noqa: PLW0603
|
|
88
|
+
global _NUM_FALLBACK # noqa: PLW0603
|
|
89
|
+
global _NUM_FASTPATH_PRODUCT # noqa: PLW0603
|
|
90
|
+
global _NUM_FASTPATH_AGGREGATION # noqa: PLW0603
|
|
91
|
+
global _NUM_FASTPATH_SUM # noqa: PLW0603
|
|
92
|
+
|
|
93
|
+
fastpath = None
|
|
94
|
+
|
|
95
|
+
if real_expr.is_leaf():
|
|
96
|
+
_NUM_LEAF += 1
|
|
97
|
+
fastpath = _fastpath_leaf(constants_with_units, real_expr, unit)
|
|
98
|
+
|
|
99
|
+
elif _is_fastpath_sum(expr):
|
|
100
|
+
_NUM_FASTPATH_SUM += 1
|
|
101
|
+
fastpath = _fastpath_sum(constants_with_units, real_expr, unit)
|
|
102
|
+
|
|
103
|
+
elif _is_fastpath_product(real_expr):
|
|
104
|
+
_NUM_FASTPATH_PRODUCT += 1
|
|
105
|
+
fastpath = _fastpath_product(constants_with_units, real_expr, unit)
|
|
106
|
+
|
|
107
|
+
elif _is_fastpath_aggregation(real_expr):
|
|
108
|
+
_NUM_FASTPATH_AGGREGATION += 1
|
|
109
|
+
fastpath = _fastpath_aggregation(constants_with_units, real_expr, unit)
|
|
110
|
+
|
|
111
|
+
if fastpath is not None and _DEBUG is not True:
|
|
112
|
+
return fastpath
|
|
113
|
+
|
|
114
|
+
_NUM_FALLBACK += 1
|
|
115
|
+
t = time()
|
|
116
|
+
fallback = _sympy_fallback(constants_with_units, expr_str, unit)
|
|
117
|
+
elapsed_seconds_fallback = time() - t
|
|
118
|
+
|
|
119
|
+
if _DEBUG and fastpath is not None and round(fastpath, _DEBUG_ROUND_DECIMALS) != round(fallback, _DEBUG_ROUND_DECIMALS):
|
|
120
|
+
message = f"Different results!\nExpr {real_expr}\nwith symbolic representation {expr_str}\nfastpath {fastpath} and fallback {fallback}"
|
|
121
|
+
raise RuntimeError(message)
|
|
122
|
+
|
|
123
|
+
if _DEBUG is False and _WARN_IF_FALLBACK is True and elapsed_seconds_fallback > _WARN_MAX_ELAPSED_SECONDS:
|
|
124
|
+
message = f"fallback used {elapsed_seconds_fallback} seconds for (symbolic) expr: {expr_str}"
|
|
125
|
+
send_warning_event(sender=_get_constant_from_expr, message=message)
|
|
126
|
+
|
|
127
|
+
return fallback
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
def _update_constants_with_units(
|
|
131
|
+
constants_with_units: dict[str, tuple],
|
|
132
|
+
real_expr: Expr,
|
|
133
|
+
db: QueryDB,
|
|
134
|
+
data_dim: SinglePeriodTimeIndex,
|
|
135
|
+
scen_dim: FixedFrequencyTimeIndex,
|
|
136
|
+
is_max: bool,
|
|
137
|
+
) -> str:
|
|
138
|
+
"""Extract symbol, constant value and unit info from all leaf expressions of real_expr."""
|
|
139
|
+
# To avoid circular import TODO: improve?
|
|
140
|
+
from framcore.expressions.queries import _get_level_value_from_timevector
|
|
141
|
+
|
|
142
|
+
if real_expr.is_leaf():
|
|
143
|
+
is_level = real_expr.is_level()
|
|
144
|
+
is_profile = real_expr.is_profile()
|
|
145
|
+
|
|
146
|
+
src = real_expr.get_src()
|
|
147
|
+
|
|
148
|
+
if isinstance(src, str) and db.has_key(src):
|
|
149
|
+
obj = db.get(src)
|
|
150
|
+
assert not isinstance(obj, Expr), f"{obj}"
|
|
151
|
+
assert isinstance(obj, TimeVector | Curve), f"{obj}"
|
|
152
|
+
|
|
153
|
+
elif isinstance(src, ConstantTimeVector):
|
|
154
|
+
obj: ConstantTimeVector = src
|
|
155
|
+
src = obj.get_expr_str()
|
|
156
|
+
else:
|
|
157
|
+
message = f"Unexpected value for src: {src}\nin expr {real_expr}"
|
|
158
|
+
raise ValueError(message)
|
|
159
|
+
|
|
160
|
+
if src in constants_with_units:
|
|
161
|
+
sym, value, unit = constants_with_units[src]
|
|
162
|
+
return sym
|
|
163
|
+
|
|
164
|
+
if isinstance(obj, TimeVector):
|
|
165
|
+
obj: TimeVector
|
|
166
|
+
|
|
167
|
+
# added to support any_expr * ConstantTimeVector
|
|
168
|
+
times_constant_case = (not is_profile) and isinstance(obj, ConstantTimeVector)
|
|
169
|
+
|
|
170
|
+
if is_level or times_constant_case:
|
|
171
|
+
unit = obj.get_unit()
|
|
172
|
+
profile_expr = real_expr.get_profile()
|
|
173
|
+
value = _get_level_value_from_timevector(obj, db, unit, data_dim, scen_dim, is_max, profile_expr)
|
|
174
|
+
sym = f"x{len(constants_with_units)}"
|
|
175
|
+
constants_with_units[src] = (sym, float(value), unit)
|
|
176
|
+
return sym
|
|
177
|
+
|
|
178
|
+
if not is_profile:
|
|
179
|
+
message = f"Unsupported case where expr is not level and not profile:\nexpr: {real_expr}\nobj: {obj}"
|
|
180
|
+
raise ValueError(message)
|
|
181
|
+
|
|
182
|
+
assert is_profile
|
|
183
|
+
|
|
184
|
+
raise NotImplementedError("Profile TimeVector not implemented yet")
|
|
185
|
+
raise NotImplementedError("Curve not implemented yet")
|
|
186
|
+
|
|
187
|
+
ops, args = real_expr.get_operations(expect_ops=True, copy_list=False)
|
|
188
|
+
|
|
189
|
+
x = _update_constants_with_units(constants_with_units, args[0], db, data_dim, scen_dim, is_max)
|
|
190
|
+
out = f"{x}"
|
|
191
|
+
for op, arg in zip(ops, args[1:], strict=True):
|
|
192
|
+
x = _update_constants_with_units(constants_with_units, arg, db, data_dim, scen_dim, is_max)
|
|
193
|
+
out = f"{out} {op} {x}"
|
|
194
|
+
|
|
195
|
+
return f"({out})"
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
def _is_fastpath_sum(expr: Expr) -> bool:
|
|
199
|
+
"""E.g. x0 + x1 + x2 + .. where x is leaf."""
|
|
200
|
+
if expr.is_leaf():
|
|
201
|
+
return True
|
|
202
|
+
ops, args = expr.get_operations(expect_ops=True, copy_list=False)
|
|
203
|
+
if ops[0] not in "+-":
|
|
204
|
+
return False
|
|
205
|
+
return all(arg.is_leaf() for arg in args)
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
def _is_fastpath_product(expr: Expr) -> bool:
|
|
209
|
+
"""E.g. x1 * (x2 + x3), or x1 * x2 * x3 where x is leaf."""
|
|
210
|
+
if expr.is_leaf():
|
|
211
|
+
return True
|
|
212
|
+
ops, args = expr.get_operations(expect_ops=True, copy_list=False)
|
|
213
|
+
if not all(op == "*" for op in ops):
|
|
214
|
+
return False
|
|
215
|
+
return all(arg.is_leaf() or _is_fastpath_sum(arg) for arg in args)
|
|
216
|
+
|
|
217
|
+
|
|
218
|
+
def _is_fastpath_sum(expr: Expr) -> bool:
|
|
219
|
+
"""x1 + x2 + .. + xn where x is leaf and n >= 1."""
|
|
220
|
+
if expr.is_leaf():
|
|
221
|
+
return True
|
|
222
|
+
ops, args = expr.get_operations(expect_ops=True, copy_list=False)
|
|
223
|
+
if ops[0] not in "+-":
|
|
224
|
+
return False
|
|
225
|
+
return all(arg.is_leaf() for op, arg in zip(ops, args[1:], strict=True))
|
|
226
|
+
|
|
227
|
+
|
|
228
|
+
def _is_fastpath_sum_of_products(expr: Expr) -> bool:
|
|
229
|
+
"""E.g. x1 * (x2 + x3) + x4 * x5 where x is leaf."""
|
|
230
|
+
if expr.is_leaf():
|
|
231
|
+
return True
|
|
232
|
+
if _is_fastpath_product(expr):
|
|
233
|
+
return True
|
|
234
|
+
ops, args = expr.get_operations(expect_ops=True, copy_list=False)
|
|
235
|
+
if ops[0] not in "+-":
|
|
236
|
+
return False
|
|
237
|
+
return all(_is_fastpath_product(arg) for arg in args)
|
|
238
|
+
|
|
239
|
+
|
|
240
|
+
def _is_fastpath_aggregation(expr: Expr) -> bool:
|
|
241
|
+
"""E.g. ((x1 * (x2 + x3) + x4 * x5) / (x6 + x7)) where x is leaf."""
|
|
242
|
+
ops, args = expr.get_operations(expect_ops=True, copy_list=False)
|
|
243
|
+
if ops != "/":
|
|
244
|
+
return False
|
|
245
|
+
try:
|
|
246
|
+
numerator, denominator = args
|
|
247
|
+
except Exception:
|
|
248
|
+
return False
|
|
249
|
+
if not _is_fastpath_sum_of_products(numerator):
|
|
250
|
+
return False
|
|
251
|
+
return _is_fastpath_sum(denominator)
|
|
252
|
+
|
|
253
|
+
|
|
254
|
+
def _fastpath_leaf(
|
|
255
|
+
constants_with_units: dict[str, tuple],
|
|
256
|
+
expr: Expr,
|
|
257
|
+
target_unit: str | None,
|
|
258
|
+
) -> float:
|
|
259
|
+
__, value, unit = _lookup_expr_from_constants_with_units(constants_with_units, expr)
|
|
260
|
+
if unit == target_unit:
|
|
261
|
+
return value
|
|
262
|
+
return get_unit_conversion_factor(unit, target_unit) * value
|
|
263
|
+
|
|
264
|
+
|
|
265
|
+
def _fastpath_sum(
|
|
266
|
+
constants_with_units: dict[str, tuple],
|
|
267
|
+
expr: Expr,
|
|
268
|
+
target_unit: str | None,
|
|
269
|
+
) -> float:
|
|
270
|
+
d = _get_fastpath_sum_dict(constants_with_units, expr)
|
|
271
|
+
|
|
272
|
+
out = 0.0
|
|
273
|
+
for unit, value in d.items():
|
|
274
|
+
if value == 0.0:
|
|
275
|
+
continue
|
|
276
|
+
if unit == target_unit:
|
|
277
|
+
out += value
|
|
278
|
+
else:
|
|
279
|
+
out += value * get_unit_conversion_factor(unit, target_unit)
|
|
280
|
+
|
|
281
|
+
return out
|
|
282
|
+
|
|
283
|
+
|
|
284
|
+
def _fastpath_product(
|
|
285
|
+
constants_with_units: dict[str, tuple],
|
|
286
|
+
expr: Expr,
|
|
287
|
+
target_unit: str | None,
|
|
288
|
+
) -> float:
|
|
289
|
+
d = _get_fastpath_product_dict(constants_with_units, expr)
|
|
290
|
+
|
|
291
|
+
out = 1.0
|
|
292
|
+
from_unit = None
|
|
293
|
+
for unit, value in d.items():
|
|
294
|
+
if value == 0.0:
|
|
295
|
+
return 0.0
|
|
296
|
+
out *= value
|
|
297
|
+
if unit is None:
|
|
298
|
+
continue
|
|
299
|
+
from_unit = unit if from_unit is None else f"{from_unit} * {unit}"
|
|
300
|
+
|
|
301
|
+
if not from_unit:
|
|
302
|
+
return out
|
|
303
|
+
|
|
304
|
+
return out * get_unit_conversion_factor(from_unit, target_unit)
|
|
305
|
+
|
|
306
|
+
|
|
307
|
+
def _fastpath_aggregation( # noqa: C901, PLR0911
|
|
308
|
+
constants_with_units: dict[str, tuple],
|
|
309
|
+
expr: Expr,
|
|
310
|
+
target_unit: str | None,
|
|
311
|
+
) -> float:
|
|
312
|
+
__, args = expr.get_operations(expect_ops=True, copy_list=False)
|
|
313
|
+
numerator, denominator = args
|
|
314
|
+
|
|
315
|
+
num = _get_fastpath_aggregation_numerator_dict(constants_with_units, numerator)
|
|
316
|
+
dem = _get_fastpath_aggregation_denominator_dict(constants_with_units, denominator)
|
|
317
|
+
|
|
318
|
+
if len(dem) == len(num) == 1:
|
|
319
|
+
num_unit, num_value = next(iter(num.items()))
|
|
320
|
+
dem_unit, dem_value = next(iter(dem.items()))
|
|
321
|
+
|
|
322
|
+
not_num_unit = num_unit is None
|
|
323
|
+
not_dem_unit = dem_unit is None
|
|
324
|
+
has_num_unit = num_unit is not None
|
|
325
|
+
has_dem_unit = dem_unit is not None
|
|
326
|
+
|
|
327
|
+
if not_num_unit and not_dem_unit:
|
|
328
|
+
if target_unit is None:
|
|
329
|
+
return num_value / dem_value
|
|
330
|
+
message = f"Could not convert to {target_unit} with numerator {numerator} and denominator {denominator} for expr {expr}"
|
|
331
|
+
raise ValueError(message)
|
|
332
|
+
|
|
333
|
+
if not_dem_unit and has_num_unit:
|
|
334
|
+
if target_unit == num_unit:
|
|
335
|
+
return num_value / dem_value
|
|
336
|
+
return get_unit_conversion_factor(num_unit, target_unit) * (num_value / dem_value)
|
|
337
|
+
if has_dem_unit and not_num_unit:
|
|
338
|
+
inverse_dem_unit = f"1/({dem_unit})"
|
|
339
|
+
if target_unit == inverse_dem_unit:
|
|
340
|
+
return num_value / dem_value
|
|
341
|
+
return get_unit_conversion_factor(inverse_dem_unit, target_unit) * (num_value / dem_value)
|
|
342
|
+
combined_unit = f"{num_unit}/({dem_unit})"
|
|
343
|
+
if target_unit == combined_unit:
|
|
344
|
+
return num_value / dem_value
|
|
345
|
+
return get_unit_conversion_factor(combined_unit, target_unit) * (num_value / dem_value)
|
|
346
|
+
|
|
347
|
+
combined_num_unit = ""
|
|
348
|
+
for unit, value in num.items():
|
|
349
|
+
op = "+" if value > 0 else "-"
|
|
350
|
+
combined_num_unit = f"{combined_num_unit} {op} {abs(value)}"
|
|
351
|
+
|
|
352
|
+
combined_dem_unit = ""
|
|
353
|
+
for unit, value in num.items():
|
|
354
|
+
op = "+" if value > 0 else "-"
|
|
355
|
+
combined_dem_unit = f"{combined_dem_unit} {op} {abs(value)}"
|
|
356
|
+
|
|
357
|
+
combined_unit = f"({combined_num_unit})/({combined_dem_unit})"
|
|
358
|
+
|
|
359
|
+
return get_unit_conversion_factor(combined_unit, target_unit)
|
|
360
|
+
|
|
361
|
+
|
|
362
|
+
def _get_fastpath_sum_dict(
|
|
363
|
+
constants_with_units: dict[str, tuple],
|
|
364
|
+
expr: Expr,
|
|
365
|
+
) -> dict[str | None, float]:
|
|
366
|
+
d = dict()
|
|
367
|
+
if expr.is_leaf():
|
|
368
|
+
__, value, unit = _lookup_expr_from_constants_with_units(constants_with_units, expr)
|
|
369
|
+
return {unit: value}
|
|
370
|
+
ops, args = expr.get_operations(expect_ops=True, copy_list=False)
|
|
371
|
+
__, value, unit = _lookup_expr_from_constants_with_units(constants_with_units, args[0])
|
|
372
|
+
d[unit] = value
|
|
373
|
+
for op, arg in zip(ops, args[1:], strict=True):
|
|
374
|
+
__, value, unit = _lookup_expr_from_constants_with_units(constants_with_units, arg)
|
|
375
|
+
contribution = value if op == "+" else -value
|
|
376
|
+
if unit not in d:
|
|
377
|
+
d[unit] = contribution
|
|
378
|
+
else:
|
|
379
|
+
d[unit] += contribution
|
|
380
|
+
return d
|
|
381
|
+
|
|
382
|
+
|
|
383
|
+
def _get_fastpath_product_dict(
|
|
384
|
+
constants_with_units: dict[str, tuple],
|
|
385
|
+
expr: Expr,
|
|
386
|
+
) -> dict[str | None, float]:
|
|
387
|
+
d = dict()
|
|
388
|
+
if expr.is_leaf():
|
|
389
|
+
__, value, unit = _lookup_expr_from_constants_with_units(constants_with_units, expr)
|
|
390
|
+
return {unit: value}
|
|
391
|
+
__, args = expr.get_operations(expect_ops=True, copy_list=False)
|
|
392
|
+
for arg in args:
|
|
393
|
+
if _is_fastpath_sum(arg):
|
|
394
|
+
values = _get_fastpath_sum_dict(constants_with_units, arg)
|
|
395
|
+
else:
|
|
396
|
+
__, value, unit = _lookup_expr_from_constants_with_units(constants_with_units, arg)
|
|
397
|
+
values = {unit: value}
|
|
398
|
+
for unit, value in values.items():
|
|
399
|
+
if unit not in d:
|
|
400
|
+
d[unit] = value
|
|
401
|
+
else:
|
|
402
|
+
d[unit] *= value
|
|
403
|
+
return d
|
|
404
|
+
|
|
405
|
+
|
|
406
|
+
def _get_fastpath_product_unit(d: dict[str | None, float]) -> tuple[float, str | None]:
|
|
407
|
+
combined_value = 1.0
|
|
408
|
+
combined_unit = []
|
|
409
|
+
for unit, value in d.items():
|
|
410
|
+
combined_value *= value
|
|
411
|
+
if unit is not None:
|
|
412
|
+
combined_unit.append(unit)
|
|
413
|
+
if not combined_unit:
|
|
414
|
+
return combined_value, None
|
|
415
|
+
return combined_value, "*".join(sorted(f"({s})" for s in combined_unit))
|
|
416
|
+
|
|
417
|
+
|
|
418
|
+
def _get_fastpath_aggregation_numerator_dict(
|
|
419
|
+
constants_with_units: dict[str, tuple],
|
|
420
|
+
expr: Expr,
|
|
421
|
+
) -> dict[str | None, float]:
|
|
422
|
+
if expr.is_leaf():
|
|
423
|
+
__, value, unit = _lookup_expr_from_constants_with_units(constants_with_units, expr)
|
|
424
|
+
return {unit: value}
|
|
425
|
+
ops, args = expr.get_operations(expect_ops=True, copy_list=False)
|
|
426
|
+
out = dict()
|
|
427
|
+
value, unit = _get_fastpath_product_unit(_get_fastpath_product_dict(constants_with_units, args[0]))
|
|
428
|
+
out[unit] = value
|
|
429
|
+
for op, arg in zip(ops, args[1:], strict=True):
|
|
430
|
+
value, unit = _get_fastpath_product_unit(_get_fastpath_product_dict(constants_with_units, arg))
|
|
431
|
+
contribution = value if op == "+" else -value
|
|
432
|
+
if unit not in out:
|
|
433
|
+
out[unit] = contribution
|
|
434
|
+
else:
|
|
435
|
+
out[unit] += contribution
|
|
436
|
+
return out
|
|
437
|
+
|
|
438
|
+
|
|
439
|
+
def _get_fastpath_aggregation_denominator_dict(
|
|
440
|
+
constants_with_units: dict[str, tuple],
|
|
441
|
+
expr: Expr,
|
|
442
|
+
) -> dict[str | None, float]:
|
|
443
|
+
if expr.is_leaf():
|
|
444
|
+
__, value, unit = _lookup_expr_from_constants_with_units(constants_with_units, expr)
|
|
445
|
+
return {unit: value}
|
|
446
|
+
ops, args = expr.get_operations(expect_ops=True, copy_list=False)
|
|
447
|
+
d = dict()
|
|
448
|
+
__, value, unit = _lookup_expr_from_constants_with_units(constants_with_units, args[0])
|
|
449
|
+
d[unit] = value
|
|
450
|
+
for op, arg in zip(ops, args[1:], strict=True):
|
|
451
|
+
__, value, unit = _lookup_expr_from_constants_with_units(constants_with_units, arg)
|
|
452
|
+
if value == 0.0:
|
|
453
|
+
continue
|
|
454
|
+
contribution = value if op == "+" else -value
|
|
455
|
+
if unit not in d:
|
|
456
|
+
d[unit] = contribution
|
|
457
|
+
else:
|
|
458
|
+
d[unit] += contribution
|
|
459
|
+
return d
|
|
460
|
+
|
|
461
|
+
|
|
462
|
+
def _sympy_fallback(constants_with_units: dict[str, tuple], expr_str: str, target_unit: str | None) -> float:
|
|
463
|
+
"""Convert expr to sympy expr, substitue in constants with units, and let sympy evaluate."""
|
|
464
|
+
expr_sym = sympy.sympify(expr_str)
|
|
465
|
+
for src, (sym, value, unit) in constants_with_units.items():
|
|
466
|
+
sympy_sym = sympy.Symbol(sym)
|
|
467
|
+
|
|
468
|
+
if unit is not None:
|
|
469
|
+
unit_sym = _unit_str_to_sym(unit)
|
|
470
|
+
expr_sym = expr_sym.subs(sympy_sym, value * unit_sym)
|
|
471
|
+
else:
|
|
472
|
+
expr_sym = expr_sym.subs(sympy_sym, value)
|
|
473
|
+
|
|
474
|
+
if target_unit:
|
|
475
|
+
unit_sym = _unit_str_to_sym(target_unit)
|
|
476
|
+
expr_sym = expr_sym / unit_sym
|
|
477
|
+
|
|
478
|
+
value = _get_scalar_from_expr(expr_sym)
|
|
479
|
+
if isinstance(value, str):
|
|
480
|
+
message = f"Cannot convert expression '{expr_str}' with target_unit {target_unit} to scalar value. {value}."
|
|
481
|
+
raise ValueError(message)
|
|
482
|
+
|
|
483
|
+
return value
|