fram-core 0.0.0__py3-none-any.whl → 0.1.0a2__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.0a2.dist-info/METADATA +42 -0
  2. fram_core-0.1.0a2.dist-info/RECORD +100 -0
  3. {fram_core-0.0.0.dist-info → fram_core-0.1.0a2.dist-info}/WHEEL +1 -2
  4. fram_core-0.1.0a2.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,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
+ ]