fram-core 0.0.0__py3-none-any.whl → 0.1.0a1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (103) hide show
  1. fram_core-0.1.0a1.dist-info/METADATA +41 -0
  2. fram_core-0.1.0a1.dist-info/RECORD +100 -0
  3. {fram_core-0.0.0.dist-info → fram_core-0.1.0a1.dist-info}/WHEEL +1 -2
  4. fram_core-0.1.0a1.dist-info/licenses/LICENSE.md +8 -0
  5. framcore/Base.py +142 -0
  6. framcore/Model.py +73 -0
  7. framcore/__init__.py +9 -0
  8. framcore/aggregators/Aggregator.py +153 -0
  9. framcore/aggregators/HydroAggregator.py +837 -0
  10. framcore/aggregators/NodeAggregator.py +495 -0
  11. framcore/aggregators/WindSolarAggregator.py +323 -0
  12. framcore/aggregators/__init__.py +13 -0
  13. framcore/aggregators/_utils.py +184 -0
  14. framcore/attributes/Arrow.py +305 -0
  15. framcore/attributes/ElasticDemand.py +90 -0
  16. framcore/attributes/ReservoirCurve.py +37 -0
  17. framcore/attributes/SoftBound.py +19 -0
  18. framcore/attributes/StartUpCost.py +54 -0
  19. framcore/attributes/Storage.py +146 -0
  20. framcore/attributes/TargetBound.py +18 -0
  21. framcore/attributes/__init__.py +65 -0
  22. framcore/attributes/hydro/HydroBypass.py +42 -0
  23. framcore/attributes/hydro/HydroGenerator.py +83 -0
  24. framcore/attributes/hydro/HydroPump.py +156 -0
  25. framcore/attributes/hydro/HydroReservoir.py +27 -0
  26. framcore/attributes/hydro/__init__.py +13 -0
  27. framcore/attributes/level_profile_attributes.py +714 -0
  28. framcore/components/Component.py +112 -0
  29. framcore/components/Demand.py +130 -0
  30. framcore/components/Flow.py +167 -0
  31. framcore/components/HydroModule.py +330 -0
  32. framcore/components/Node.py +76 -0
  33. framcore/components/Thermal.py +204 -0
  34. framcore/components/Transmission.py +183 -0
  35. framcore/components/_PowerPlant.py +81 -0
  36. framcore/components/__init__.py +22 -0
  37. framcore/components/wind_solar.py +67 -0
  38. framcore/curves/Curve.py +44 -0
  39. framcore/curves/LoadedCurve.py +155 -0
  40. framcore/curves/__init__.py +9 -0
  41. framcore/events/__init__.py +21 -0
  42. framcore/events/events.py +51 -0
  43. framcore/expressions/Expr.py +490 -0
  44. framcore/expressions/__init__.py +28 -0
  45. framcore/expressions/_get_constant_from_expr.py +483 -0
  46. framcore/expressions/_time_vector_operations.py +615 -0
  47. framcore/expressions/_utils.py +73 -0
  48. framcore/expressions/queries.py +423 -0
  49. framcore/expressions/units.py +207 -0
  50. framcore/fingerprints/__init__.py +11 -0
  51. framcore/fingerprints/fingerprint.py +293 -0
  52. framcore/juliamodels/JuliaModel.py +161 -0
  53. framcore/juliamodels/__init__.py +7 -0
  54. framcore/loaders/__init__.py +10 -0
  55. framcore/loaders/loaders.py +407 -0
  56. framcore/metadata/Div.py +73 -0
  57. framcore/metadata/ExprMeta.py +50 -0
  58. framcore/metadata/LevelExprMeta.py +17 -0
  59. framcore/metadata/Member.py +55 -0
  60. framcore/metadata/Meta.py +44 -0
  61. framcore/metadata/__init__.py +15 -0
  62. framcore/populators/Populator.py +108 -0
  63. framcore/populators/__init__.py +7 -0
  64. framcore/querydbs/CacheDB.py +50 -0
  65. framcore/querydbs/ModelDB.py +34 -0
  66. framcore/querydbs/QueryDB.py +45 -0
  67. framcore/querydbs/__init__.py +11 -0
  68. framcore/solvers/Solver.py +48 -0
  69. framcore/solvers/SolverConfig.py +272 -0
  70. framcore/solvers/__init__.py +9 -0
  71. framcore/timeindexes/AverageYearRange.py +20 -0
  72. framcore/timeindexes/ConstantTimeIndex.py +17 -0
  73. framcore/timeindexes/DailyIndex.py +21 -0
  74. framcore/timeindexes/FixedFrequencyTimeIndex.py +762 -0
  75. framcore/timeindexes/HourlyIndex.py +21 -0
  76. framcore/timeindexes/IsoCalendarDay.py +31 -0
  77. framcore/timeindexes/ListTimeIndex.py +197 -0
  78. framcore/timeindexes/ModelYear.py +17 -0
  79. framcore/timeindexes/ModelYears.py +18 -0
  80. framcore/timeindexes/OneYearProfileTimeIndex.py +21 -0
  81. framcore/timeindexes/ProfileTimeIndex.py +32 -0
  82. framcore/timeindexes/SinglePeriodTimeIndex.py +37 -0
  83. framcore/timeindexes/TimeIndex.py +90 -0
  84. framcore/timeindexes/WeeklyIndex.py +21 -0
  85. framcore/timeindexes/__init__.py +36 -0
  86. framcore/timevectors/ConstantTimeVector.py +135 -0
  87. framcore/timevectors/LinearTransformTimeVector.py +114 -0
  88. framcore/timevectors/ListTimeVector.py +123 -0
  89. framcore/timevectors/LoadedTimeVector.py +104 -0
  90. framcore/timevectors/ReferencePeriod.py +41 -0
  91. framcore/timevectors/TimeVector.py +94 -0
  92. framcore/timevectors/__init__.py +17 -0
  93. framcore/utils/__init__.py +36 -0
  94. framcore/utils/get_regional_volumes.py +369 -0
  95. framcore/utils/get_supported_components.py +60 -0
  96. framcore/utils/global_energy_equivalent.py +46 -0
  97. framcore/utils/isolate_subnodes.py +163 -0
  98. framcore/utils/loaders.py +97 -0
  99. framcore/utils/node_flow_utils.py +236 -0
  100. framcore/utils/storage_subsystems.py +107 -0
  101. fram_core-0.0.0.dist-info/METADATA +0 -5
  102. fram_core-0.0.0.dist-info/RECORD +0 -4
  103. fram_core-0.0.0.dist-info/top_level.txt +0 -1
@@ -0,0 +1,423 @@
1
+ from __future__ import annotations
2
+
3
+ import time
4
+ from typing import TYPE_CHECKING
5
+
6
+ import numpy as np
7
+ from numpy.typing import NDArray
8
+
9
+ from framcore.curves import Curve
10
+ from framcore.expressions import Expr
11
+ from framcore.expressions._get_constant_from_expr import _get_constant_from_expr
12
+ from framcore.expressions._utils import _load_model_and_create_model_db
13
+ from framcore.expressions.units import get_unit_conversion_factor
14
+ from framcore.querydbs import QueryDB
15
+ from framcore.timeindexes import FixedFrequencyTimeIndex, SinglePeriodTimeIndex, TimeIndex
16
+ from framcore.timevectors import TimeVector
17
+
18
+ if TYPE_CHECKING:
19
+ from framcore import Model
20
+
21
+
22
+ def get_level_value(
23
+ expr: Expr,
24
+ db: QueryDB | Model,
25
+ unit: str | None,
26
+ data_dim: SinglePeriodTimeIndex,
27
+ scen_dim: FixedFrequencyTimeIndex,
28
+ is_max: bool,
29
+ ) -> float:
30
+ """
31
+ Evaluate expr representing a (possibly aggregated) level.
32
+
33
+ The follwing will be automatically handeled for you:
34
+ - conversion to requested unit
35
+ - conversion to requested reference period
36
+ - conversion to requested level type
37
+ - fetching from different data objecs
38
+
39
+ expr = sum(scale[i] * level[i]) where
40
+
41
+ scale[i] >= 0 and is unitless
42
+ level[i] is a unitful level expr
43
+
44
+ level[i] is either "avg" or "max" type of level
45
+
46
+ if "avg", it has a reference period (startyear, startyear + numyears) and a corresponding
47
+ "mean_one" profile, for which mean(values) = 1 for values covering the reference period
48
+
49
+ "avg" and "max" level type must be converted to the same standard to be added correctly.
50
+
51
+ Two "avg" types with different reference period must be converted to same reference period
52
+ to be added correctly.
53
+
54
+ The query parameter scen_dim tells which reference period to convert to.
55
+
56
+ The query parameter is_max tells which level type to convert to.
57
+ """
58
+ db = _load_model_and_create_model_db(db)
59
+ return _get_level_value(expr, db, unit, data_dim, scen_dim, is_max)
60
+
61
+
62
+ def get_profile_vector(
63
+ expr: Expr,
64
+ db: QueryDB | Model,
65
+ data_dim: SinglePeriodTimeIndex,
66
+ scen_dim: FixedFrequencyTimeIndex,
67
+ is_zero_one: bool,
68
+ is_float32: bool = True,
69
+ ) -> NDArray:
70
+ """
71
+ Evaluate expr representing a (possibly aggregated) profile.
72
+
73
+ expr = sum(weight[i] * profile[i]) where
74
+
75
+ weight[i] >= 0 and is unitless
76
+ profile[i] is a unitless profile expr
77
+
78
+ profile[i] is either "zero_one" or "mean_one" type of profile
79
+
80
+ "zero_one" and "mean_one" profile type must be converted to the
81
+ same standard to be added correctly.
82
+
83
+ The query parameter data_dim is used to compute weight[i].
84
+
85
+ The query parameter scen_dim specifies the out vector dimension.
86
+
87
+ The query parameter is_zero_one tells which profile type the output
88
+ vector should be converted to.
89
+ """
90
+ db = _load_model_and_create_model_db(db)
91
+ # TODO: Implement checks cleaner
92
+ return _get_profile_vector(expr, db, data_dim, scen_dim, is_zero_one, is_float32)
93
+
94
+
95
+ def get_units_from_expr(db: QueryDB | Model, expr: Expr) -> set[str]:
96
+ """Find all units behind an expression. Useful for queries involving conversion factors."""
97
+ db = _load_model_and_create_model_db(db)
98
+
99
+ units: set[str] = set()
100
+
101
+ _recursively_update_units(units, db, expr)
102
+
103
+ return units
104
+
105
+
106
+ def get_timeindexes_from_expr(db: QueryDB | Model, expr: Expr) -> set[TimeIndex]:
107
+ """
108
+ Find all timeindexes behind an expression.
109
+
110
+ Useful for optimized queries (not asking for more data than necessary).
111
+ """
112
+ db = _load_model_and_create_model_db(db)
113
+
114
+ timeindexes: set[TimeIndex] = set()
115
+
116
+ _recursively_update_timeindexes(timeindexes, db, expr)
117
+
118
+ return timeindexes
119
+
120
+
121
+ def _get_level_value(
122
+ expr: Expr,
123
+ db: QueryDB,
124
+ unit: str | None,
125
+ data_dim: SinglePeriodTimeIndex,
126
+ scen_dim: FixedFrequencyTimeIndex,
127
+ is_max: bool,
128
+ ) -> float:
129
+ # TODO: Implement checks cleaner
130
+ assert isinstance(expr, Expr), f"{expr}"
131
+
132
+ cache_key = ("_get_constant_from_expr", expr, unit, data_dim, scen_dim, is_max)
133
+ if db.has_key(cache_key):
134
+ return db.get(cache_key)
135
+ t0 = time.perf_counter()
136
+ output_value = _get_constant_from_expr(expr, db, unit, data_dim, scen_dim, is_max)
137
+ t1 = time.perf_counter()
138
+ db.put(cache_key, output_value, elapsed_seconds=t1 - t0)
139
+
140
+ return output_value
141
+
142
+
143
+ def _get_profile_vector(
144
+ expr: Expr,
145
+ db: QueryDB,
146
+ data_dim: SinglePeriodTimeIndex,
147
+ scen_dim: FixedFrequencyTimeIndex,
148
+ is_zero_one: bool,
149
+ is_float32: bool = True,
150
+ ) -> NDArray:
151
+ assert isinstance(expr, Expr), f"{expr}"
152
+
153
+ if expr.is_leaf():
154
+ src = expr.get_src()
155
+
156
+ if isinstance(src, str):
157
+ obj = db.get(src)
158
+ else:
159
+ assert isinstance(src, TimeVector)
160
+ obj = src
161
+
162
+ if isinstance(obj, Expr):
163
+ assert obj.is_profile(), f"{obj}"
164
+ return _get_profile_vector(obj, db, data_dim, scen_dim, is_zero_one, is_float32)
165
+
166
+ assert isinstance(obj, TimeVector)
167
+ cache_key = ("_get_profile_vector_from_timevector", obj, data_dim, scen_dim, is_zero_one, is_float32)
168
+ if db.has_key(cache_key):
169
+ vector: NDArray = db.get(cache_key)
170
+ return vector.copy()
171
+ t0 = time.perf_counter()
172
+ vector = _get_profile_vector_from_timevector(obj, scen_dim, is_zero_one, is_float32)
173
+ t1 = time.perf_counter()
174
+ db.put(cache_key, vector, elapsed_seconds=t1 - t0)
175
+ return vector
176
+
177
+ ops, args = expr.get_operations(expect_ops=True, copy_list=False)
178
+
179
+ tmp = np.zeros(scen_dim.get_num_periods(), dtype=np.float32 if is_float32 else np.float64)
180
+
181
+ if "+" in ops:
182
+ out = _get_profile_vector(args[0], db, data_dim, scen_dim, is_zero_one, is_float32)
183
+ for op, arg in zip(ops, args[1:], strict=True):
184
+ assert op == "+", f"{ops} {args}"
185
+ tmp = _get_profile_vector(arg, db, data_dim, scen_dim, is_zero_one, is_float32)
186
+ np.add(out, tmp, out=out)
187
+ return out
188
+
189
+ if not all(op == "*" for op in ops):
190
+ message = f"Expected w1*w2*..*wn*profile. Got operations {ops} for expr {expr}"
191
+ raise ValueError(message)
192
+
193
+ profiles = [arg for arg in args if arg.is_profile()]
194
+ weights = [arg for arg in args if not arg.is_profile()]
195
+ if len(profiles) != 1:
196
+ message = f"Got {len(profiles)} profiles in expr {expr}"
197
+ raise ValueError(message)
198
+ total_weight = 0.0
199
+ is_max = False # use avg-values to calculate weights
200
+ for weight_expr in weights:
201
+ total_weight += _get_constant_from_expr(weight_expr, db, None, data_dim, scen_dim, is_max)
202
+ out = _get_profile_vector(profiles[0], db, data_dim, scen_dim, is_zero_one, is_float32)
203
+ np.multiply(out, total_weight, out=out)
204
+ return out
205
+
206
+
207
+ def _get_profile_vector_from_timevector(
208
+ timevector: TimeVector,
209
+ scen_dim: FixedFrequencyTimeIndex,
210
+ target_is_zero_one: bool,
211
+ is_float32: bool,
212
+ ) -> NDArray:
213
+ out = np.zeros(scen_dim.get_num_periods(), dtype=np.float32 if is_float32 else np.float64)
214
+
215
+ tv_is_zero_one = timevector.is_zero_one_profile() # OPPGAVE endrer TimeVector-API
216
+ assert isinstance(tv_is_zero_one, bool)
217
+ assert isinstance(timevector.get_unit(), type(None))
218
+ values = timevector.get_vector(is_float32)
219
+ timeindex = timevector.get_timeindex()
220
+ timeindex.write_into_fixed_frequency(out, scen_dim, values)
221
+
222
+ # CASE HANDLED:
223
+ # Both profiles are mean one within their respective reference periods.
224
+ # We convert timevector ref period to target reference period by
225
+ # making sure the mean value of 'out' is 1 within the target reference period.
226
+ if not target_is_zero_one and not tv_is_zero_one:
227
+ target_ref_period = scen_dim.get_reference_period()
228
+ tv_ref_period = timevector.get_reference_period()
229
+ if target_ref_period != tv_ref_period:
230
+ tv_target_ref_period_mean = timeindex.get_period_average(
231
+ vector=values,
232
+ start_time=scen_dim.get_start_time(),
233
+ duration=scen_dim.get_period_duration() * scen_dim.get_num_periods(),
234
+ is_52_week_years=scen_dim.is_52_week_years(),
235
+ )
236
+
237
+ if tv_target_ref_period_mean == 0.0:
238
+ message = f"TimeVector {timevector} has invalid mean value of 0.0 for target reference period {target_ref_period}."
239
+ raise ValueError(message)
240
+
241
+ np.multiply(out, 1 / tv_target_ref_period_mean, out=out)
242
+
243
+ # TODO: Bør heller "ikke stole på"
244
+ if target_is_zero_one != tv_is_zero_one:
245
+ if target_is_zero_one:
246
+ np.multiply(out, 1 / out.max(), out=out) # convert to zero one profile standard
247
+ elif not np.all(out == 0):
248
+ np.multiply(out, 1 / out.mean(), out=out) # convert to mean one profile standard")
249
+
250
+ # TODO: handle different ref periods if is_avg
251
+ return out
252
+
253
+
254
+ def _recursively_update_units(units: set[str], db: QueryDB, expr: Expr) -> None:
255
+ if expr.is_leaf():
256
+ src = expr.get_src()
257
+
258
+ obj = src if isinstance(src, TimeVector) else db.get(key=src)
259
+
260
+ if isinstance(obj, Expr):
261
+ _recursively_update_units(units, db, obj)
262
+ elif isinstance(obj, Curve):
263
+ message = "Not yet implemented for Curve objects."
264
+ raise NotImplementedError(message)
265
+ elif isinstance(obj, TimeVector):
266
+ unit = obj.get_unit()
267
+ if unit is not None:
268
+ units.add(unit)
269
+ else:
270
+ message = f"Got unexpected object {obj}."
271
+ raise RuntimeError(message)
272
+ __, args = expr.get_operations(expect_ops=False, copy_list=False)
273
+ for arg in args:
274
+ _recursively_update_units(units, db, arg)
275
+
276
+
277
+ def _recursively_update_units(units: set[str], db: QueryDB, expr: Expr) -> None:
278
+ if expr.is_leaf():
279
+ src = expr.get_src()
280
+ obj = src if isinstance(src, TimeVector) else db.get(key=src)
281
+ if isinstance(obj, Expr):
282
+ _recursively_update_units(units, db, obj)
283
+ elif isinstance(obj, Curve):
284
+ message = "Not yet implemented for Curve objects."
285
+ raise NotImplementedError(message)
286
+ elif isinstance(obj, TimeVector):
287
+ unit = obj.get_unit()
288
+ if unit is not None:
289
+ units.add(unit)
290
+ else:
291
+ message = f"Got unexpected object {obj}."
292
+ raise RuntimeError(message)
293
+ __, args = expr.get_operations(expect_ops=False, copy_list=False)
294
+ for arg in args:
295
+ _recursively_update_units(units, db, arg)
296
+
297
+
298
+ def _recursively_update_timeindexes(timeindexes: set[TimeIndex], db: QueryDB, expr: Expr) -> None:
299
+ if expr.is_leaf():
300
+ src = expr.get_src()
301
+ obj = src if isinstance(src, TimeVector) else db.get(key=src)
302
+ if isinstance(obj, Expr):
303
+ _recursively_update_timeindexes(timeindexes, db, obj)
304
+ elif isinstance(obj, Curve):
305
+ pass
306
+ elif isinstance(obj, TimeVector):
307
+ timeindex = obj.get_timeindex()
308
+ if timeindex is not None:
309
+ timeindexes.add(timeindex)
310
+ else:
311
+ message = f"Got unexpected object {obj}."
312
+ raise RuntimeError(message)
313
+ __, args = expr.get_operations(expect_ops=False, copy_list=False)
314
+ for arg in args:
315
+ _recursively_update_timeindexes(timeindexes, db, arg)
316
+
317
+
318
+ def _get_level_value_from_timevector( # noqa: C901
319
+ timevector: TimeVector,
320
+ db: QueryDB,
321
+ target_unit: str | None,
322
+ data_dim: SinglePeriodTimeIndex,
323
+ scen_dim: FixedFrequencyTimeIndex,
324
+ target_is_max: bool,
325
+ profile_expr: Expr | None,
326
+ ) -> float:
327
+ tv_is_max = timevector.is_max_level() # OPPGAVE endrer TimeVector-API
328
+
329
+ is_float32 = True
330
+
331
+ values = timevector.get_vector(is_float32) # OPPGAVE endrer TimeVector-API
332
+
333
+ # if DEFENSIVE_MODE: # global i data-mng-modul
334
+ # assert isinstance(values, np.ndarray)
335
+ # assert len(values.shape) == 1
336
+
337
+ timeindex = timevector.get_timeindex()
338
+ from_unit = timevector.get_unit()
339
+
340
+ starttime = data_dim.get_start_time() # OPPGAVE endrer ConstantTimeIndex-API?
341
+ timedelta = data_dim.get_period_duration() # OPPGAVE endrer ConstantTimeIndex-API?
342
+ scalar = timeindex.get_period_average(values, starttime, timedelta, data_dim.is_52_week_years())
343
+
344
+ if from_unit is not None and target_unit is not None:
345
+ scalar *= get_unit_conversion_factor(from_unit, target_unit)
346
+ elif from_unit is None and target_unit is None:
347
+ pass
348
+ else:
349
+ message = "Mismatch between 'target_unit' 'from_unit'. One is None while the other is not." # more descriptive?
350
+ raise ValueError(message)
351
+
352
+ if not target_is_max:
353
+ if not tv_is_max: # from avg to avg
354
+ avg_level = scalar
355
+ tv_ref_period = timevector.get_reference_period() # Vi lar ReferencePeriod være tidsindex som en lang periode
356
+ if tv_ref_period is None:
357
+ assert profile_expr is None, f"Timevector {timevector} has no reference period, profile_expr must therefore be None."
358
+ return avg_level # avg level fra timevector uten ref periode
359
+ target_ref_period = scen_dim.get_reference_period()
360
+ if target_ref_period is None:
361
+ message = f"No reference period for scen_dim {scen_dim}"
362
+ raise ValueError(message)
363
+ if tv_ref_period != target_ref_period:
364
+ # if DEFENSIVE_MODE:
365
+ # tv_ref_period_mean = get_profile_vector(profile_expr, db, data_dim, tv_ref_period, is_float32, is_zero_one=False)
366
+ # assert tv_ref_period_mean.size == 1
367
+ # assert tv_ref_period_mean[0] == 1
368
+ assert profile_expr, f"Profile Expr is None for TimeVector {timevector} when it should exist"
369
+ tv_target_ref_period_mean = get_profile_vector(
370
+ profile_expr,
371
+ db,
372
+ data_dim,
373
+ scen_dim.copy_as_reference_period(target_ref_period),
374
+ is_zero_one=False,
375
+ is_float32=is_float32,
376
+ )
377
+ assert tv_target_ref_period_mean.size == 1
378
+ avg_level = tv_target_ref_period_mean[0] * avg_level
379
+ return avg_level
380
+ # timevector fra max til avg
381
+ max_level = scalar
382
+ assert timevector.get_reference_period() is None
383
+
384
+ zero_one_profile_vector_mean = 1
385
+ if profile_expr is not None: # only try to get profile if the level is actually associated with one.
386
+ zero_one_profile_vector_mean = get_profile_vector(
387
+ profile_expr,
388
+ db,
389
+ data_dim,
390
+ scen_dim,
391
+ is_zero_one=True,
392
+ is_float32=is_float32,
393
+ ).mean()
394
+ return zero_one_profile_vector_mean * max_level
395
+
396
+ assert target_is_max # vi skal ha max level
397
+
398
+ if not tv_is_max:
399
+ avg_level = scalar
400
+ tv_ref_period = timevector.get_reference_period() # Vi lar ReferencePeriod være tidsindex som en lang periode
401
+ if tv_ref_period is None:
402
+ assert profile_expr is None, f"Timevector {timevector} has no reference period, profile_expr must therefore be None."
403
+ return avg_level
404
+ target_ref_period = scen_dim.get_reference_period()
405
+ if target_ref_period is None:
406
+ message = f"No reference period for scen_dim {scen_dim}"
407
+ raise ValueError(message)
408
+ if tv_ref_period != target_ref_period:
409
+ tv_target_ref_period_mean = get_profile_vector(
410
+ profile_expr,
411
+ db,
412
+ data_dim,
413
+ scen_dim.copy_as_reference_period(target_ref_period),
414
+ is_zero_one=False,
415
+ is_float32=is_float32,
416
+ )
417
+ assert tv_target_ref_period_mean.size == 1
418
+ avg_level = tv_target_ref_period_mean[0] * avg_level
419
+ # avg_level med korrekt ref periode
420
+ mean_one_profile_vector = get_profile_vector(profile_expr, db, data_dim, scen_dim, is_zero_one=False, is_float32=is_float32)
421
+ return mean_one_profile_vector.max() * avg_level
422
+
423
+ return scalar # all good
@@ -0,0 +1,207 @@
1
+ from __future__ import annotations
2
+
3
+ import contextlib
4
+ import re
5
+
6
+ import sympy
7
+ from sympy import Expr as SymPyExpr
8
+ from sympy.core.power import Pow
9
+ from sympy.core.symbol import Symbol
10
+ from sympy.physics.units import Quantity, giga, gram, hour, kilo, mega, meter, second, tera, tonne, watt, year
11
+ from sympy.physics.units.prefixes import Prefix
12
+
13
+ EUR = Quantity("EUR", abbrev="€")
14
+
15
+ _SUPPORTED_UNITS = {
16
+ "second": second,
17
+ "s": second,
18
+ "hour": hour,
19
+ "h": hour,
20
+ "year": year,
21
+ "y": year,
22
+ "watt": watt,
23
+ "g": gram,
24
+ "gram": gram,
25
+ "kg": kilo * gram,
26
+ "t": tonne,
27
+ "tonne": tonne,
28
+ "meter": meter,
29
+ "m": meter,
30
+ "m3": meter**3,
31
+ "Mm3": mega * meter**3,
32
+ "m3/s": meter**3 / second,
33
+ "kilo": kilo,
34
+ "mega": mega,
35
+ "giga": giga,
36
+ "tera": tera,
37
+ "kWh": kilo * watt * hour,
38
+ "MWh": mega * watt * hour,
39
+ "GWh": giga * watt * hour,
40
+ "TWh": tera * watt * hour,
41
+ "kW": kilo * watt,
42
+ "MW": mega * watt,
43
+ "GW": giga * watt,
44
+ "TW": tera * watt,
45
+ "EUR": EUR,
46
+ "€": EUR,
47
+ }
48
+
49
+ _FASTPATH_CONVERSION_FACTORS = {
50
+ ("MW", "GW"): 0.001,
51
+ ("MWh", "TWh"): 1e-6,
52
+ ("m3", "Mm3"): 1e-6,
53
+ ("kWh/m3", "GWh/Mm3"): 1.0,
54
+ ("EUR/MWh", "EUR/GWh"): 1000.0,
55
+ ("GWh/year", "MW"): 0.11407955544967756,
56
+ ("Mm3/year", "m3/s"): 0.03168876540268821,
57
+ ("t/MWh", "t/GWh"): 1000.0,
58
+ }
59
+
60
+ _FASTPATH_INCOMPATIBLE_CONVERSIONS = {
61
+ ("MW", "m3/s"),
62
+ ("m3/s", "MW"),
63
+ ("GWh", "Mm3"),
64
+ ("Mm3", "GWh"),
65
+ }
66
+
67
+ _DEBUG = False
68
+
69
+ _COLLECT_FASTPATH_DATA = False
70
+ _OBSERVED_UNIT_CONVERSIONS = set()
71
+
72
+
73
+ def get_unit_conversion_factor(from_unit: str | None, to_unit: str | None) -> float:
74
+ """Get the conversion factor from one unit to another."""
75
+ if from_unit == to_unit:
76
+ return 1.0
77
+
78
+ if from_unit is None or to_unit is None:
79
+ message = f"Incompatible units: from_unit {from_unit} to_unit {to_unit}"
80
+ raise ValueError(message)
81
+
82
+ fastpath = _fastpath_get_unit_conversion_factor(from_unit, to_unit)
83
+
84
+ if _DEBUG is False and fastpath is not None:
85
+ return fastpath
86
+
87
+ if fastpath is None:
88
+ has_multiplier = False
89
+ with contextlib.suppress(Exception):
90
+ ix = from_unit.index("*")
91
+ multiplier = float(from_unit[:ix])
92
+ base_from_unit = from_unit[ix + 1 :].strip()
93
+ has_multiplier = True
94
+
95
+ if has_multiplier:
96
+ fastpath = _fastpath_get_unit_conversion_factor(base_from_unit, to_unit)
97
+ fastpath = fastpath if fastpath is None else fastpath * multiplier
98
+ if _DEBUG is False and fastpath is not None:
99
+ return fastpath
100
+
101
+ if _COLLECT_FASTPATH_DATA and fastpath is None:
102
+ if has_multiplier:
103
+ _OBSERVED_UNIT_CONVERSIONS.add((base_from_unit, to_unit))
104
+ else:
105
+ _OBSERVED_UNIT_CONVERSIONS.add((from_unit, to_unit))
106
+
107
+ fallback = _fallback_get_unit_conversion_factor(from_unit, to_unit)
108
+
109
+ if _DEBUG and fastpath is not None and fallback != fastpath:
110
+ message = f"Different results!\nfrom_unit {from_unit} to_unit {to_unit}\nfastpath {fastpath} fallback {fallback}"
111
+ raise RuntimeError(message)
112
+
113
+ if _unit_has_no_floats(from_unit) and _unit_has_no_floats(to_unit):
114
+ _FASTPATH_CONVERSION_FACTORS[(from_unit, to_unit)] = fallback
115
+
116
+ return fallback
117
+
118
+
119
+ def _unit_has_no_floats(unit: str) -> bool:
120
+ if not unit:
121
+ return True
122
+ floats_in_str = re.findall(r"[-+]?(?:\d*\.*\d+)", unit)
123
+ if not floats_in_str:
124
+ return True
125
+ floats_in_str: list[float] = [float(x) for x in floats_in_str]
126
+ return all(x.is_integer() for x in floats_in_str)
127
+
128
+
129
+ def validate_unit_conversion_fastpaths() -> bool:
130
+ """Run-Time validation of fastpaths."""
131
+ errors = []
132
+ for (from_unit, to_unit), result in _FASTPATH_CONVERSION_FACTORS.items():
133
+ sympy_result = None
134
+ with contextlib.suppress(Exception):
135
+ sympy_result = _fallback_get_unit_conversion_factor(from_unit, to_unit)
136
+ if result != sympy_result:
137
+ message = f"'{from_unit}' to '{to_unit}' failed. Fastpath: {result}, SymPy: {sympy_result}"
138
+ errors.append(message)
139
+ for from_unit, to_unit in _FASTPATH_INCOMPATIBLE_CONVERSIONS:
140
+ with contextlib.suppress(Exception):
141
+ sympy_result = _fallback_get_unit_conversion_factor(from_unit, to_unit)
142
+ message = f"'{from_unit}' to '{to_unit}'. Fastpath claim incompatible units, but SymPy fallback returned {sympy_result}"
143
+ errors.append(message)
144
+ if errors:
145
+ message = "\n".join(errors)
146
+ raise RuntimeError(message)
147
+
148
+
149
+ def _fastpath_get_unit_conversion_factor(from_unit: str, to_unit: str) -> float | None:
150
+ """Try to look up the result."""
151
+ key = (from_unit, to_unit)
152
+ if key in _FASTPATH_CONVERSION_FACTORS:
153
+ return _FASTPATH_CONVERSION_FACTORS[key]
154
+ if key in _FASTPATH_INCOMPATIBLE_CONVERSIONS:
155
+ message = f"Cannot convert from '{from_unit}' to '{to_unit}'"
156
+ raise ValueError(message)
157
+ return None
158
+
159
+
160
+ def _fallback_get_unit_conversion_factor(from_unit: str, to_unit: str) -> float | str:
161
+ """Calculate conversion factor using sympy."""
162
+ from_unit_sym = _unit_str_to_sym(from_unit)
163
+ to_unit_sym = _unit_str_to_sym(to_unit)
164
+
165
+ conversion_expr = from_unit_sym / to_unit_sym
166
+
167
+ value = _get_scalar_from_expr(conversion_expr)
168
+
169
+ if not isinstance(value, float):
170
+ s = f"Incompatible units in expression: {conversion_expr}\nSimplified: {value}"
171
+ message = f"Cannot convert from '{from_unit}' to '{to_unit}':\n{s}"
172
+ raise ValueError(message)
173
+
174
+ return value
175
+
176
+
177
+ def _unit_str_to_sym(unit: str) -> SymPyExpr:
178
+ """Convert str unit to valid sympy representation or error."""
179
+ unit = unit.strip()
180
+ x = sympy.sympify(unit, locals=_SUPPORTED_UNITS)
181
+ unsupported_args = [arg for arg in x.args if not (isinstance(arg, Prefix | Quantity | Pow | Symbol) or arg.is_number)]
182
+ if unsupported_args:
183
+ message = f"Unit string '{unit}' not valid. Unsupported args: {unsupported_args}"
184
+ raise ValueError(message)
185
+ return x
186
+
187
+
188
+ def _get_scalar_from_expr(expr_sym: SymPyExpr) -> float | str:
189
+ """Get scalar value from a sympy expression."""
190
+ simplified_expr = expr_sym.simplify()
191
+ if not simplified_expr.is_number:
192
+ for prefix in _SUPPORTED_UNITS.values():
193
+ if isinstance(prefix, Prefix):
194
+ expr_sym = expr_sym.subs(prefix, prefix.scale_factor)
195
+ simplified_expr = expr_sym.simplify()
196
+ try:
197
+ return float(simplified_expr)
198
+ except Exception:
199
+ return str(simplified_expr)
200
+
201
+
202
+ def is_convertable(unit_from: str, unit_to: str) -> bool:
203
+ """Return True if from_unit can be converted to to_unit else False."""
204
+ with contextlib.suppress(Exception):
205
+ get_unit_conversion_factor(unit_from, unit_to)
206
+ return True
207
+ return False
@@ -0,0 +1,11 @@
1
+ # framcore/fingerprints/__init__.py
2
+
3
+ from framcore.fingerprints.fingerprint import Fingerprint, FingerprintDiff, FingerprintDiffType, FingerprintRef
4
+
5
+ __all__ = [
6
+ "Fingerprint",
7
+ "FingerprintDiff",
8
+ "FingerprintDiffType",
9
+ "FingerprintRef",
10
+ ]
11
+