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