fram-core 0.1.0a1__py3-none-any.whl → 0.1.1__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 → fram_core-0.1.1.dist-info}/METADATA +6 -5
- fram_core-0.1.1.dist-info/RECORD +100 -0
- {fram_core-0.1.0a1.dist-info → fram_core-0.1.1.dist-info}/WHEEL +1 -1
- framcore/Base.py +22 -3
- framcore/Model.py +26 -9
- framcore/__init__.py +2 -1
- framcore/aggregators/Aggregator.py +30 -11
- framcore/aggregators/HydroAggregator.py +37 -25
- framcore/aggregators/NodeAggregator.py +65 -30
- framcore/aggregators/WindSolarAggregator.py +22 -30
- framcore/attributes/Arrow.py +6 -4
- framcore/attributes/ElasticDemand.py +13 -13
- framcore/attributes/ReservoirCurve.py +3 -17
- framcore/attributes/SoftBound.py +2 -5
- framcore/attributes/StartUpCost.py +14 -3
- framcore/attributes/Storage.py +17 -5
- framcore/attributes/TargetBound.py +2 -4
- framcore/attributes/__init__.py +2 -4
- framcore/attributes/hydro/HydroBypass.py +9 -2
- framcore/attributes/hydro/HydroGenerator.py +24 -7
- framcore/attributes/hydro/HydroPump.py +32 -10
- framcore/attributes/hydro/HydroReservoir.py +4 -4
- framcore/attributes/level_profile_attributes.py +250 -53
- framcore/components/Component.py +27 -3
- framcore/components/Demand.py +18 -4
- framcore/components/Flow.py +26 -4
- framcore/components/HydroModule.py +45 -4
- framcore/components/Node.py +32 -9
- framcore/components/Thermal.py +12 -8
- framcore/components/Transmission.py +17 -2
- framcore/components/wind_solar.py +25 -10
- framcore/curves/LoadedCurve.py +0 -9
- framcore/expressions/Expr.py +137 -36
- framcore/expressions/__init__.py +3 -1
- framcore/expressions/_get_constant_from_expr.py +14 -20
- framcore/expressions/queries.py +121 -84
- framcore/expressions/units.py +30 -3
- framcore/fingerprints/fingerprint.py +0 -1
- framcore/juliamodels/JuliaModel.py +13 -3
- framcore/loaders/loaders.py +0 -2
- framcore/metadata/ExprMeta.py +13 -7
- framcore/metadata/LevelExprMeta.py +16 -1
- framcore/metadata/Member.py +7 -7
- framcore/metadata/__init__.py +1 -1
- framcore/querydbs/CacheDB.py +1 -1
- framcore/solvers/Solver.py +21 -6
- framcore/solvers/SolverConfig.py +4 -4
- framcore/timeindexes/AverageYearRange.py +9 -2
- framcore/timeindexes/ConstantTimeIndex.py +7 -2
- framcore/timeindexes/DailyIndex.py +14 -2
- framcore/timeindexes/FixedFrequencyTimeIndex.py +105 -53
- framcore/timeindexes/HourlyIndex.py +14 -2
- framcore/timeindexes/IsoCalendarDay.py +5 -3
- framcore/timeindexes/ListTimeIndex.py +103 -23
- framcore/timeindexes/ModelYear.py +8 -2
- framcore/timeindexes/ModelYears.py +11 -2
- framcore/timeindexes/OneYearProfileTimeIndex.py +10 -2
- framcore/timeindexes/ProfileTimeIndex.py +14 -3
- framcore/timeindexes/SinglePeriodTimeIndex.py +1 -1
- framcore/timeindexes/TimeIndex.py +16 -3
- framcore/timeindexes/WeeklyIndex.py +14 -2
- framcore/{expressions → timeindexes}/_time_vector_operations.py +76 -2
- framcore/timevectors/ConstantTimeVector.py +12 -16
- framcore/timevectors/LinearTransformTimeVector.py +20 -3
- framcore/timevectors/ListTimeVector.py +18 -14
- framcore/timevectors/LoadedTimeVector.py +1 -8
- framcore/timevectors/ReferencePeriod.py +13 -3
- framcore/timevectors/TimeVector.py +26 -12
- framcore/utils/__init__.py +0 -1
- framcore/utils/get_regional_volumes.py +21 -3
- framcore/utils/get_supported_components.py +1 -1
- framcore/utils/global_energy_equivalent.py +22 -5
- framcore/utils/isolate_subnodes.py +12 -3
- framcore/utils/loaders.py +7 -7
- framcore/utils/node_flow_utils.py +4 -4
- framcore/utils/storage_subsystems.py +3 -4
- fram_core-0.1.0a1.dist-info/RECORD +0 -100
- {fram_core-0.1.0a1.dist-info → fram_core-0.1.1.dist-info}/licenses/LICENSE.md +0 -0
|
@@ -164,7 +164,7 @@ def _update_constants_with_units(
|
|
|
164
164
|
if isinstance(obj, TimeVector):
|
|
165
165
|
obj: TimeVector
|
|
166
166
|
|
|
167
|
-
# added to support any_expr * ConstantTimeVector
|
|
167
|
+
# added to support any_expr * ConstantTimeVector. We added this to support LevelProfile.scale()
|
|
168
168
|
times_constant_case = (not is_profile) and isinstance(obj, ConstantTimeVector)
|
|
169
169
|
|
|
170
170
|
if is_level or times_constant_case:
|
|
@@ -215,16 +215,6 @@ def _is_fastpath_product(expr: Expr) -> bool:
|
|
|
215
215
|
return all(arg.is_leaf() or _is_fastpath_sum(arg) for arg in args)
|
|
216
216
|
|
|
217
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
218
|
def _is_fastpath_sum_of_products(expr: Expr) -> bool:
|
|
229
219
|
"""E.g. x1 * (x2 + x3) + x4 * x5 where x is leaf."""
|
|
230
220
|
if expr.is_leaf():
|
|
@@ -344,19 +334,23 @@ def _fastpath_aggregation( # noqa: C901, PLR0911
|
|
|
344
334
|
return num_value / dem_value
|
|
345
335
|
return get_unit_conversion_factor(combined_unit, target_unit) * (num_value / dem_value)
|
|
346
336
|
|
|
347
|
-
|
|
348
|
-
for unit, value in num.items():
|
|
349
|
-
op = "+" if value > 0 else "-"
|
|
350
|
-
combined_num_unit = f"{combined_num_unit} {op} {abs(value)}"
|
|
337
|
+
new_constants_with_units = dict()
|
|
351
338
|
|
|
352
|
-
|
|
339
|
+
combined_num = ""
|
|
353
340
|
for unit, value in num.items():
|
|
354
|
-
|
|
355
|
-
|
|
341
|
+
sym = f"x{len(new_constants_with_units)}"
|
|
342
|
+
new_constants_with_units[sym] = (sym, value, unit)
|
|
343
|
+
combined_num = f"{combined_num} + {sym}"
|
|
344
|
+
|
|
345
|
+
combined_dem = ""
|
|
346
|
+
for unit, value in dem.items():
|
|
347
|
+
sym = f"x{len(new_constants_with_units)}"
|
|
348
|
+
new_constants_with_units[sym] = (sym, value, unit)
|
|
349
|
+
combined_dem = f"{combined_dem} + {sym}"
|
|
356
350
|
|
|
357
|
-
|
|
351
|
+
combined = f"({combined_num})/({combined_dem})"
|
|
358
352
|
|
|
359
|
-
return
|
|
353
|
+
return _sympy_fallback(new_constants_with_units, combined, target_unit)
|
|
360
354
|
|
|
361
355
|
|
|
362
356
|
def _get_fastpath_sum_dict(
|
framcore/expressions/queries.py
CHANGED
|
@@ -1,18 +1,20 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
3
|
import time
|
|
4
|
+
from datetime import timedelta
|
|
4
5
|
from typing import TYPE_CHECKING
|
|
5
6
|
|
|
6
7
|
import numpy as np
|
|
7
8
|
from numpy.typing import NDArray
|
|
8
9
|
|
|
10
|
+
from framcore import check_type
|
|
9
11
|
from framcore.curves import Curve
|
|
10
12
|
from framcore.expressions import Expr
|
|
11
13
|
from framcore.expressions._get_constant_from_expr import _get_constant_from_expr
|
|
12
14
|
from framcore.expressions._utils import _load_model_and_create_model_db
|
|
13
15
|
from framcore.expressions.units import get_unit_conversion_factor
|
|
14
16
|
from framcore.querydbs import QueryDB
|
|
15
|
-
from framcore.timeindexes import FixedFrequencyTimeIndex, SinglePeriodTimeIndex, TimeIndex
|
|
17
|
+
from framcore.timeindexes import FixedFrequencyTimeIndex, ProfileTimeIndex, SinglePeriodTimeIndex, TimeIndex, WeeklyIndex
|
|
16
18
|
from framcore.timevectors import TimeVector
|
|
17
19
|
|
|
18
20
|
if TYPE_CHECKING:
|
|
@@ -28,34 +30,26 @@ def get_level_value(
|
|
|
28
30
|
is_max: bool,
|
|
29
31
|
) -> float:
|
|
30
32
|
"""
|
|
31
|
-
Evaluate
|
|
33
|
+
Evaluate Expr representing a (possibly aggregated) level.
|
|
32
34
|
|
|
33
|
-
The follwing will be automatically
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
35
|
+
The follwing will be automatically handled for you:
|
|
36
|
+
- fetching from different data objecs (from db)
|
|
37
|
+
- conversion to requested unit
|
|
38
|
+
- query at requested TimeIndex for data and scenario dimension, and with requested reference period
|
|
39
|
+
- conversion to requested level type (is_max or is_avg)
|
|
38
40
|
|
|
39
|
-
|
|
41
|
+
Supports all expressions. Will evaluate level Exprs at data_dim (with reference period of scen_dim),
|
|
42
|
+
and profile Exprs as an average over scen_dim (both as constants). Has optimized fastpath methods for sums, products and aggregations.
|
|
43
|
+
The rest uses a fallback method with SymPy.
|
|
40
44
|
|
|
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
45
|
"""
|
|
46
|
+
check_type(expr, Expr) # check expr here since _get_level_value is not recursively called.
|
|
47
|
+
check_type(unit, (str, type(None)))
|
|
48
|
+
check_type(data_dim, SinglePeriodTimeIndex)
|
|
49
|
+
check_type(scen_dim, FixedFrequencyTimeIndex)
|
|
50
|
+
check_type(is_max, bool)
|
|
58
51
|
db = _load_model_and_create_model_db(db)
|
|
52
|
+
|
|
59
53
|
return _get_level_value(expr, db, unit, data_dim, scen_dim, is_max)
|
|
60
54
|
|
|
61
55
|
|
|
@@ -72,7 +66,7 @@ def get_profile_vector(
|
|
|
72
66
|
|
|
73
67
|
expr = sum(weight[i] * profile[i]) where
|
|
74
68
|
|
|
75
|
-
weight[i] >= 0 and is unitless
|
|
69
|
+
weight[i] >= 0 and is unitless, and will be evaluated as a constant
|
|
76
70
|
profile[i] is a unitless profile expr
|
|
77
71
|
|
|
78
72
|
profile[i] is either "zero_one" or "mean_one" type of profile
|
|
@@ -80,15 +74,24 @@ def get_profile_vector(
|
|
|
80
74
|
"zero_one" and "mean_one" profile type must be converted to the
|
|
81
75
|
same standard to be added correctly.
|
|
82
76
|
|
|
83
|
-
The query
|
|
77
|
+
The query parameters data_dim and scen_dim are used to evaluate the values
|
|
78
|
+
requested TimeIndex for data and scenario dimension, and with requested reference period
|
|
84
79
|
|
|
85
|
-
|
|
80
|
+
weight[i] will be evaluated level Exprs at data_dim (with reference period of scen_dim),
|
|
81
|
+
and profile Exprs as an average over scen_dim (both as constants)
|
|
82
|
+
|
|
83
|
+
profile[i] will be evaluated as profile vectors over scen_dim
|
|
86
84
|
|
|
87
85
|
The query parameter is_zero_one tells which profile type the output
|
|
88
86
|
vector should be converted to.
|
|
89
87
|
"""
|
|
88
|
+
# Argument expr checked in _get_profile_vector since it can be recursively called.
|
|
89
|
+
check_type(data_dim, SinglePeriodTimeIndex)
|
|
90
|
+
check_type(scen_dim, FixedFrequencyTimeIndex)
|
|
91
|
+
check_type(is_zero_one, bool)
|
|
92
|
+
check_type(is_float32, bool)
|
|
90
93
|
db = _load_model_and_create_model_db(db)
|
|
91
|
-
|
|
94
|
+
|
|
92
95
|
return _get_profile_vector(expr, db, data_dim, scen_dim, is_zero_one, is_float32)
|
|
93
96
|
|
|
94
97
|
|
|
@@ -126,9 +129,6 @@ def _get_level_value(
|
|
|
126
129
|
scen_dim: FixedFrequencyTimeIndex,
|
|
127
130
|
is_max: bool,
|
|
128
131
|
) -> float:
|
|
129
|
-
# TODO: Implement checks cleaner
|
|
130
|
-
assert isinstance(expr, Expr), f"{expr}"
|
|
131
|
-
|
|
132
132
|
cache_key = ("_get_constant_from_expr", expr, unit, data_dim, scen_dim, is_max)
|
|
133
133
|
if db.has_key(cache_key):
|
|
134
134
|
return db.get(cache_key)
|
|
@@ -148,34 +148,12 @@ def _get_profile_vector(
|
|
|
148
148
|
is_zero_one: bool,
|
|
149
149
|
is_float32: bool = True,
|
|
150
150
|
) -> NDArray:
|
|
151
|
-
|
|
151
|
+
check_type(expr, Expr)
|
|
152
152
|
|
|
153
153
|
if expr.is_leaf():
|
|
154
|
-
|
|
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
|
|
154
|
+
return _get_profile_vector_from_leaf_expr(expr, db, data_dim, scen_dim, is_zero_one, is_float32)
|
|
176
155
|
|
|
177
156
|
ops, args = expr.get_operations(expect_ops=True, copy_list=False)
|
|
178
|
-
|
|
179
157
|
tmp = np.zeros(scen_dim.get_num_periods(), dtype=np.float32 if is_float32 else np.float64)
|
|
180
158
|
|
|
181
159
|
if "+" in ops:
|
|
@@ -192,18 +170,56 @@ def _get_profile_vector(
|
|
|
192
170
|
|
|
193
171
|
profiles = [arg for arg in args if arg.is_profile()]
|
|
194
172
|
weights = [arg for arg in args if not arg.is_profile()]
|
|
173
|
+
|
|
195
174
|
if len(profiles) != 1:
|
|
196
175
|
message = f"Got {len(profiles)} profiles in expr {expr}"
|
|
197
176
|
raise ValueError(message)
|
|
177
|
+
|
|
198
178
|
total_weight = 0.0
|
|
199
179
|
is_max = False # use avg-values to calculate weights
|
|
180
|
+
|
|
200
181
|
for weight_expr in weights:
|
|
201
182
|
total_weight += _get_constant_from_expr(weight_expr, db, None, data_dim, scen_dim, is_max)
|
|
183
|
+
|
|
202
184
|
out = _get_profile_vector(profiles[0], db, data_dim, scen_dim, is_zero_one, is_float32)
|
|
203
185
|
np.multiply(out, total_weight, out=out)
|
|
204
186
|
return out
|
|
205
187
|
|
|
206
188
|
|
|
189
|
+
def _get_profile_vector_from_leaf_expr(
|
|
190
|
+
expr: Expr,
|
|
191
|
+
db: QueryDB,
|
|
192
|
+
data_dim: SinglePeriodTimeIndex,
|
|
193
|
+
scen_dim: FixedFrequencyTimeIndex,
|
|
194
|
+
is_zero_one: bool,
|
|
195
|
+
is_float32: bool,
|
|
196
|
+
) -> NDArray:
|
|
197
|
+
src = expr.get_src()
|
|
198
|
+
|
|
199
|
+
if isinstance(src, str):
|
|
200
|
+
obj = db.get(src)
|
|
201
|
+
else:
|
|
202
|
+
obj = src
|
|
203
|
+
|
|
204
|
+
if isinstance(obj, Expr):
|
|
205
|
+
if not obj.is_profile():
|
|
206
|
+
msg = f"Expected {obj} to be is_profile=True." # User may be getting this from setting wrong metadata in time vector files?
|
|
207
|
+
raise ValueError(msg)
|
|
208
|
+
return _get_profile_vector(obj, db, data_dim, scen_dim, is_zero_one, is_float32)
|
|
209
|
+
|
|
210
|
+
assert isinstance(obj, (TimeVector, Curve))
|
|
211
|
+
|
|
212
|
+
cache_key = ("_get_profile_vector_from_timevector", obj, data_dim, scen_dim, is_zero_one, is_float32)
|
|
213
|
+
if db.has_key(cache_key):
|
|
214
|
+
vector: NDArray = db.get(cache_key)
|
|
215
|
+
return vector.copy()
|
|
216
|
+
t0 = time.perf_counter()
|
|
217
|
+
vector = _get_profile_vector_from_timevector(obj, scen_dim, is_zero_one, is_float32)
|
|
218
|
+
t1 = time.perf_counter()
|
|
219
|
+
db.put(cache_key, vector, elapsed_seconds=t1 - t0)
|
|
220
|
+
return vector
|
|
221
|
+
|
|
222
|
+
|
|
207
223
|
def _get_profile_vector_from_timevector(
|
|
208
224
|
timevector: TimeVector,
|
|
209
225
|
scen_dim: FixedFrequencyTimeIndex,
|
|
@@ -254,7 +270,6 @@ def _get_profile_vector_from_timevector(
|
|
|
254
270
|
def _recursively_update_units(units: set[str], db: QueryDB, expr: Expr) -> None:
|
|
255
271
|
if expr.is_leaf():
|
|
256
272
|
src = expr.get_src()
|
|
257
|
-
|
|
258
273
|
obj = src if isinstance(src, TimeVector) else db.get(key=src)
|
|
259
274
|
|
|
260
275
|
if isinstance(obj, Expr):
|
|
@@ -274,27 +289,6 @@ def _recursively_update_units(units: set[str], db: QueryDB, expr: Expr) -> None:
|
|
|
274
289
|
_recursively_update_units(units, db, arg)
|
|
275
290
|
|
|
276
291
|
|
|
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
292
|
def _recursively_update_timeindexes(timeindexes: set[TimeIndex], db: QueryDB, expr: Expr) -> None:
|
|
299
293
|
if expr.is_leaf():
|
|
300
294
|
src = expr.get_src()
|
|
@@ -315,7 +309,7 @@ def _recursively_update_timeindexes(timeindexes: set[TimeIndex], db: QueryDB, ex
|
|
|
315
309
|
_recursively_update_timeindexes(timeindexes, db, arg)
|
|
316
310
|
|
|
317
311
|
|
|
318
|
-
def _get_level_value_from_timevector( # noqa: C901
|
|
312
|
+
def _get_level_value_from_timevector( # noqa: C901, PLR0915
|
|
319
313
|
timevector: TimeVector,
|
|
320
314
|
db: QueryDB,
|
|
321
315
|
target_unit: str | None,
|
|
@@ -338,8 +332,8 @@ def _get_level_value_from_timevector( # noqa: C901
|
|
|
338
332
|
from_unit = timevector.get_unit()
|
|
339
333
|
|
|
340
334
|
starttime = data_dim.get_start_time() # OPPGAVE endrer ConstantTimeIndex-API?
|
|
341
|
-
|
|
342
|
-
scalar = timeindex.get_period_average(values, starttime,
|
|
335
|
+
timedelta_ = data_dim.get_period_duration() # OPPGAVE endrer ConstantTimeIndex-API?
|
|
336
|
+
scalar = timeindex.get_period_average(values, starttime, timedelta_, data_dim.is_52_week_years())
|
|
343
337
|
|
|
344
338
|
if from_unit is not None and target_unit is not None:
|
|
345
339
|
scalar *= get_unit_conversion_factor(from_unit, target_unit)
|
|
@@ -366,16 +360,59 @@ def _get_level_value_from_timevector( # noqa: C901
|
|
|
366
360
|
# assert tv_ref_period_mean.size == 1
|
|
367
361
|
# assert tv_ref_period_mean[0] == 1
|
|
368
362
|
assert profile_expr, f"Profile Expr is None for TimeVector {timevector} when it should exist"
|
|
369
|
-
|
|
363
|
+
|
|
364
|
+
start_year = min(tv_ref_period.get_start_year(), target_ref_period.get_start_year())
|
|
365
|
+
end_year_1 = tv_ref_period.get_start_year() + tv_ref_period.get_num_years() - 1
|
|
366
|
+
end_year_2 = target_ref_period.get_start_year() + target_ref_period.get_num_years() - 1
|
|
367
|
+
end_year = max(end_year_1, end_year_2)
|
|
368
|
+
num_years = end_year - start_year + 1
|
|
369
|
+
|
|
370
|
+
pti = ProfileTimeIndex(
|
|
371
|
+
start_year=start_year,
|
|
372
|
+
num_years=num_years,
|
|
373
|
+
period_duration=timedelta(weeks=1),
|
|
374
|
+
is_52_week_years=scen_dim.is_52_week_years(),
|
|
375
|
+
)
|
|
376
|
+
|
|
377
|
+
profile_vector = get_profile_vector(
|
|
370
378
|
profile_expr,
|
|
371
379
|
db,
|
|
372
380
|
data_dim,
|
|
373
|
-
|
|
381
|
+
pti,
|
|
374
382
|
is_zero_one=False,
|
|
375
383
|
is_float32=is_float32,
|
|
376
384
|
)
|
|
377
|
-
|
|
378
|
-
|
|
385
|
+
|
|
386
|
+
assert np.isclose(profile_vector.mean(), 1.0)
|
|
387
|
+
|
|
388
|
+
ref_period_index = WeeklyIndex(
|
|
389
|
+
start_year=tv_ref_period.get_start_year(),
|
|
390
|
+
num_years=tv_ref_period.get_num_years(),
|
|
391
|
+
is_52_week_years=scen_dim.is_52_week_years(),
|
|
392
|
+
)
|
|
393
|
+
|
|
394
|
+
avg_ref_period = pti.get_period_average(
|
|
395
|
+
vector=profile_vector,
|
|
396
|
+
start_time=ref_period_index.get_start_time(),
|
|
397
|
+
duration=ref_period_index.total_duration(),
|
|
398
|
+
is_52_week_years=ref_period_index.is_52_week_years(),
|
|
399
|
+
)
|
|
400
|
+
|
|
401
|
+
target_ref_period_index = WeeklyIndex(
|
|
402
|
+
start_year=target_ref_period.get_start_year(),
|
|
403
|
+
num_years=target_ref_period.get_num_years(),
|
|
404
|
+
is_52_week_years=scen_dim.is_52_week_years(),
|
|
405
|
+
)
|
|
406
|
+
|
|
407
|
+
avg_target_ref_period = pti.get_period_average(
|
|
408
|
+
vector=profile_vector,
|
|
409
|
+
start_time=target_ref_period_index.get_start_time(),
|
|
410
|
+
duration=target_ref_period_index.total_duration(),
|
|
411
|
+
is_52_week_years=target_ref_period_index.is_52_week_years(),
|
|
412
|
+
)
|
|
413
|
+
|
|
414
|
+
scale = avg_target_ref_period / avg_ref_period
|
|
415
|
+
avg_level = scale * avg_level
|
|
379
416
|
return avg_level
|
|
380
417
|
# timevector fra max til avg
|
|
381
418
|
max_level = scalar
|
framcore/expressions/units.py
CHANGED
|
@@ -1,3 +1,9 @@
|
|
|
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
|
+
|
|
1
7
|
from __future__ import annotations
|
|
2
8
|
|
|
3
9
|
import contextlib
|
|
@@ -20,6 +26,7 @@ _SUPPORTED_UNITS = {
|
|
|
20
26
|
"year": year,
|
|
21
27
|
"y": year,
|
|
22
28
|
"watt": watt,
|
|
29
|
+
"joule": watt * second,
|
|
23
30
|
"g": gram,
|
|
24
31
|
"gram": gram,
|
|
25
32
|
"kg": kilo * gram,
|
|
@@ -28,6 +35,7 @@ _SUPPORTED_UNITS = {
|
|
|
28
35
|
"meter": meter,
|
|
29
36
|
"m": meter,
|
|
30
37
|
"m3": meter**3,
|
|
38
|
+
"l": meter**3 / 1000,
|
|
31
39
|
"Mm3": mega * meter**3,
|
|
32
40
|
"m3/s": meter**3 / second,
|
|
33
41
|
"kilo": kilo,
|
|
@@ -38,6 +46,10 @@ _SUPPORTED_UNITS = {
|
|
|
38
46
|
"MWh": mega * watt * hour,
|
|
39
47
|
"GWh": giga * watt * hour,
|
|
40
48
|
"TWh": tera * watt * hour,
|
|
49
|
+
"J": watt * second,
|
|
50
|
+
"kJ": kilo * watt * second,
|
|
51
|
+
"MJ": mega * watt * second,
|
|
52
|
+
"GJ": giga * watt * second,
|
|
41
53
|
"kW": kilo * watt,
|
|
42
54
|
"MW": mega * watt,
|
|
43
55
|
"GW": giga * watt,
|
|
@@ -55,6 +67,7 @@ _FASTPATH_CONVERSION_FACTORS = {
|
|
|
55
67
|
("GWh/year", "MW"): 0.11407955544967756,
|
|
56
68
|
("Mm3/year", "m3/s"): 0.03168876540268821,
|
|
57
69
|
("t/MWh", "t/GWh"): 1000.0,
|
|
70
|
+
("MWh", "GJ"): 3.6,
|
|
58
71
|
}
|
|
59
72
|
|
|
60
73
|
_FASTPATH_INCOMPATIBLE_CONVERSIONS = {
|
|
@@ -70,14 +83,13 @@ _COLLECT_FASTPATH_DATA = False
|
|
|
70
83
|
_OBSERVED_UNIT_CONVERSIONS = set()
|
|
71
84
|
|
|
72
85
|
|
|
73
|
-
def get_unit_conversion_factor(from_unit: str | None, to_unit: str | None) -> float:
|
|
86
|
+
def get_unit_conversion_factor(from_unit: str | None, to_unit: str | None) -> float: # noqa C901
|
|
74
87
|
"""Get the conversion factor from one unit to another."""
|
|
75
88
|
if from_unit == to_unit:
|
|
76
89
|
return 1.0
|
|
77
90
|
|
|
78
91
|
if from_unit is None or to_unit is None:
|
|
79
|
-
|
|
80
|
-
raise ValueError(message)
|
|
92
|
+
return _get_unit_conversion_factor_with_none(from_unit, to_unit)
|
|
81
93
|
|
|
82
94
|
fastpath = _fastpath_get_unit_conversion_factor(from_unit, to_unit)
|
|
83
95
|
|
|
@@ -116,6 +128,21 @@ def get_unit_conversion_factor(from_unit: str | None, to_unit: str | None) -> fl
|
|
|
116
128
|
return fallback
|
|
117
129
|
|
|
118
130
|
|
|
131
|
+
def _get_unit_conversion_factor_with_none(from_unit: str | None, to_unit: str | None) -> float:
|
|
132
|
+
if from_unit:
|
|
133
|
+
try:
|
|
134
|
+
return get_unit_conversion_factor(from_unit, "1")
|
|
135
|
+
except Exception:
|
|
136
|
+
pass
|
|
137
|
+
if to_unit:
|
|
138
|
+
try:
|
|
139
|
+
return get_unit_conversion_factor("1", to_unit)
|
|
140
|
+
except Exception:
|
|
141
|
+
pass
|
|
142
|
+
message = f"Incompatible units: from_unit {from_unit} to_unit {to_unit}"
|
|
143
|
+
raise ValueError(message)
|
|
144
|
+
|
|
145
|
+
|
|
119
146
|
def _unit_has_no_floats(unit: str) -> bool:
|
|
120
147
|
if not unit:
|
|
121
148
|
return True
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
"""Manage Julia environment and usage of juliacall."""
|
|
1
|
+
"""Manage Julia environment and usage of juliacall for Solvers implemented in the Julia language."""
|
|
2
2
|
|
|
3
3
|
import importlib
|
|
4
4
|
import os
|
|
@@ -43,14 +43,16 @@ class JuliaModel(Base):
|
|
|
43
43
|
julia_path: Path | str | None = None,
|
|
44
44
|
dependencies: list[str | tuple[str, str | None]] | None = None,
|
|
45
45
|
skip_install_dependencies: bool = False,
|
|
46
|
+
force_julia_install: bool = True,
|
|
46
47
|
) -> None:
|
|
47
48
|
"""
|
|
48
49
|
Initialize management of Julia model, environment and dependencies.
|
|
49
50
|
|
|
50
51
|
The three parameters env_path, depot_path and julia_path sets environment variables for locations of your Julia
|
|
51
52
|
environment, packages and language.
|
|
52
|
-
|
|
53
|
-
|
|
53
|
+
|
|
54
|
+
- If user has not specified locations, the default is to use the current python/conda environment.
|
|
55
|
+
- If a system installation of Python is used, the default is set to the current user location.
|
|
54
56
|
|
|
55
57
|
Args:
|
|
56
58
|
env_path (Path | str | None, optional): Path to location of Julia environment. If it doesnt exist it will be
|
|
@@ -61,6 +63,8 @@ class JuliaModel(Base):
|
|
|
61
63
|
doesnt exist. Defaults to None.
|
|
62
64
|
dependencies (list[str] | None, optional): List of dependencies of the model. The strings in the list can be
|
|
63
65
|
either urls or Julia package names.. Defaults to None.
|
|
66
|
+
skip_install_dependencies (bool, optional): Skip installation of dependencies. Defaults to False.
|
|
67
|
+
force_julia_install (bool): Force new Julia install.
|
|
64
68
|
|
|
65
69
|
"""
|
|
66
70
|
self._check_type(env_path, (Path, str, type(None)))
|
|
@@ -74,6 +78,7 @@ class JuliaModel(Base):
|
|
|
74
78
|
self._julia_path = julia_path
|
|
75
79
|
self._dependencies = dependencies if dependencies else []
|
|
76
80
|
self._skip_install_dependencies = skip_install_dependencies
|
|
81
|
+
self._force_julia_install = force_julia_install
|
|
77
82
|
|
|
78
83
|
self._jlpkg = None
|
|
79
84
|
self._initialize_julia()
|
|
@@ -96,6 +101,11 @@ class JuliaModel(Base):
|
|
|
96
101
|
if self._julia_path: # If Julia path is not set, let JuliaCall handle defaults.
|
|
97
102
|
os.environ["PYTHON_JULIAPKG_EXE"] = str(self._julia_path)
|
|
98
103
|
|
|
104
|
+
if self._force_julia_install:
|
|
105
|
+
path = os.environ.get("PATH", "")
|
|
106
|
+
cleaned = os.pathsep.join(p for p in path.split(os.pathsep) if "julia" not in p.lower())
|
|
107
|
+
os.environ["PATH"] = cleaned
|
|
108
|
+
|
|
99
109
|
juliacall = importlib.import_module("juliacall")
|
|
100
110
|
JuliaModel._jl = juliacall.Main
|
|
101
111
|
self._jlpkg = juliacall.Pkg
|
framcore/loaders/loaders.py
CHANGED
|
@@ -47,8 +47,6 @@ class Loader(Base, ABC):
|
|
|
47
47
|
"""Clear cached data from the loader."""
|
|
48
48
|
pass
|
|
49
49
|
|
|
50
|
-
# TODO: Is this correct? Also figure out how sharing Loaders should be when copying model given filepaths and copied
|
|
51
|
-
# database
|
|
52
50
|
def __deepcopy__(self, memo: dict) -> Loader:
|
|
53
51
|
"""
|
|
54
52
|
Overwrite deepcopy.
|
framcore/metadata/ExprMeta.py
CHANGED
|
@@ -2,8 +2,7 @@ from __future__ import annotations
|
|
|
2
2
|
|
|
3
3
|
from framcore.expressions import Expr
|
|
4
4
|
from framcore.fingerprints import Fingerprint
|
|
5
|
-
from framcore.metadata import Div
|
|
6
|
-
from framcore.metadata.Meta import Meta # NB! full import path needed for inheritance to work
|
|
5
|
+
from framcore.metadata import Div, Meta
|
|
7
6
|
|
|
8
7
|
|
|
9
8
|
class ExprMeta(Meta):
|
|
@@ -20,6 +19,8 @@ class ExprMeta(Meta):
|
|
|
20
19
|
|
|
21
20
|
def __repr__(self) -> str:
|
|
22
21
|
"""Overwrite __repr__ for better string representation."""
|
|
22
|
+
if not hasattr(self, "_value"):
|
|
23
|
+
return f"{type(self).__name__}(uninitialized)"
|
|
23
24
|
return f"{type(self).__name__}(expr={self._value})"
|
|
24
25
|
|
|
25
26
|
def __eq__(self, other: object) -> bool:
|
|
@@ -28,7 +29,11 @@ class ExprMeta(Meta):
|
|
|
28
29
|
return False
|
|
29
30
|
return self._value == other._value
|
|
30
31
|
|
|
31
|
-
def
|
|
32
|
+
def __hash__(self) -> int:
|
|
33
|
+
"""Compute the hash of the ExprMeta."""
|
|
34
|
+
return hash(self._value)
|
|
35
|
+
|
|
36
|
+
def get_value(self) -> Expr:
|
|
32
37
|
"""Return expr."""
|
|
33
38
|
return self._value
|
|
34
39
|
|
|
@@ -40,10 +45,11 @@ class ExprMeta(Meta):
|
|
|
40
45
|
def combine(self, other: Meta) -> Expr | Div:
|
|
41
46
|
"""Sum Expr."""
|
|
42
47
|
if isinstance(other, ExprMeta):
|
|
43
|
-
return
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
48
|
+
return self._value + other.get_value()
|
|
49
|
+
|
|
50
|
+
div = Div(self)
|
|
51
|
+
div.set_value(other)
|
|
52
|
+
return div
|
|
47
53
|
|
|
48
54
|
def get_fingerprint(self) -> Fingerprint:
|
|
49
55
|
"""Get the fingerprint of the ScalarMeta."""
|
|
@@ -13,5 +13,20 @@ class LevelExprMeta(ExprMeta):
|
|
|
13
13
|
"""
|
|
14
14
|
|
|
15
15
|
def __init__(self, value: Expr | TimeVector) -> None:
|
|
16
|
-
"""
|
|
16
|
+
"""
|
|
17
|
+
Create new LevelExprMeta with Expr value.
|
|
18
|
+
|
|
19
|
+
Args:
|
|
20
|
+
value (Expr | TimeVector): Accepts Expr with is_level=True or TimeVector with is_max_level=True/False.
|
|
21
|
+
|
|
22
|
+
Raises:
|
|
23
|
+
TypeError: If value is not Expr or TimeVector.
|
|
24
|
+
ValueError: If value is non-level Expr or TimeVector.
|
|
25
|
+
|
|
26
|
+
"""
|
|
27
|
+
self._check_type(value, (Expr, TimeVector))
|
|
28
|
+
|
|
29
|
+
if isinstance(value, TimeVector) and value.is_max_level() is None:
|
|
30
|
+
raise ValueError("Parameter 'value' (TimeVector) must be a level (is_max_level must be True or False).")
|
|
31
|
+
|
|
17
32
|
self._value = ensure_expr(value, is_level=True)
|
framcore/metadata/Member.py
CHANGED
|
@@ -7,17 +7,17 @@ from framcore.metadata.Meta import Meta # NB! full import path needed for inher
|
|
|
7
7
|
|
|
8
8
|
class Member(Meta):
|
|
9
9
|
"""
|
|
10
|
-
Member represent membership to a catergory
|
|
10
|
+
Member represent membership to a catergory or group using a str. Subclass of Meta.
|
|
11
11
|
|
|
12
12
|
Should not have missing values.
|
|
13
13
|
|
|
14
14
|
When used, all components must have a membership.
|
|
15
15
|
"""
|
|
16
16
|
|
|
17
|
-
def __init__(self, value: str
|
|
17
|
+
def __init__(self, value: str) -> None:
|
|
18
18
|
"""Create new member with str value."""
|
|
19
|
-
self._value = value
|
|
20
|
-
self._check_type(value,
|
|
19
|
+
self._value = value
|
|
20
|
+
self._check_type(value, str)
|
|
21
21
|
|
|
22
22
|
def __repr__(self) -> str:
|
|
23
23
|
"""Overwrite __repr__ for better string representation."""
|
|
@@ -33,13 +33,13 @@ class Member(Meta):
|
|
|
33
33
|
"""Overwrite __hash__ since its added to sets."""
|
|
34
34
|
return hash(self.__repr__())
|
|
35
35
|
|
|
36
|
-
def get_value(self) -> str
|
|
36
|
+
def get_value(self) -> str:
|
|
37
37
|
"""Return str value."""
|
|
38
38
|
return self._value
|
|
39
39
|
|
|
40
|
-
def set_value(self, value: str
|
|
40
|
+
def set_value(self, value: str) -> None:
|
|
41
41
|
"""Set str value. TypeError if not str."""
|
|
42
|
-
self._check_type(value,
|
|
42
|
+
self._check_type(value, str)
|
|
43
43
|
self._value = value
|
|
44
44
|
|
|
45
45
|
def combine(self, other: Meta) -> Member | Div:
|
framcore/metadata/__init__.py
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
# framcore/metadata/__init__.py
|
|
2
2
|
|
|
3
|
+
from framcore.metadata.Meta import Meta
|
|
3
4
|
from framcore.metadata.Div import Div
|
|
4
5
|
from framcore.metadata.ExprMeta import ExprMeta
|
|
5
6
|
from framcore.metadata.LevelExprMeta import LevelExprMeta
|
|
6
7
|
from framcore.metadata.Member import Member
|
|
7
|
-
from framcore.metadata.Meta import Meta
|
|
8
8
|
|
|
9
9
|
__all__ = [
|
|
10
10
|
"Div",
|
framcore/querydbs/CacheDB.py
CHANGED
|
@@ -21,7 +21,7 @@ class CacheDB(QueryDB):
|
|
|
21
21
|
def set_min_elapsed_seconds(self, value: float) -> None:
|
|
22
22
|
"""Values that takes below this threshold to compute, does not get cached."""
|
|
23
23
|
self._check_type(value, float)
|
|
24
|
-
self._check_float(lower_bound=0.0)
|
|
24
|
+
self._check_float(value=value, lower_bound=0.0, upper_bound=None)
|
|
25
25
|
self._min_elapsed_seconds = value
|
|
26
26
|
|
|
27
27
|
def get_min_elapsed_seconds(self) -> float:
|