fram-core 0.1.0a2__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.0a2.dist-info → fram_core-0.1.1.dist-info}/METADATA +4 -4
- fram_core-0.1.1.dist-info/RECORD +100 -0
- {fram_core-0.1.0a2.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 +35 -23
- 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.0a2.dist-info/RECORD +0 -100
- {fram_core-0.1.0a2.dist-info → fram_core-0.1.1.dist-info}/licenses/LICENSE.md +0 -0
|
@@ -10,7 +10,9 @@ from framcore import Base
|
|
|
10
10
|
from framcore.expressions import (
|
|
11
11
|
Expr,
|
|
12
12
|
ensure_expr,
|
|
13
|
+
get_leaf_profiles,
|
|
13
14
|
get_level_value,
|
|
15
|
+
get_profile_exprs_from_leaf_levels,
|
|
14
16
|
get_profile_vector,
|
|
15
17
|
get_timeindexes_from_expr,
|
|
16
18
|
get_units_from_expr,
|
|
@@ -27,7 +29,45 @@ if TYPE_CHECKING:
|
|
|
27
29
|
|
|
28
30
|
# TODO: Name all abstract classes Abstract[clsname]
|
|
29
31
|
class LevelProfile(Base, ABC):
|
|
30
|
-
"""
|
|
32
|
+
"""
|
|
33
|
+
Attributes representing timeseries data for Components. Mostly as Level * Profile, where both Level and Profile are Expr (expressions).
|
|
34
|
+
|
|
35
|
+
Level and Profile represent two distinct dimensions of time. This is because we want to simulate future system states with historical weather patterns.
|
|
36
|
+
Therefore, Level represents the system state at a given time (data_dim), while Profile represents the scenario dimension (scen_dim).
|
|
37
|
+
A Level would for example represent the installed capacity of solar plants towards 2030,
|
|
38
|
+
while the Profile would represent the historical variation between 1991-2020.
|
|
39
|
+
|
|
40
|
+
Level and Profile can have two main formats: A maximum Level with a Profile that varies between 0-1,
|
|
41
|
+
and an average Level with a Profile with a mean of 1 (the latter can have a ReferencePeriod).
|
|
42
|
+
The max format is, for example, used for capacities, while the mean format can be used for prices and flows.
|
|
43
|
+
The system needs to be able to convert between the two formats. This is especially important for aggregations
|
|
44
|
+
(for example weighted averages) where all the TimeVectors need to be on the same format for a correct result.
|
|
45
|
+
One simple example of conversion is pairing a max Level of 100 MW with a mean_one Profile [0, 1, 2].
|
|
46
|
+
Asking for this on the max format will return the series 100*[0, 0.5, 1] MW, while on the avg format it will return 50*[0, 1, 2] MW.
|
|
47
|
+
|
|
48
|
+
Queries to LevelProfile need to provide a database, the desired target TimeIndex for both dimensions, the target unit and the desired format.
|
|
49
|
+
At the moment we support these queries for LevelProfile:
|
|
50
|
+
- self.get_data_value(db, scen_dim, data_dim, unit, is_max_level)
|
|
51
|
+
- self.get_scenario_vector(db, scen_dim, data_dim, unit, is_float32)
|
|
52
|
+
|
|
53
|
+
In addition, we have the possibility to shift, scale, and change the intercept of the LevelProfiles.
|
|
54
|
+
Then we get the full representation: Scale * (Level + Level_shift) * Profile + Intercept.
|
|
55
|
+
- Level_shift adds a constant value to Level, has the same Profile as Level.
|
|
56
|
+
- Scale multiplies (Level + Level_shift) by a constant value.
|
|
57
|
+
- Intercept adds a constant value to LevelProfile, ignoring Level and Profile. **This is the only way of supporting a timeseries that crosses zero
|
|
58
|
+
in our system. This functionality is under development and has not been properly tested.**
|
|
59
|
+
|
|
60
|
+
LevelProfiles also have additional properties that describes their behaviour. These can be used for initialization, validation,
|
|
61
|
+
and to simplify queries. The properties are:
|
|
62
|
+
- is_stock: True if attribute is a stock variable. Level Expr should also have is_stock=True. See Expr for details.
|
|
63
|
+
- is_flow: True if attribute is a flow variable. Level Expr should also have is_flow=True. See Expr for details.
|
|
64
|
+
- is_not_negative: True if attribute is not allowed to have negative values. Level Expr should also have only non-negative values.
|
|
65
|
+
- is_max_and_zero_one: Preferred format of Level and Profile. Used for initialization and queries.
|
|
66
|
+
- is_ingoing: True if attribute is ingoing, False if outgoing, None if neither.
|
|
67
|
+
- is_cost: True if attribute is objective function cost coefficient. Else None.
|
|
68
|
+
- is_unitless: True if attribute is known to be unitless. False if known to have a unit that is not None. Else None.
|
|
69
|
+
|
|
70
|
+
"""
|
|
31
71
|
|
|
32
72
|
# must be overwritten by subclass when otherwise
|
|
33
73
|
# don't change the defaults
|
|
@@ -52,7 +92,24 @@ class LevelProfile(Base, ABC):
|
|
|
52
92
|
intercept: Expr | None = None,
|
|
53
93
|
scale: Expr | None = None,
|
|
54
94
|
) -> None:
|
|
55
|
-
"""
|
|
95
|
+
"""
|
|
96
|
+
Initialize LevelProfile.
|
|
97
|
+
|
|
98
|
+
See the LevelProfile class docstring for details. A complete LevelProfile is represented as:
|
|
99
|
+
Scale * (Level + Level_shift) * Profile + Intercept. Normally only Level and Profile are used.
|
|
100
|
+
|
|
101
|
+
Either give level and profile, or value and unit.
|
|
102
|
+
|
|
103
|
+
Args:
|
|
104
|
+
level (Expr | TimeVector | str | None, optional): Level Expr. Defaults to None.
|
|
105
|
+
profile (Expr | TimeVector | str | None, optional): Profile Expr. Defaults to None.
|
|
106
|
+
value (float | int | None, optional): A constant value to initialize Level. Defaults to None.
|
|
107
|
+
unit (str | None, optional): Unit of the constant value to initialize Level. Defaults to None.
|
|
108
|
+
level_shift (Expr | None, optional): Level_shift Expr. Defaults to None.
|
|
109
|
+
intercept (Expr | None, optional): Intercept Expr. Defaults to None.
|
|
110
|
+
scale (Expr | None, optional): Scale Expr. Defaults to None.
|
|
111
|
+
|
|
112
|
+
"""
|
|
56
113
|
self._assert_invariants()
|
|
57
114
|
|
|
58
115
|
self._check_type(value, (float, int, type(None)))
|
|
@@ -62,11 +119,18 @@ class LevelProfile(Base, ABC):
|
|
|
62
119
|
self._check_type(level_shift, (Expr, type(None)))
|
|
63
120
|
self._check_type(intercept, (Expr, type(None)))
|
|
64
121
|
self._check_type(scale, (Expr, type(None)))
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
self.
|
|
68
|
-
self.
|
|
69
|
-
self.
|
|
122
|
+
level = self._ensure_level_expr(level, value, unit)
|
|
123
|
+
profile = self._ensure_profile_expr(profile)
|
|
124
|
+
self._ensure_compatible_level_profile_combo(level, profile)
|
|
125
|
+
self._ensure_compatible_level_profile_combo(level_shift, profile)
|
|
126
|
+
self._level: Expr | None = level
|
|
127
|
+
self._profile: Expr | None = profile
|
|
128
|
+
self._level_shift: Expr | None = level_shift
|
|
129
|
+
self._intercept: Expr | None = intercept
|
|
130
|
+
self._scale: Expr | None = scale
|
|
131
|
+
# TODO: Validate that profiles are equal in level and level_shift.
|
|
132
|
+
# TODO: Validate that level_shift, scale and intercept only consist of Exprs with ConstantTimeVectors
|
|
133
|
+
# TODO: Validate that level_shift, level_scale and intercept have correct Expr properties
|
|
70
134
|
|
|
71
135
|
def _assert_invariants(self) -> None:
|
|
72
136
|
abstract = self._IS_ABSTRACT
|
|
@@ -101,7 +165,7 @@ class LevelProfile(Base, ABC):
|
|
|
101
165
|
|
|
102
166
|
def add_loaders(self, loaders: set[Loader]) -> None:
|
|
103
167
|
"""Add all loaders stored in expressions to loaders."""
|
|
104
|
-
from framcore.utils import add_loaders_if
|
|
168
|
+
from framcore.utils import add_loaders_if
|
|
105
169
|
|
|
106
170
|
add_loaders_if(loaders, self.get_level())
|
|
107
171
|
add_loaders_if(loaders, self.get_profile())
|
|
@@ -217,7 +281,7 @@ class LevelProfile(Base, ABC):
|
|
|
217
281
|
is_flow=level.is_flow(),
|
|
218
282
|
is_level=True,
|
|
219
283
|
is_profile=False,
|
|
220
|
-
profile=self._profile,
|
|
284
|
+
profile=self._profile,
|
|
221
285
|
)
|
|
222
286
|
|
|
223
287
|
if self._level_shift is not None:
|
|
@@ -229,18 +293,22 @@ class LevelProfile(Base, ABC):
|
|
|
229
293
|
return level
|
|
230
294
|
|
|
231
295
|
def set_level(self, level: Expr | TimeVector | str | None) -> None:
|
|
232
|
-
"""Set level part of (level * profile + intercept)."""
|
|
296
|
+
"""Set level part of (scale * (level + level_shift) * profile + intercept)."""
|
|
233
297
|
self._check_type(level, (Expr, TimeVector, str, type(None)))
|
|
234
|
-
|
|
298
|
+
level = self._ensure_level_expr(level)
|
|
299
|
+
self._ensure_compatible_level_profile_combo(level, self._profile)
|
|
300
|
+
self._level = level
|
|
235
301
|
|
|
236
302
|
def get_profile(self) -> Expr | None:
|
|
237
303
|
"""Get profile part of (level * profile + intercept)."""
|
|
238
304
|
return self._profile
|
|
239
305
|
|
|
240
306
|
def set_profile(self, profile: Expr | TimeVector | str | None) -> None:
|
|
241
|
-
"""Set profile part of (level * profile + intercept)."""
|
|
307
|
+
"""Set profile part of (scale * (level + level_shift) * profile + intercept)."""
|
|
242
308
|
self._check_type(profile, (Expr, TimeVector, str, type(None)))
|
|
243
|
-
|
|
309
|
+
profile = self._ensure_profile_expr(profile)
|
|
310
|
+
self._ensure_compatible_level_profile_combo(self._level, profile)
|
|
311
|
+
self._profile = profile
|
|
244
312
|
|
|
245
313
|
def get_intercept(self) -> Expr | None:
|
|
246
314
|
"""Get intercept part of (level * profile + intercept)."""
|
|
@@ -291,7 +359,22 @@ class LevelProfile(Base, ABC):
|
|
|
291
359
|
unit: str | None,
|
|
292
360
|
is_float32: bool = True,
|
|
293
361
|
) -> NDArray:
|
|
294
|
-
"""
|
|
362
|
+
"""
|
|
363
|
+
Evaluate LevelProfile over the periods in scenario dimension, and at the level period of the data dimension.
|
|
364
|
+
|
|
365
|
+
Underlying profiles are evalutated over the scenario dimension,
|
|
366
|
+
and levels are evalutated to scalars over level_period in the data dimension.
|
|
367
|
+
|
|
368
|
+
Args:
|
|
369
|
+
db (QueryDB | Model): The database or model instance used to fetch the required data.
|
|
370
|
+
scenario_horizon (FixedFrequencyTimeIndex): TimeIndex of the scenario dimension to evaluate profiles.
|
|
371
|
+
level_period (SinglePeriodTimeIndex): TimeIndex of the data dimension to evaluate levels.
|
|
372
|
+
unit (str | None): The unit to convert the resulting values into (e.g., MW, GWh). If None,
|
|
373
|
+
the expression should be unitless.
|
|
374
|
+
is_float32 (bool, optional): Whether to return the vector as a NumPy array with `float32`
|
|
375
|
+
precision. Defaults to True.
|
|
376
|
+
|
|
377
|
+
"""
|
|
295
378
|
return self._get_scenario_vector(db, scenario_horizon, level_period, unit, is_float32)
|
|
296
379
|
|
|
297
380
|
def get_data_value(
|
|
@@ -302,11 +385,23 @@ class LevelProfile(Base, ABC):
|
|
|
302
385
|
unit: str | None,
|
|
303
386
|
is_max_level: bool | None = None,
|
|
304
387
|
) -> float:
|
|
305
|
-
"""
|
|
388
|
+
"""
|
|
389
|
+
Evaluate LevelProfile to a scalar at the level period of the data dimension, and as an average over the scenario horizon.
|
|
390
|
+
|
|
391
|
+
Args:
|
|
392
|
+
db (QueryDB | Model): The database or model instance used to fetch the required data.
|
|
393
|
+
scenario_horizon (FixedFrequencyTimeIndex): TimeIndex of the scenario dimension to evaluate profiles.
|
|
394
|
+
level_period (SinglePeriodTimeIndex): TimeIndex of the data dimension to evaluate levels.
|
|
395
|
+
unit (str | None): The unit to convert the resulting values into (e.g., MW, GWh). If None,
|
|
396
|
+
the expression should be unitless.
|
|
397
|
+
is_max_level (bool | None, optional): Whether to evaluate the expression as a maximum level (with a zero_one profile)
|
|
398
|
+
or as an average level (with a mean_one profile). If None, the default format of the attribute is used.
|
|
399
|
+
|
|
400
|
+
"""
|
|
306
401
|
return self._get_data_value(db, scenario_horizon, level_period, unit, is_max_level)
|
|
307
402
|
|
|
308
403
|
def shift_intercept(self, value: float, unit: str | None) -> None:
|
|
309
|
-
"""Modify the intercept part of (level*profile + intercept) of an attribute by adding a constant value."""
|
|
404
|
+
"""Modify the intercept part of (level * profile + intercept) of an attribute by adding a constant value."""
|
|
310
405
|
expr = ensure_expr(
|
|
311
406
|
ConstantTimeVector(self._ensure_float(value), unit=unit, is_max_level=False),
|
|
312
407
|
is_level=True,
|
|
@@ -326,9 +421,10 @@ class LevelProfile(Base, ABC):
|
|
|
326
421
|
unit: str | None = None,
|
|
327
422
|
reference_period: ReferencePeriod | None = None,
|
|
328
423
|
is_max_level: bool | None = None,
|
|
329
|
-
use_profile: bool =
|
|
424
|
+
use_profile: bool = True, # TODO: Remove. Should always use profile. If has profile validate that it is equal to the profile of Level.
|
|
330
425
|
) -> None:
|
|
331
|
-
"""Modify the
|
|
426
|
+
"""Modify the level_shift part of (scale * (level + level_shift) * profile + intercept) of an attribute by adding a constant value."""
|
|
427
|
+
# TODO: Not allowed to shift if there is intercept?
|
|
332
428
|
self._check_type(value, (float, int))
|
|
333
429
|
self._check_type(unit, (str, type(None)))
|
|
334
430
|
self._check_type(reference_period, (ReferencePeriod, type(None)))
|
|
@@ -357,7 +453,8 @@ class LevelProfile(Base, ABC):
|
|
|
357
453
|
self._level_shift += expr
|
|
358
454
|
|
|
359
455
|
def scale(self, value: float | int) -> None:
|
|
360
|
-
"""Modify the
|
|
456
|
+
"""Modify the scale part of (scale * (level + level_shift) * profile + intercept) of an attribute by multiplying with a constant value."""
|
|
457
|
+
# TODO: Not allowed to scale if there is intercept?
|
|
361
458
|
expr = ensure_expr(
|
|
362
459
|
ConstantTimeVector(self._ensure_float(value), unit=None, is_max_level=False),
|
|
363
460
|
is_level=True,
|
|
@@ -400,17 +497,43 @@ class LevelProfile(Base, ABC):
|
|
|
400
497
|
profile=None,
|
|
401
498
|
)
|
|
402
499
|
|
|
500
|
+
def _ensure_compatible_level_profile_combo(self, level: Expr | None, profile: Expr | None) -> None:
|
|
501
|
+
"""Check that all profiles in leaf levels (in level) also exist in profile."""
|
|
502
|
+
if level is None or profile is None:
|
|
503
|
+
return
|
|
504
|
+
|
|
505
|
+
leaf_level_profiles = get_profile_exprs_from_leaf_levels(level)
|
|
506
|
+
leaf_profile_profiles = get_leaf_profiles(profile)
|
|
507
|
+
|
|
508
|
+
for p in leaf_level_profiles:
|
|
509
|
+
if p not in leaf_profile_profiles:
|
|
510
|
+
message = (
|
|
511
|
+
f"Incompatible level/profile combination because all profiles in leaf levels (in level) does not exist in profile. "
|
|
512
|
+
f"Profile expression {p} found in level {level} but not in profile."
|
|
513
|
+
)
|
|
514
|
+
raise ValueError(message)
|
|
515
|
+
|
|
403
516
|
def _check_level_expr(self, expr: Expr) -> None:
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
517
|
+
msg = f"{self} requires {expr} to be "
|
|
518
|
+
if expr.is_stock() != self._IS_STOCK:
|
|
519
|
+
raise ValueError(msg + f"is_stock={self._IS_STOCK}")
|
|
520
|
+
if expr.is_flow() != self._IS_FLOW:
|
|
521
|
+
raise ValueError(msg + f"is_flow={self._IS_STOCK}")
|
|
522
|
+
if expr.is_level() is False:
|
|
523
|
+
raise ValueError(msg + "is_level=True")
|
|
524
|
+
if expr.is_profile() is True:
|
|
525
|
+
raise ValueError(msg + "is_profile=False")
|
|
408
526
|
|
|
409
527
|
def _check_profile_expr(self, expr: Expr) -> None:
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
528
|
+
msg = f"{self} requires {expr} to be "
|
|
529
|
+
if expr.is_stock() is True:
|
|
530
|
+
raise ValueError(msg + "is_stock=False")
|
|
531
|
+
if expr.is_flow() is True:
|
|
532
|
+
raise ValueError(msg + "is_flow=False")
|
|
533
|
+
if expr.is_level() is True:
|
|
534
|
+
raise ValueError(msg + "is_level=False")
|
|
535
|
+
if expr.is_profile() is False:
|
|
536
|
+
raise ValueError(msg + "is_profile=True")
|
|
414
537
|
|
|
415
538
|
def _ensure_profile_expr(
|
|
416
539
|
self,
|
|
@@ -452,7 +575,8 @@ class LevelProfile(Base, ABC):
|
|
|
452
575
|
is_max_level = self._IS_MAX_AND_ZERO_ONE
|
|
453
576
|
|
|
454
577
|
self._check_type(level_expr, (Expr, type(None)))
|
|
455
|
-
|
|
578
|
+
if not isinstance(level_expr, Expr):
|
|
579
|
+
raise ValueError("Attribute level Expr is None. Have you called Solver.solve yet?")
|
|
456
580
|
|
|
457
581
|
level_value = get_level_value(
|
|
458
582
|
expr=level_expr,
|
|
@@ -497,7 +621,8 @@ class LevelProfile(Base, ABC):
|
|
|
497
621
|
level_expr = self.get_level()
|
|
498
622
|
|
|
499
623
|
self._check_type(level_expr, (Expr, type(None)))
|
|
500
|
-
|
|
624
|
+
if not isinstance(level_expr, Expr):
|
|
625
|
+
raise ValueError("Attribute level Expr is None. Have you called Solver.solve yet?")
|
|
501
626
|
|
|
502
627
|
level_value = get_level_value(
|
|
503
628
|
expr=level_expr,
|
|
@@ -593,121 +718,193 @@ class LevelProfile(Base, ABC):
|
|
|
593
718
|
|
|
594
719
|
|
|
595
720
|
class FlowVolume(LevelProfile):
|
|
596
|
-
"""
|
|
721
|
+
"""
|
|
722
|
+
Abstract class representing a flow volume attribute, indicating that the attribute is a flow variable.
|
|
723
|
+
|
|
724
|
+
Subclass of LevelProfile. See LevelProfile for details.
|
|
725
|
+
"""
|
|
597
726
|
|
|
598
727
|
_IS_FLOW = True
|
|
599
728
|
|
|
600
729
|
|
|
601
730
|
class Coefficient(LevelProfile):
|
|
602
|
-
"""
|
|
731
|
+
"""
|
|
732
|
+
Abstract class representing a coefficient attribute, used as a base class for various coefficient types.
|
|
733
|
+
|
|
734
|
+
Subclass of LevelProfile. See LevelProfile for details.
|
|
735
|
+
"""
|
|
603
736
|
|
|
604
737
|
pass
|
|
605
738
|
|
|
606
739
|
|
|
607
740
|
class ArrowCoefficient(Coefficient):
|
|
608
|
-
"""
|
|
741
|
+
"""
|
|
742
|
+
Abstract class representing an arrow coefficient attribute, used for efficiency, loss, and conversion coefficients.
|
|
743
|
+
|
|
744
|
+
Subclass of Coefficient < LevelProfile. See LevelProfile for details.
|
|
745
|
+
"""
|
|
609
746
|
|
|
610
747
|
pass
|
|
611
748
|
|
|
612
749
|
|
|
613
|
-
class
|
|
614
|
-
"""
|
|
750
|
+
class ShadowPrice(Coefficient):
|
|
751
|
+
"""
|
|
752
|
+
Abstract class representing a shadow price attribute, indicating that the attribute has a unit and might be negative.
|
|
753
|
+
|
|
754
|
+
Subclass of Coefficient < LevelProfile. See LevelProfile for details.
|
|
755
|
+
"""
|
|
615
756
|
|
|
616
757
|
_IS_UNITLESS = False
|
|
617
758
|
_IS_NOT_NEGATIVE = False
|
|
618
759
|
|
|
619
760
|
|
|
620
761
|
class ObjectiveCoefficient(Coefficient):
|
|
621
|
-
"""
|
|
762
|
+
"""
|
|
763
|
+
Abstract class representing an objective coefficient attribute, indicating cost or revenue coefficients in the objective function.
|
|
764
|
+
|
|
765
|
+
Subclass of Coefficient < LevelProfile. See LevelProfile for details.
|
|
766
|
+
"""
|
|
622
767
|
|
|
623
768
|
_IS_UNITLESS = False
|
|
624
769
|
_IS_NOT_NEGATIVE = False
|
|
625
770
|
|
|
626
771
|
|
|
627
|
-
#
|
|
772
|
+
# Concrete subclasses intended for final use
|
|
628
773
|
|
|
629
774
|
|
|
630
|
-
class Price(
|
|
631
|
-
"""
|
|
775
|
+
class Price(ShadowPrice):
|
|
776
|
+
"""
|
|
777
|
+
Concrete class representing a price attribute, indicating the price of a commodity at a specific node.
|
|
778
|
+
|
|
779
|
+
Subclass of ShadowPrice < Coefficient < LevelProfile. See LevelProfile for details.
|
|
780
|
+
"""
|
|
632
781
|
|
|
633
782
|
_IS_ABSTRACT = False
|
|
634
783
|
|
|
635
784
|
|
|
636
|
-
class WaterValue(
|
|
637
|
-
"""
|
|
785
|
+
class WaterValue(ShadowPrice):
|
|
786
|
+
"""
|
|
787
|
+
Concrete class representing a water value attribute, indicating the value of water in the system.
|
|
788
|
+
|
|
789
|
+
Subclass of ShadowPrice < Coefficient < LevelProfile. See LevelProfile for details.
|
|
790
|
+
"""
|
|
638
791
|
|
|
639
792
|
_IS_ABSTRACT = False
|
|
640
793
|
|
|
641
794
|
|
|
642
795
|
class Cost(ObjectiveCoefficient):
|
|
643
|
-
"""
|
|
796
|
+
"""
|
|
797
|
+
Concrete class representing a cost attribute, indicating cost coefficients in the objective function.
|
|
798
|
+
|
|
799
|
+
Subclass of ObjectiveCoefficient < Coefficient < LevelProfile. See LevelProfile for details.
|
|
800
|
+
"""
|
|
644
801
|
|
|
645
802
|
_IS_ABSTRACT = False
|
|
646
803
|
_IS_COST = True
|
|
647
804
|
|
|
648
805
|
|
|
649
806
|
class ReservePrice(ObjectiveCoefficient):
|
|
650
|
-
"""
|
|
807
|
+
"""
|
|
808
|
+
Concrete class representing a reserve price attribute, indicating revenue coefficients in the objective function.
|
|
809
|
+
|
|
810
|
+
Subclass of ObjectiveCoefficient < Coefficient < LevelProfile. See LevelProfile for details.
|
|
811
|
+
"""
|
|
651
812
|
|
|
652
813
|
_IS_ABSTRACT = False
|
|
653
814
|
_IS_COST = False
|
|
654
815
|
|
|
655
816
|
|
|
656
817
|
class Elasticity(Coefficient): # TODO: How do this work?
|
|
657
|
-
"""
|
|
818
|
+
"""
|
|
819
|
+
Concrete class representing an elasticity coefficient attribute, indicating a unitless coefficient.
|
|
820
|
+
|
|
821
|
+
Subclass of Coefficient < LevelProfile. See LevelProfile for details.
|
|
822
|
+
"""
|
|
658
823
|
|
|
659
824
|
_IS_ABSTRACT = False
|
|
660
825
|
_IS_UNITLESS = True
|
|
661
826
|
|
|
662
827
|
|
|
663
|
-
class Proportion(Coefficient):
|
|
664
|
-
"""
|
|
828
|
+
class Proportion(Coefficient):
|
|
829
|
+
"""
|
|
830
|
+
Concrete class representing a proportion coefficient attribute, indicating a unitless coefficient between 0 and 1.
|
|
831
|
+
|
|
832
|
+
Subclass of Coefficient < LevelProfile. See LevelProfile for details.
|
|
833
|
+
"""
|
|
665
834
|
|
|
666
835
|
_IS_ABSTRACT = False
|
|
667
836
|
_IS_UNITLESS = True
|
|
668
837
|
|
|
669
838
|
|
|
670
839
|
class Hours(Coefficient): # TODO: How do this work?
|
|
671
|
-
"""
|
|
840
|
+
"""
|
|
841
|
+
Concrete class representing an hours coefficient attribute, indicating a time-related coefficient.
|
|
842
|
+
|
|
843
|
+
Subclass of Coefficient < LevelProfile. See LevelProfile for details.
|
|
844
|
+
"""
|
|
672
845
|
|
|
673
846
|
_IS_ABSTRACT = False
|
|
674
847
|
|
|
675
848
|
|
|
676
849
|
class Efficiency(ArrowCoefficient):
|
|
677
|
-
"""
|
|
850
|
+
"""
|
|
851
|
+
Concrete class representing an efficiency coefficient attribute, indicating a unitless coefficient.
|
|
852
|
+
|
|
853
|
+
Subclass of ArrowCoefficient < Coefficient < LevelProfile. See LevelProfile for details.
|
|
854
|
+
"""
|
|
678
855
|
|
|
679
856
|
_IS_ABSTRACT = False
|
|
680
857
|
_IS_UNITLESS = True
|
|
681
858
|
|
|
682
859
|
|
|
683
|
-
class Loss(ArrowCoefficient):
|
|
684
|
-
"""
|
|
860
|
+
class Loss(ArrowCoefficient): # TODO: Make a loss for storage that is percentage per time
|
|
861
|
+
"""
|
|
862
|
+
Concrete class representing a loss coefficient attribute, indicating a unitless coefficient.
|
|
863
|
+
|
|
864
|
+
Subclass of ArrowCoefficient < Coefficient < LevelProfile. See LevelProfile for details.
|
|
865
|
+
"""
|
|
685
866
|
|
|
686
867
|
_IS_ABSTRACT = False
|
|
687
868
|
_IS_UNITLESS = True
|
|
688
869
|
|
|
689
870
|
|
|
690
871
|
class Conversion(ArrowCoefficient):
|
|
691
|
-
"""
|
|
872
|
+
"""
|
|
873
|
+
Concrete class representing a conversion coefficient attribute, used for conversion factors in the model.
|
|
874
|
+
|
|
875
|
+
Subclass of ArrowCoefficient < Coefficient < LevelProfile. See LevelProfile for details.
|
|
876
|
+
"""
|
|
692
877
|
|
|
693
878
|
_IS_ABSTRACT = False
|
|
694
879
|
|
|
695
880
|
|
|
696
881
|
class AvgFlowVolume(FlowVolume):
|
|
697
|
-
"""
|
|
882
|
+
"""
|
|
883
|
+
Concrete class representing an average flow volume attribute, indicating a flow variable with average values.
|
|
884
|
+
|
|
885
|
+
Subclass of FlowVolume < LevelProfile. See LevelProfile for details.
|
|
886
|
+
"""
|
|
698
887
|
|
|
699
888
|
_IS_ABSTRACT = False
|
|
700
889
|
|
|
701
890
|
|
|
702
891
|
class MaxFlowVolume(FlowVolume):
|
|
703
|
-
"""
|
|
892
|
+
"""
|
|
893
|
+
Concrete class representing a maximum flow volume attribute, indicating a flow variable with maximum values.
|
|
894
|
+
|
|
895
|
+
Subclass of FlowVolume < LevelProfile. See LevelProfile for details.
|
|
896
|
+
"""
|
|
704
897
|
|
|
705
898
|
_IS_ABSTRACT = False
|
|
706
899
|
_IS_MAX_AND_ZERO_ONE = True
|
|
707
900
|
|
|
708
901
|
|
|
709
902
|
class StockVolume(LevelProfile):
|
|
710
|
-
"""
|
|
903
|
+
"""
|
|
904
|
+
Concrete class representing a stock volume attribute, indicating a stock variable with maximum values.
|
|
905
|
+
|
|
906
|
+
Subclass of LevelProfile. See LevelProfile for details.
|
|
907
|
+
"""
|
|
711
908
|
|
|
712
909
|
_IS_ABSTRACT = False
|
|
713
910
|
_IS_STOCK = True
|
framcore/components/Component.py
CHANGED
|
@@ -8,7 +8,31 @@ from framcore.metadata import Meta
|
|
|
8
8
|
|
|
9
9
|
|
|
10
10
|
class Component(Base, ABC):
|
|
11
|
-
"""
|
|
11
|
+
"""
|
|
12
|
+
Components describe the main elements in the energy system. Can have additional Attributes and Metadata.
|
|
13
|
+
|
|
14
|
+
We have high-level and low-level Components. High-level Components, such as a HydroModule,
|
|
15
|
+
can be decomposed into low-level Components like Flows and Nodes. The high-level description lets
|
|
16
|
+
analysts work with recognizable domain objects, while the low-level descriptions enable generic algorithms
|
|
17
|
+
that minimize code duplication and simplify data manipulation.
|
|
18
|
+
|
|
19
|
+
Some energy market models like JulES, SpineOpt and PyPSA also have a generic description of the system,
|
|
20
|
+
so this two-tier system can be used to easier adapt the dataset to their required formats.
|
|
21
|
+
|
|
22
|
+
The method Component.get_simpler_components() is used to decompose high-level Components into low-level
|
|
23
|
+
Components. This can also be used together with the utility function get_supported_components() to transform
|
|
24
|
+
a set of Components into a set that only contains supported Component types.
|
|
25
|
+
|
|
26
|
+
Result attributes are initialized in the high-level Components. When they are transferred to low-level Components,
|
|
27
|
+
and the results are set by a model like JulES, the results will also appear in the high-level Components.
|
|
28
|
+
|
|
29
|
+
Nodes, Flows and Arrows are the main building blocks in FRAM's low-level representation of energy systems.
|
|
30
|
+
Node represent a point where a commodity can possibly be traded, stored or pass through.
|
|
31
|
+
Movement between Nodes is represented by Flows and Arrows. Flows represent a commodity flow,
|
|
32
|
+
and can have Arrows that each describe contribution of the Flow into a Node.
|
|
33
|
+
The Arrows have direction to determine input or output, and parameters for the contribution of the
|
|
34
|
+
Flow to the Node (conversion, efficiency and loss).
|
|
35
|
+
"""
|
|
12
36
|
|
|
13
37
|
def __init__(self) -> None:
|
|
14
38
|
"""Set mandatory private variables."""
|
|
@@ -46,7 +70,7 @@ class Component(Base, ABC):
|
|
|
46
70
|
"""
|
|
47
71
|
self._check_type(base_name, str)
|
|
48
72
|
components = self._get_simpler_components(base_name)
|
|
49
|
-
assert base_name not in components, f"base_name: {base_name}\ncomponent: {self}"
|
|
73
|
+
assert base_name not in components, f"base_name: {base_name} should not be in \ncomponent: {self}"
|
|
50
74
|
components: dict[str, Component]
|
|
51
75
|
self._check_type(components, dict)
|
|
52
76
|
for name, c in components.items():
|
|
@@ -85,7 +109,7 @@ class Component(Base, ABC):
|
|
|
85
109
|
return parents[-1]
|
|
86
110
|
|
|
87
111
|
def replace_node(self, old: str, new: str) -> None:
|
|
88
|
-
"""Replace old
|
|
112
|
+
"""Replace old Node with new. Not error if no match."""
|
|
89
113
|
self._check_type(old, str)
|
|
90
114
|
self._check_type(new, str)
|
|
91
115
|
self._replace_node(old, new)
|
framcore/components/Demand.py
CHANGED
|
@@ -18,7 +18,21 @@ class Demand(Component):
|
|
|
18
18
|
temperature_profile: Expr | str | TimeVector | None = None,
|
|
19
19
|
consumption: AvgFlowVolume | None = None,
|
|
20
20
|
) -> None:
|
|
21
|
-
"""
|
|
21
|
+
"""
|
|
22
|
+
Initialize the Demand class.
|
|
23
|
+
|
|
24
|
+
Args:
|
|
25
|
+
node (str): Node which this Demand consumes power on.
|
|
26
|
+
capacity (FlowVolume | None, optional): Maximum consumption capacity. Defaults to None.
|
|
27
|
+
reserve_price (ReservePrice | None, optional): Price in node at which the Demand will stop consumption. Defaults to None.
|
|
28
|
+
elastic_demand (ElasticDemand | None, optional): Describe changes in consumption based on commodity price in node. Defaults to None.
|
|
29
|
+
temperature_profile (Expr | str | TimeVector | None, optional): Describe changes in consumption based on temperatures. Defaults to None.
|
|
30
|
+
consumption (AvgFlowVolume | None, optional): Actual calculated consumption. Defaults to None.
|
|
31
|
+
|
|
32
|
+
Raises:
|
|
33
|
+
ValueError: When both reserve_price and elastic_demand is passed as arguments. This is ambiguous.
|
|
34
|
+
|
|
35
|
+
"""
|
|
22
36
|
super().__init__()
|
|
23
37
|
self._check_type(node, str)
|
|
24
38
|
self._check_type(capacity, (FlowVolume, type(None)))
|
|
@@ -62,9 +76,9 @@ class Demand(Component):
|
|
|
62
76
|
"""Get the reserve price level of the demand component."""
|
|
63
77
|
return self._reserve_price
|
|
64
78
|
|
|
65
|
-
def
|
|
79
|
+
def set_reserve_price(self, reserve_price: ReservePrice | None) -> None:
|
|
66
80
|
"""Set the reserve price level of the demand component."""
|
|
67
|
-
self._check_type(reserve_price, (
|
|
81
|
+
self._check_type(reserve_price, (ReservePrice, type(None)))
|
|
68
82
|
if self._elastic_demand and reserve_price:
|
|
69
83
|
message = "Cannot set reserve_price when elastic_demand is not None."
|
|
70
84
|
raise ValueError(message)
|
|
@@ -78,7 +92,7 @@ class Demand(Component):
|
|
|
78
92
|
"""Set the elastic demand of the demand component."""
|
|
79
93
|
self._check_type(elastic_demand, (ElasticDemand, type(None)))
|
|
80
94
|
if self._reserve_price is not None and elastic_demand is not None:
|
|
81
|
-
message = "Cannot set elastic_demand when
|
|
95
|
+
message = "Cannot set elastic_demand when reserve_price is not None."
|
|
82
96
|
raise ValueError(message)
|
|
83
97
|
self._elastic_demand = elastic_demand
|
|
84
98
|
|
framcore/components/Flow.py
CHANGED
|
@@ -12,7 +12,15 @@ if TYPE_CHECKING:
|
|
|
12
12
|
|
|
13
13
|
|
|
14
14
|
class Flow(Component):
|
|
15
|
-
"""
|
|
15
|
+
"""
|
|
16
|
+
Represents a commodity flow in or out of one or more nodes. Can have Attributes and Metadata.
|
|
17
|
+
|
|
18
|
+
Main attributes are arrows, main_node, max_capacity, min_capacity, startupcost and if it is exogenous.
|
|
19
|
+
|
|
20
|
+
Arrows describes contribution of a Flow into a Node. Has direction to determine input or output,
|
|
21
|
+
and parameters for the contribution of the Flow to the Node (conversion, efficiency, loss).
|
|
22
|
+
Nodes, Flows and Arrows are the main building blocks in FRAM's low-level representation of energy systems.
|
|
23
|
+
"""
|
|
16
24
|
|
|
17
25
|
def __init__(
|
|
18
26
|
self,
|
|
@@ -24,7 +32,21 @@ class Flow(Component):
|
|
|
24
32
|
arrow_volumes: dict[Arrow, AvgFlowVolume] | None = None,
|
|
25
33
|
is_exogenous: bool = False,
|
|
26
34
|
) -> None:
|
|
27
|
-
"""
|
|
35
|
+
"""
|
|
36
|
+
Initialize Flow with main node, capacity, and startup cost.
|
|
37
|
+
|
|
38
|
+
Args:
|
|
39
|
+
main_node (str): Node which the Flow is primarily associated with.
|
|
40
|
+
max_capacity (FlowVolume | None, optional): Maximum capacity of the Flow. Defaults to None.
|
|
41
|
+
min_capacity (FlowVolume | None, optional): Minimum capacity of the Flow. Defaults to None.
|
|
42
|
+
startupcost (StartUpCost | None, optional): Costs associated with starting up this Flow. Defaults to None.
|
|
43
|
+
volume (AvgFlowVolume | None, optional): The actual volume carried by this Flow at a given moment. Defaults to None.
|
|
44
|
+
arrow_volumes (dict[Arrow, AvgFlowVolume] | None, optional): Possibility to store a version of volume for each Arrow. Can account for conversion,
|
|
45
|
+
efficiency and loss to represent the result for different commodities and units. Defaults to None.
|
|
46
|
+
is_exogenous (bool, optional): Flag denoting if a Solver should calculate the volumes associated with this flow or use its predefined volume.
|
|
47
|
+
Defaults to False.
|
|
48
|
+
|
|
49
|
+
"""
|
|
28
50
|
super().__init__()
|
|
29
51
|
self._check_type(main_node, str)
|
|
30
52
|
self._check_type(max_capacity, (FlowVolume, type(None)))
|
|
@@ -54,7 +76,7 @@ class Flow(Component):
|
|
|
54
76
|
|
|
55
77
|
def set_exogenous(self) -> None:
|
|
56
78
|
"""
|
|
57
|
-
Treat flow as
|
|
79
|
+
Treat flow as fixed variable.
|
|
58
80
|
|
|
59
81
|
Use volume if it exists.
|
|
60
82
|
If no volume, then try to use
|
|
@@ -132,7 +154,7 @@ class Flow(Component):
|
|
|
132
154
|
|
|
133
155
|
def add_loaders(self, loaders: set[Loader]) -> None:
|
|
134
156
|
"""Add loaders stored in attributes to loaders."""
|
|
135
|
-
from framcore.utils import add_loaders_if
|
|
157
|
+
from framcore.utils import add_loaders_if
|
|
136
158
|
|
|
137
159
|
add_loaders_if(loaders, self.get_volume())
|
|
138
160
|
add_loaders_if(loaders, self.get_max_capacity())
|