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
|
@@ -3,7 +3,25 @@ from framcore.components import Component, Flow, Node
|
|
|
3
3
|
|
|
4
4
|
|
|
5
5
|
class HydroModule(Component):
|
|
6
|
-
"""
|
|
6
|
+
"""
|
|
7
|
+
HydroModules represents a physical element in a river system, with its topology and other attributes.
|
|
8
|
+
|
|
9
|
+
The hydromodule can contain a HydroReservoir, HydroGenerator, HydroPump, HydroBypass and local inflow, aswell as the topological attributes release_to
|
|
10
|
+
and spill_to:
|
|
11
|
+
|
|
12
|
+
- HydroGenerator uses the release pathway of the HydroModule to generate power, while HydroPump has its own water way that consumes
|
|
13
|
+
power. Both HydroGenerator and HydroPump connects to power nodes.
|
|
14
|
+
- HydroBypass also have attributes that define the topology of the river system.
|
|
15
|
+
- HydroReservoir represents the water storage of the HydroModule.
|
|
16
|
+
- The hydraulic_coupling attribute is used to identify which HydroModules have hydraulic coupled reservoirs.
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
Results for the release volume, spill volume and the water value are stored directly in the HydroModule, while the production, pumping,
|
|
20
|
+
reservoir volume and bypass volume are stored in the attributes.
|
|
21
|
+
|
|
22
|
+
HydroModule is compatible with HydroAggregator for aggregation of multiple HydroModules into one.
|
|
23
|
+
|
|
24
|
+
"""
|
|
7
25
|
|
|
8
26
|
# We add this to module name to get corresponding node name
|
|
9
27
|
_NODE_NAME_POSTFIX = "_node"
|
|
@@ -24,7 +42,28 @@ class HydroModule(Component):
|
|
|
24
42
|
release_volume: AvgFlowVolume | None = None,
|
|
25
43
|
spill_volume: AvgFlowVolume | None = None,
|
|
26
44
|
) -> None:
|
|
27
|
-
"""
|
|
45
|
+
"""
|
|
46
|
+
Initialize the HydroModule with its parameters.
|
|
47
|
+
|
|
48
|
+
Args:
|
|
49
|
+
release_to (str | None, optional): Reference to another HydroModule which recieves the water releases through the main release. Defaults to None.
|
|
50
|
+
release_capacity (FlowVolume | None): Amount of water which can be released via main release at a given moment. Defaults to None.
|
|
51
|
+
generator (HydroGenerator | None, optional): Represents generation of electricity from the movement of water through the Modules main release
|
|
52
|
+
pathway. Defaults to None.
|
|
53
|
+
pump (HydroPump | None): Pump associated with this Module. Can move water to another using power. Defaults to None.
|
|
54
|
+
inflow (AvgFlowVolume | None, optional): The local inflow of the HydroModule. Defaults to None.
|
|
55
|
+
reservoir (HydroReservoir | None, optional): The Modules water storage. Defaults to None.
|
|
56
|
+
hydraulic_coupling (int): Number other than 0 if the HydroModules reservoir is hydraulic coupled to another reservoir. Defaults to 0.
|
|
57
|
+
TODO: Replace with HydraulicCoupling class
|
|
58
|
+
bypass (HydroBypass | None, optional): Bypass water way. Defaults to None.
|
|
59
|
+
spill_to (str | None): Reference to another Module recieving this ones spill volume. Defaults to None.
|
|
60
|
+
commodity (str, optional): Commodity of the hydro node. Defaults to "Hydro".
|
|
61
|
+
water_value (WaterValue | None, optional): Water value of the reservoir in currency per water volume. Defaults to None.
|
|
62
|
+
TODO: Allow water values with multiple demimensions?
|
|
63
|
+
release_volume (AvgFlowVolume | None, optional): Volume of water released via main waterway. Defaults to None.
|
|
64
|
+
spill_volume (AvgFlowVolume | None, optional): Volume of water spilled. Defaults to None.
|
|
65
|
+
|
|
66
|
+
"""
|
|
28
67
|
super().__init__()
|
|
29
68
|
self._check_type(release_to, (str, type(None)))
|
|
30
69
|
self._check_type(release_capacity, (FlowVolume, type(None)))
|
|
@@ -187,6 +226,7 @@ class HydroModule(Component):
|
|
|
187
226
|
arrow_volumes=None,
|
|
188
227
|
is_exogenous=False,
|
|
189
228
|
)
|
|
229
|
+
|
|
190
230
|
arrow_volumes = flow.get_arrow_volumes()
|
|
191
231
|
|
|
192
232
|
outgoing_arrow = Arrow(
|
|
@@ -194,6 +234,7 @@ class HydroModule(Component):
|
|
|
194
234
|
is_ingoing=False,
|
|
195
235
|
conversion=Conversion(value=1),
|
|
196
236
|
)
|
|
237
|
+
|
|
197
238
|
flow.add_arrow(outgoing_arrow)
|
|
198
239
|
|
|
199
240
|
if self._release_to:
|
|
@@ -209,7 +250,7 @@ class HydroModule(Component):
|
|
|
209
250
|
production_arrow = Arrow(
|
|
210
251
|
node=self._generator.get_power_node(),
|
|
211
252
|
is_ingoing=True,
|
|
212
|
-
conversion=self._generator.
|
|
253
|
+
conversion=self._generator.get_energy_equivalent(),
|
|
213
254
|
)
|
|
214
255
|
flow.add_arrow(production_arrow)
|
|
215
256
|
arrow_volumes[production_arrow] = self._generator.get_production()
|
|
@@ -322,7 +363,7 @@ class HydroModule(Component):
|
|
|
322
363
|
pump_arrow = Arrow(
|
|
323
364
|
node=self._pump.get_power_node(),
|
|
324
365
|
is_ingoing=False,
|
|
325
|
-
conversion=self._pump.
|
|
366
|
+
conversion=self._pump.get_energy_equivalent(),
|
|
326
367
|
)
|
|
327
368
|
flow.add_arrow(pump_arrow)
|
|
328
369
|
arrow_volumes[pump_arrow] = self._pump.get_power_consumption()
|
framcore/components/Node.py
CHANGED
|
@@ -2,7 +2,7 @@ from __future__ import annotations
|
|
|
2
2
|
|
|
3
3
|
from typing import TYPE_CHECKING
|
|
4
4
|
|
|
5
|
-
from framcore.attributes import Price,
|
|
5
|
+
from framcore.attributes import Price, ShadowPrice, Storage
|
|
6
6
|
from framcore.components import Component
|
|
7
7
|
|
|
8
8
|
if TYPE_CHECKING:
|
|
@@ -10,20 +10,43 @@ if TYPE_CHECKING:
|
|
|
10
10
|
|
|
11
11
|
|
|
12
12
|
class Node(Component):
|
|
13
|
-
"""
|
|
13
|
+
"""
|
|
14
|
+
Represents a point in the energy system where a commodity can possibly be traded, stored or pass through.
|
|
15
|
+
|
|
16
|
+
A node is characterized by the commodity it handles, its price, and optionally storage capabilities. If the
|
|
17
|
+
node is exogenous, the commodity can be bought and sold at a fixed price determined by the user.
|
|
18
|
+
If the node is endogenous, the price is determined by the market dynamics at the Node.
|
|
19
|
+
|
|
20
|
+
Nodes, Flows and Arrows are the main building blocks in FRAM's low-level representation of energy systems.
|
|
21
|
+
Movement between Nodes is represented by Flows and Arrows. Flows represent a commodity flow,
|
|
22
|
+
and can have Arrows that each describe contribution of the Flow into a Node.
|
|
23
|
+
The Arrows have direction to determine input or output,
|
|
24
|
+
and parameters for the contribution of the Flow to the Node (conversion, efficiency and loss).
|
|
25
|
+
|
|
26
|
+
"""
|
|
14
27
|
|
|
15
28
|
def __init__(
|
|
16
29
|
self,
|
|
17
30
|
commodity: str,
|
|
18
31
|
is_exogenous: bool = False, # TODO
|
|
19
|
-
price:
|
|
32
|
+
price: ShadowPrice | None = None,
|
|
20
33
|
storage: Storage | None = None,
|
|
21
34
|
) -> None:
|
|
22
|
-
"""
|
|
35
|
+
"""
|
|
36
|
+
Initialize the Node class.
|
|
37
|
+
|
|
38
|
+
Args:
|
|
39
|
+
commodity (str): Commodity at the Node. Power/electricity, gas, heat, etc.
|
|
40
|
+
is_exogenous (bool, optional): Flag used to signal Solvers whether they should simulate the node endogenously or use the pre-set price.
|
|
41
|
+
Defaults to False.
|
|
42
|
+
price (ShadowPrice | None): Actual, calculated price of Commodity in this Node for each moment of simulation. Defaulta to None.
|
|
43
|
+
storage (Storage | None, optional): The amount of the Commodity stored on this Node. Defaults to None.
|
|
44
|
+
|
|
45
|
+
"""
|
|
23
46
|
super().__init__()
|
|
24
47
|
self._check_type(commodity, str)
|
|
25
48
|
self._check_type(is_exogenous, bool)
|
|
26
|
-
self._check_type(price, (
|
|
49
|
+
self._check_type(price, (ShadowPrice, type(None)))
|
|
27
50
|
self._check_type(storage, (Storage, type(None)))
|
|
28
51
|
|
|
29
52
|
self._commodity = commodity
|
|
@@ -37,12 +60,12 @@ class Node(Component):
|
|
|
37
60
|
self._price: Price = price
|
|
38
61
|
|
|
39
62
|
def set_exogenous(self) -> None:
|
|
40
|
-
"""Set
|
|
63
|
+
"""Set the Node to be exogenous."""
|
|
41
64
|
self._check_type(self._is_exogenous, bool)
|
|
42
65
|
self._is_exogenous = True
|
|
43
66
|
|
|
44
67
|
def set_endogenous(self) -> None:
|
|
45
|
-
"""Set
|
|
68
|
+
"""Set the Node to be endogenous."""
|
|
46
69
|
self._check_type(self._is_exogenous, bool)
|
|
47
70
|
self._is_exogenous = False
|
|
48
71
|
|
|
@@ -50,7 +73,7 @@ class Node(Component):
|
|
|
50
73
|
"""Return True if Node is exogenous (i.e. has fixed prices determined outside the model) else False."""
|
|
51
74
|
return self._is_exogenous
|
|
52
75
|
|
|
53
|
-
def get_price(self) ->
|
|
76
|
+
def get_price(self) -> ShadowPrice:
|
|
54
77
|
"""Return price."""
|
|
55
78
|
return self._price
|
|
56
79
|
|
|
@@ -64,7 +87,7 @@ class Node(Component):
|
|
|
64
87
|
|
|
65
88
|
def add_loaders(self, loaders: set[Loader]) -> None:
|
|
66
89
|
"""Add loaders stored in attributes to loaders."""
|
|
67
|
-
from framcore.utils import add_loaders_if
|
|
90
|
+
from framcore.utils import add_loaders_if
|
|
68
91
|
|
|
69
92
|
add_loaders_if(loaders, self.get_price())
|
|
70
93
|
add_loaders_if(loaders, self.get_storage())
|
framcore/components/Thermal.py
CHANGED
|
@@ -2,10 +2,6 @@ from framcore.attributes import Arrow, AvgFlowVolume, Conversion, Cost, Efficien
|
|
|
2
2
|
from framcore.components import Component, Flow
|
|
3
3
|
from framcore.components._PowerPlant import _PowerPlant
|
|
4
4
|
|
|
5
|
-
# TODO
|
|
6
|
-
# refactor to use _ensure_flow and _ensure_coeff by using check_type, and possibly other methods
|
|
7
|
-
# add exception to replace_nodes (see implementation in Demand)
|
|
8
|
-
|
|
9
5
|
|
|
10
6
|
class Thermal(_PowerPlant):
|
|
11
7
|
"""
|
|
@@ -13,12 +9,14 @@ class Thermal(_PowerPlant):
|
|
|
13
9
|
|
|
14
10
|
This class models a thermal power plant with attributes inherited from PowerPlant.
|
|
15
11
|
Additionally, it includes specific attributes such as:
|
|
12
|
+
|
|
16
13
|
- fuel node
|
|
17
14
|
- efficiency
|
|
18
15
|
- emission node
|
|
19
16
|
- emission coefficient
|
|
20
17
|
- startup costs
|
|
21
18
|
|
|
19
|
+
|
|
22
20
|
This class is compatible with ThermalAggregator.
|
|
23
21
|
"""
|
|
24
22
|
|
|
@@ -27,10 +25,10 @@ class Thermal(_PowerPlant):
|
|
|
27
25
|
power_node: str,
|
|
28
26
|
fuel_node: str,
|
|
29
27
|
efficiency: Efficiency,
|
|
28
|
+
max_capacity: FlowVolume,
|
|
30
29
|
emission_node: str | None = None,
|
|
31
30
|
emission_coefficient: Conversion | None = None,
|
|
32
31
|
startupcost: StartUpCost | None = None,
|
|
33
|
-
max_capacity: FlowVolume | None = None,
|
|
34
32
|
min_capacity: FlowVolume | None = None,
|
|
35
33
|
voc: Cost | None = None,
|
|
36
34
|
production: AvgFlowVolume | None = None,
|
|
@@ -46,9 +44,9 @@ class Thermal(_PowerPlant):
|
|
|
46
44
|
efficiency (Efficiency): Efficiency of the plant.
|
|
47
45
|
emission_node (str | None, optional): Emission node.
|
|
48
46
|
emission_coefficient (Conversion | None, optional): Emission coefficient.
|
|
49
|
-
startupcost (StartUpCost | None, optional):
|
|
50
|
-
max_capacity (FlowVolume | None, optional): Maximum capacity.
|
|
51
|
-
min_capacity (FlowVolume | None, optional): Minimum capacity.
|
|
47
|
+
startupcost (StartUpCost | None, optional): Cost associated with starting up the Plant.
|
|
48
|
+
max_capacity (FlowVolume | None, optional): Maximum production capacity.
|
|
49
|
+
min_capacity (FlowVolume | None, optional): Minimum production capacity.
|
|
52
50
|
voc (Cost | None, optional): Variable operating cost.
|
|
53
51
|
production (AvgFlowVolume | None, optional): Production volume.
|
|
54
52
|
fuel_demand (AvgFlowVolume | None, optional): Fuel demand.
|
|
@@ -149,6 +147,12 @@ class Thermal(_PowerPlant):
|
|
|
149
147
|
return {base_name + "_Flow": self._create_flow()}
|
|
150
148
|
|
|
151
149
|
def _replace_node(self, old: str, new: str) -> None:
|
|
150
|
+
existing_nodes = [self._power_node, self._fuel_node]
|
|
151
|
+
existing_nodes = existing_nodes if self._emission_node is None else [*existing_nodes, self._emission_node]
|
|
152
|
+
if old not in existing_nodes:
|
|
153
|
+
message = f"{old} not found in {self}. Expected one of the existing nodes {existing_nodes}."
|
|
154
|
+
raise ValueError(message)
|
|
155
|
+
|
|
152
156
|
if self._power_node == old:
|
|
153
157
|
self._power_node = new
|
|
154
158
|
if self._fuel_node == old:
|
|
@@ -6,7 +6,7 @@ from framcore.components import Component, Flow
|
|
|
6
6
|
|
|
7
7
|
class Transmission(Component):
|
|
8
8
|
"""
|
|
9
|
-
Transmission component representing a transmission line. Subclass of Component.
|
|
9
|
+
Transmission component representing a one directional transmission line. Subclass of Component.
|
|
10
10
|
|
|
11
11
|
An object of this class represents one transmission line where power flows one direction (the other direction is
|
|
12
12
|
represented by another Transmission object). However, the actual measured power being sent can be higher than the
|
|
@@ -28,7 +28,22 @@ class Transmission(Component):
|
|
|
28
28
|
ingoing_volume: AvgFlowVolume | None = None,
|
|
29
29
|
outgoing_volume: AvgFlowVolume | None = None,
|
|
30
30
|
) -> None:
|
|
31
|
-
"""
|
|
31
|
+
"""
|
|
32
|
+
Initialize object of the Transmission class. Perform type checks and convert arguments to expressions.
|
|
33
|
+
|
|
34
|
+
Args:
|
|
35
|
+
from_node (str): Node which power is transported from.
|
|
36
|
+
to_node (str): Destination Node.
|
|
37
|
+
max_capacity (FlowVolume, optional): Maximum transmission capacity. Defaults to None.
|
|
38
|
+
min_capacity (FlowVolume | None, optional): Minimum transmission capacity. Defaults to None.
|
|
39
|
+
loss (Loss | None, optional): Amount of power lost while transmitting. Defaults to None.
|
|
40
|
+
tariff (Cost | None, optional): Costs associated with operating this transmission line. Defaults to None.
|
|
41
|
+
ramp_up (Proportion | None, optional): Max upwards change in transmission per time. Defaults to None.
|
|
42
|
+
ramp_down (Proportion | None, optional): Max downwards change in transmission per time. Defaults to None.
|
|
43
|
+
ingoing_volume (AvgFlowVolume | None, optional): Volume of power recieved by to_node. Defaults to None.
|
|
44
|
+
outgoing_volume (AvgFlowVolume | None, optional): Volume of power sent by from_node. Defaults to None.
|
|
45
|
+
|
|
46
|
+
"""
|
|
32
47
|
super().__init__()
|
|
33
48
|
|
|
34
49
|
self._check_type(from_node, str)
|
|
@@ -7,13 +7,7 @@ class _WindSolar(_PowerPlant):
|
|
|
7
7
|
"""
|
|
8
8
|
Wind and Solar class component representing a wind and solar power plant. Subclass of PowerPlant.
|
|
9
9
|
|
|
10
|
-
This class models a wind or
|
|
11
|
-
|
|
12
|
-
Capacity can be provided directly or as parts (level or profile). Max and min capacity profiles are set
|
|
13
|
-
to be equal as the capacity_profile for these technologytypes since it does not have a max or min.
|
|
14
|
-
|
|
15
|
-
The functions _get_fingerprints, _get_nodes og _get_flow are defines in this
|
|
16
|
-
subclass since other subclases are dependent on fuel and emission nodes.
|
|
10
|
+
This class models a wind or solar power plant with various attributes inherited from the parent class PowerPlant.
|
|
17
11
|
"""
|
|
18
12
|
|
|
19
13
|
def __init__(
|
|
@@ -23,7 +17,16 @@ class _WindSolar(_PowerPlant):
|
|
|
23
17
|
voc: Cost | None = None,
|
|
24
18
|
production: AvgFlowVolume | None = None,
|
|
25
19
|
) -> None:
|
|
26
|
-
"""
|
|
20
|
+
"""
|
|
21
|
+
Initialize the Wind and Solar class.
|
|
22
|
+
|
|
23
|
+
Args:
|
|
24
|
+
power_node (str): Reference to a power Node to produce to.
|
|
25
|
+
max_capacity (FlowVolume): Maximum capacity.
|
|
26
|
+
voc (Cost | None, optional): Variable operational costs. Defaults to None.
|
|
27
|
+
production (AvgFlowVolume | None, optional): Actual production. Defaults to None.
|
|
28
|
+
|
|
29
|
+
"""
|
|
27
30
|
super().__init__(
|
|
28
31
|
power_node=power_node,
|
|
29
32
|
max_capacity=max_capacity,
|
|
@@ -56,12 +59,24 @@ class _WindSolar(_PowerPlant):
|
|
|
56
59
|
|
|
57
60
|
|
|
58
61
|
class Wind(_WindSolar):
|
|
59
|
-
"""
|
|
62
|
+
"""
|
|
63
|
+
Wind power component.
|
|
64
|
+
|
|
65
|
+
Has attributes for power node, capacity, variable operation cost, and production.
|
|
66
|
+
|
|
67
|
+
Compatible with WindSolarAggregator.
|
|
68
|
+
"""
|
|
60
69
|
|
|
61
70
|
pass
|
|
62
71
|
|
|
63
72
|
|
|
64
73
|
class Solar(_WindSolar):
|
|
65
|
-
"""
|
|
74
|
+
"""
|
|
75
|
+
Solar power component.
|
|
76
|
+
|
|
77
|
+
Has attributes for power node, capacity, variable operation cost, and production.
|
|
78
|
+
|
|
79
|
+
Compatible with WindSolarAggregator.
|
|
80
|
+
"""
|
|
66
81
|
|
|
67
82
|
pass
|
framcore/curves/LoadedCurve.py
CHANGED
|
@@ -16,15 +16,6 @@ class LoadedCurve(Curve):
|
|
|
16
16
|
"""
|
|
17
17
|
Represents a curve loaded from a CurveLoader.
|
|
18
18
|
|
|
19
|
-
Attributes
|
|
20
|
-
----------
|
|
21
|
-
_curve_id : str
|
|
22
|
-
Identifier for the curve.
|
|
23
|
-
_loader : CurveLoader
|
|
24
|
-
Loader instance used to retrieve curve data.
|
|
25
|
-
_reference_period : Any
|
|
26
|
-
Reference period for the curve (currently not set).
|
|
27
|
-
|
|
28
19
|
Methods
|
|
29
20
|
-------
|
|
30
21
|
get_unique_name()
|
framcore/expressions/Expr.py
CHANGED
|
@@ -4,6 +4,7 @@ from copy import copy
|
|
|
4
4
|
from typing import TYPE_CHECKING
|
|
5
5
|
|
|
6
6
|
from framcore import Base
|
|
7
|
+
from framcore.curves import Curve, LoadedCurve
|
|
7
8
|
from framcore.fingerprints import Fingerprint, FingerprintRef
|
|
8
9
|
from framcore.timevectors import ConstantTimeVector, TimeVector
|
|
9
10
|
|
|
@@ -13,11 +14,44 @@ if TYPE_CHECKING:
|
|
|
13
14
|
|
|
14
15
|
# TODO: Add Expr.add_many to support faster aggregation expressions.
|
|
15
16
|
class Expr(Base):
|
|
16
|
-
"""
|
|
17
|
+
"""
|
|
18
|
+
Mathematical expression with TimeVectors and Curves to represent Levels and Profiles in LevelProfiles.
|
|
19
|
+
|
|
20
|
+
The simplest Expr is a single TimeVector, while a more complicated expression could be a weighted average of several TimeVectors or Expressions.
|
|
21
|
+
Expr can also have string references to Expr, TimeVector or Curve in a database (often Model).
|
|
22
|
+
|
|
23
|
+
Expr are classified as Stock, Flow or None of them. See https://en.wikipedia.org/wiki/Stock_and_flow. In FRAM we only support Flow data as a rate of change.
|
|
24
|
+
So, for example, a production timeseries has to be in MW, and not in MWh. Converting between the two versions of Flow would add another
|
|
25
|
+
level of complexity both in Expr and in TimeVector operations.
|
|
26
|
+
|
|
27
|
+
Expr are also classified as Level, Profile or none of them. This classification, together with Stock or Flow,
|
|
28
|
+
is used to check if the built Expr are legal operations.
|
|
29
|
+
- Expr that are Level can contain its connected Profile Expr. This is used in the queries to evaluate Levels according to their ReferencePeriod, and
|
|
30
|
+
convert between Level formats (max level or average level, see LevelProfile for more details).
|
|
31
|
+
|
|
32
|
+
Calculations using Expr are evaluated lazily, reducing unnecessary numerical operations during data manipulation.
|
|
33
|
+
Computations involving values and units occur only when the Expr is queried.
|
|
34
|
+
|
|
35
|
+
We only support calculations using +, -, *, and / in Expr, and we have no plans to change this.
|
|
36
|
+
Expanding beyond these would turn Expr into a complex programming language rather than keeping it as a simple
|
|
37
|
+
and efficient system for common time-series calculations. More advanced operations are still possible through eager evaluation, so this is not a limitation.
|
|
38
|
+
It simply distributes responsibilities across system components in a way that is practical from a maintenance perspective.
|
|
39
|
+
|
|
40
|
+
We use SymPy to support unit conversions. Already computed unit conversion factors are cached to minimize redundant calculations.
|
|
41
|
+
|
|
42
|
+
At the moment we support these queries for Expr (see Aggregators for more about how they are used):
|
|
43
|
+
- get_level_value(expr, db, unit, data_dim, scen_dim, is_max)
|
|
44
|
+
- Supports all expressions. Will evaluate level Exprs at data_dim (with reference period of scen_dim),
|
|
45
|
+
and profile Exprs as an average over scen_dim (both as constants).
|
|
46
|
+
- Has optimized fastpath methods for sums, products and aggregations. The rest uses a fallback method with SymPy.
|
|
47
|
+
- get_profile_vector(expr, db, data_dim, scen_dim, is_zero_one, is_float32)
|
|
48
|
+
- Supports expr = sum(weight[i] * profile[i]) where weight[i] is a unitless constant Expr with value >= 0, and profile[i] is a unitless profile Expr.
|
|
49
|
+
|
|
50
|
+
"""
|
|
17
51
|
|
|
18
52
|
def __init__(
|
|
19
53
|
self,
|
|
20
|
-
src: str | TimeVector | None = None,
|
|
54
|
+
src: str | Curve | TimeVector | None = None,
|
|
21
55
|
is_stock: bool = False,
|
|
22
56
|
is_flow: bool = False,
|
|
23
57
|
is_profile: bool = False,
|
|
@@ -26,19 +60,33 @@ class Expr(Base):
|
|
|
26
60
|
operations: tuple[str, list[Expr]] | None = None,
|
|
27
61
|
) -> None:
|
|
28
62
|
"""
|
|
29
|
-
Create new (immutable)
|
|
63
|
+
Create new (immutable) Expression.
|
|
30
64
|
|
|
31
65
|
Args:
|
|
32
|
-
src (str | None, optional):
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
66
|
+
src (str | Curve | TimeVector | None, optional): Source of the values to be used in the Expression. Either a Curve or TimeVector object,
|
|
67
|
+
or a reference to one of them. Defaults to None.
|
|
68
|
+
is_stock (bool, optional): Flag to signify if the Expr represents a stock type variable. Defaults to False.
|
|
69
|
+
is_flow (bool, optional): Flag to signify if the Expr represents a flow type variable. Defaults to False.
|
|
70
|
+
is_profile (bool, optional): Flag to signify if the Expr represents a profile. Defaults to False.
|
|
71
|
+
is_level (bool, optional): Flag to signify if the Expr represents a level. Defaults to False.
|
|
72
|
+
profile (Expr | None, optional): Expr that are Level can contain its connected Profile Expr. This is used in the queries to evaluate
|
|
73
|
+
Levels according to their ReferencePeriod, and convert between Level formats (max level or average level, see LevelProfile for more details).
|
|
38
74
|
operations (tuple[str, list[Expr]] | None, optional): Operations to apply to the expression. Defaults to None.
|
|
39
75
|
|
|
40
|
-
"""
|
|
41
|
-
|
|
76
|
+
"""
|
|
77
|
+
if is_level and is_profile:
|
|
78
|
+
message = "Expr cannot be both level and a profile. Set either is_level or is_profile True or both False."
|
|
79
|
+
raise ValueError(message)
|
|
80
|
+
|
|
81
|
+
if is_flow and is_stock:
|
|
82
|
+
message = "Expr cannot be both flow and stock. Set either is_flow or is_stock True or both False."
|
|
83
|
+
raise ValueError(message)
|
|
84
|
+
|
|
85
|
+
if is_profile and (is_flow or is_stock):
|
|
86
|
+
message = "Expr cannot be both a profile and a flow/stock. Profiles must be coefficients."
|
|
87
|
+
raise ValueError(message)
|
|
88
|
+
|
|
89
|
+
self._src: str | Curve | TimeVector | None = src
|
|
42
90
|
self._is_stock = is_stock
|
|
43
91
|
self._is_flow = is_flow
|
|
44
92
|
self._is_profile = is_profile
|
|
@@ -49,7 +97,7 @@ class Expr(Base):
|
|
|
49
97
|
# because fields are used to create
|
|
50
98
|
# error messages e.g. in __repr__
|
|
51
99
|
|
|
52
|
-
self._check_type(src, (str, TimeVector, type(None)))
|
|
100
|
+
self._check_type(src, (str, Curve, TimeVector, type(None)))
|
|
53
101
|
self._check_type(is_stock, (bool, type(None)))
|
|
54
102
|
self._check_type(is_flow, (bool, type(None)))
|
|
55
103
|
self._check_type(is_level, (bool, type(None)))
|
|
@@ -80,7 +128,7 @@ class Expr(Base):
|
|
|
80
128
|
raise ValueError(message)
|
|
81
129
|
return
|
|
82
130
|
if len(ops) != len(args) - 1:
|
|
83
|
-
message = f"Expected len(ops) == len(args). Got {operations}"
|
|
131
|
+
message = f"Expected len(ops) == len(args) - 1. Got {operations}"
|
|
84
132
|
raise ValueError(message)
|
|
85
133
|
for op in ops:
|
|
86
134
|
if op not in "+-/*":
|
|
@@ -106,8 +154,8 @@ class Expr(Base):
|
|
|
106
154
|
"""Return True if self is not an operation expression."""
|
|
107
155
|
return self._src is not None
|
|
108
156
|
|
|
109
|
-
def get_src(self) -> str | TimeVector | None:
|
|
110
|
-
"""Return str (either
|
|
157
|
+
def get_src(self) -> str | Curve | TimeVector | None:
|
|
158
|
+
"""Return str, Curve or TimeVector (either reference to Curve/TimeVector or Curve/TimeVector itself) or None if self is an operation expression."""
|
|
111
159
|
return self._src
|
|
112
160
|
|
|
113
161
|
def get_operations(self, expect_ops: bool, copy_list: bool) -> tuple[str, list[Expr]]:
|
|
@@ -122,23 +170,28 @@ class Expr(Base):
|
|
|
122
170
|
def _verify_operations(self, expect_ops: bool = False) -> None:
|
|
123
171
|
self._check_operations(self._operations, expect_ops)
|
|
124
172
|
ops = self._operations[0]
|
|
173
|
+
|
|
125
174
|
if not ops:
|
|
126
175
|
return
|
|
176
|
+
|
|
127
177
|
has_add = "+" in ops
|
|
128
178
|
has_sub = "-" in ops
|
|
129
179
|
has_mul = "*" in ops
|
|
130
180
|
has_div = "/" in ops
|
|
181
|
+
|
|
131
182
|
if (has_add or has_sub) and (has_mul or has_div):
|
|
132
|
-
message = f"+- in same operation level as */ in operations {self._operations} "
|
|
183
|
+
message = f"Found +- in same operation level as */ in operations {self._operations} "
|
|
133
184
|
raise ValueError(message)
|
|
185
|
+
|
|
134
186
|
if has_div:
|
|
135
187
|
seen_div = False
|
|
136
188
|
for op in ops:
|
|
137
189
|
if op == "/":
|
|
138
190
|
seen_div = True
|
|
139
|
-
|
|
191
|
+
continue
|
|
140
192
|
if seen_div and op != "/":
|
|
141
|
-
message = f"+-* after / in operations {self._operations}"
|
|
193
|
+
message = f"Found +-* after / in operations {self._operations}"
|
|
194
|
+
raise ValueError(message)
|
|
142
195
|
|
|
143
196
|
def is_flow(self) -> bool:
|
|
144
197
|
"""Return True if flow. Cannot be stock and flow."""
|
|
@@ -258,7 +311,7 @@ class Expr(Base):
|
|
|
258
311
|
def _create_op_expr( # noqa: C901
|
|
259
312
|
self,
|
|
260
313
|
op: str,
|
|
261
|
-
other:
|
|
314
|
+
other: Expr | int | float,
|
|
262
315
|
is_rhs: bool,
|
|
263
316
|
) -> Expr:
|
|
264
317
|
if isinstance(other, Expr):
|
|
@@ -377,12 +430,12 @@ class Expr(Base):
|
|
|
377
430
|
def __repr__(self) -> str:
|
|
378
431
|
"""Represent Expr as str."""
|
|
379
432
|
if self._src is not None:
|
|
380
|
-
return f"{self._src}"
|
|
433
|
+
return f"Expr({self._src})"
|
|
381
434
|
ops, args = self.get_operations(expect_ops=True, copy_list=False)
|
|
382
435
|
out = f"{args[0]}"
|
|
383
436
|
for op, arg in zip(ops, args[1:], strict=True):
|
|
384
437
|
out = f"{out} {op} {arg}"
|
|
385
|
-
return f"({out})"
|
|
438
|
+
return f"Expr({out})"
|
|
386
439
|
|
|
387
440
|
def __eq__(self, other) -> bool: # noqa: ANN001
|
|
388
441
|
"""Check if self and other are equal."""
|
|
@@ -397,7 +450,7 @@ class Expr(Base):
|
|
|
397
450
|
and self._profile == other._profile
|
|
398
451
|
and self._operations[0] == other._operations[0]
|
|
399
452
|
and len(self._operations[1]) == len(other._operations[1])
|
|
400
|
-
and all([self._operations[1][i] == other._operations[1][i] for i in range(len(self._operations[1]))])
|
|
453
|
+
and all([self._operations[1][i] == other._operations[1][i] for i in range(len(self._operations[1]))]) # noqa: SLF001
|
|
401
454
|
)
|
|
402
455
|
|
|
403
456
|
def __hash__(self) -> int:
|
|
@@ -417,11 +470,9 @@ class Expr(Base):
|
|
|
417
470
|
|
|
418
471
|
def add_loaders(self, loaders: set[Loader]) -> None:
|
|
419
472
|
"""Add all loaders stored in TimeVector or Curve within Expr to loaders."""
|
|
420
|
-
from framcore.curves import Curve
|
|
421
|
-
|
|
422
473
|
if self.is_leaf():
|
|
423
474
|
src = self.get_src()
|
|
424
|
-
if isinstance(src, TimeVector |
|
|
475
|
+
if isinstance(src, TimeVector | LoadedCurve):
|
|
425
476
|
loader = src.get_loader()
|
|
426
477
|
if loader is not None:
|
|
427
478
|
loaders.add(loader)
|
|
@@ -433,7 +484,7 @@ class Expr(Base):
|
|
|
433
484
|
|
|
434
485
|
# Proposed new way of creating Expr in classes.
|
|
435
486
|
def ensure_expr(
|
|
436
|
-
value: Expr | str | TimeVector | None, # technically anything that can be converted to float. Typehint for this?
|
|
487
|
+
value: Expr | str | Curve | TimeVector | None, # technically anything that can be converted to float. Typehint for this?
|
|
437
488
|
is_flow: bool = False,
|
|
438
489
|
is_stock: bool = False,
|
|
439
490
|
is_level: bool = False,
|
|
@@ -455,21 +506,15 @@ def ensure_expr(
|
|
|
455
506
|
value (Expr | str): The value as an expression of the expected type or None.
|
|
456
507
|
|
|
457
508
|
"""
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
raise ValueError(message)
|
|
462
|
-
if is_flow and is_stock:
|
|
463
|
-
message = "Expr cannot be both flow and stock. Set either is_flow or is_stock True or both False."
|
|
464
|
-
raise ValueError(message)
|
|
465
|
-
if is_profile and (is_flow or is_stock):
|
|
466
|
-
message = "Expr cannot be both a profile and a flow/stock. Profiles must be coefficients."
|
|
509
|
+
if not isinstance(value, (str, Expr, Curve, TimeVector)) and value is not None:
|
|
510
|
+
msg = f"Expected value to be of type Expr, str, Curve, TimeVector or None. Got {type(value).__name__}."
|
|
511
|
+
raise TypeError(msg)
|
|
467
512
|
|
|
468
513
|
if value is None:
|
|
469
514
|
return None
|
|
515
|
+
|
|
470
516
|
if isinstance(value, Expr):
|
|
471
517
|
# Check wether given Expr matches expected flow, stock, profile and level status.
|
|
472
|
-
# Alternatively we could just create a new Expr with updated status.
|
|
473
518
|
if value.is_flow() != is_flow or value.is_stock() != is_stock or value.is_level() != is_level or value.is_profile() != is_profile:
|
|
474
519
|
message = (
|
|
475
520
|
"Given Expr has a mismatch between expected and actual flow/stock or level/profile status:\nExpected: "
|
|
@@ -488,3 +533,59 @@ def ensure_expr(
|
|
|
488
533
|
is_profile=is_profile,
|
|
489
534
|
profile=profile,
|
|
490
535
|
)
|
|
536
|
+
|
|
537
|
+
|
|
538
|
+
def get_profile_exprs_from_leaf_levels(expr: Expr) -> list[Expr]:
|
|
539
|
+
"""
|
|
540
|
+
Get all profile expressions from leaf-level Expr objects that are marked as levels.
|
|
541
|
+
|
|
542
|
+
Args:
|
|
543
|
+
expr (Expr): The starting Expr object.
|
|
544
|
+
|
|
545
|
+
Returns:
|
|
546
|
+
list[Expr]: A list of profile expressions from leaf-level Expr objects.
|
|
547
|
+
|
|
548
|
+
"""
|
|
549
|
+
profile_exprs = []
|
|
550
|
+
|
|
551
|
+
def _traverse(expr: Expr) -> None:
|
|
552
|
+
if expr.is_leaf():
|
|
553
|
+
if expr.is_level() and expr.get_profile() is not None:
|
|
554
|
+
profile_exprs.append(expr.get_profile())
|
|
555
|
+
return
|
|
556
|
+
|
|
557
|
+
# Recursively traverse the arguments of the expression
|
|
558
|
+
_, args = expr.get_operations(expect_ops=False, copy_list=False)
|
|
559
|
+
for arg in args:
|
|
560
|
+
_traverse(arg)
|
|
561
|
+
|
|
562
|
+
_traverse(expr)
|
|
563
|
+
return profile_exprs
|
|
564
|
+
|
|
565
|
+
|
|
566
|
+
def get_leaf_profiles(expr: Expr) -> list[Expr]:
|
|
567
|
+
"""
|
|
568
|
+
Get all leaf profile expressions from an Expr object.
|
|
569
|
+
|
|
570
|
+
Args:
|
|
571
|
+
expr (Expr): The starting Expr object.
|
|
572
|
+
|
|
573
|
+
Returns:
|
|
574
|
+
list[Expr]: A list of leaf profile expressions.
|
|
575
|
+
|
|
576
|
+
"""
|
|
577
|
+
leaf_profiles = []
|
|
578
|
+
|
|
579
|
+
def _traverse(expr: Expr) -> None:
|
|
580
|
+
if expr.is_leaf():
|
|
581
|
+
if expr.is_profile():
|
|
582
|
+
leaf_profiles.append(expr)
|
|
583
|
+
return
|
|
584
|
+
|
|
585
|
+
# Recursively traverse the arguments of the expression
|
|
586
|
+
_, args = expr.get_operations(expect_ops=False, copy_list=False)
|
|
587
|
+
for arg in args:
|
|
588
|
+
_traverse(arg)
|
|
589
|
+
|
|
590
|
+
_traverse(expr)
|
|
591
|
+
return leaf_profiles
|
framcore/expressions/__init__.py
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# framcore/expressions/__init__.py
|
|
2
2
|
|
|
3
|
-
from framcore.expressions.Expr import Expr, ensure_expr
|
|
3
|
+
from framcore.expressions.Expr import Expr, ensure_expr, get_leaf_profiles, get_profile_exprs_from_leaf_levels
|
|
4
4
|
|
|
5
5
|
from framcore.expressions.units import (
|
|
6
6
|
get_unit_conversion_factor,
|
|
@@ -18,7 +18,9 @@ from framcore.expressions.queries import (
|
|
|
18
18
|
__all__ = [
|
|
19
19
|
"Expr",
|
|
20
20
|
"ensure_expr",
|
|
21
|
+
"get_leaf_profiles",
|
|
21
22
|
"get_level_value",
|
|
23
|
+
"get_profile_exprs_from_leaf_levels",
|
|
22
24
|
"get_profile_vector",
|
|
23
25
|
"get_timeindexes_from_expr",
|
|
24
26
|
"get_unit_conversion_factor",
|