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.
Files changed (78) hide show
  1. {fram_core-0.1.0a1.dist-info → fram_core-0.1.1.dist-info}/METADATA +6 -5
  2. fram_core-0.1.1.dist-info/RECORD +100 -0
  3. {fram_core-0.1.0a1.dist-info → fram_core-0.1.1.dist-info}/WHEEL +1 -1
  4. framcore/Base.py +22 -3
  5. framcore/Model.py +26 -9
  6. framcore/__init__.py +2 -1
  7. framcore/aggregators/Aggregator.py +30 -11
  8. framcore/aggregators/HydroAggregator.py +37 -25
  9. framcore/aggregators/NodeAggregator.py +65 -30
  10. framcore/aggregators/WindSolarAggregator.py +22 -30
  11. framcore/attributes/Arrow.py +6 -4
  12. framcore/attributes/ElasticDemand.py +13 -13
  13. framcore/attributes/ReservoirCurve.py +3 -17
  14. framcore/attributes/SoftBound.py +2 -5
  15. framcore/attributes/StartUpCost.py +14 -3
  16. framcore/attributes/Storage.py +17 -5
  17. framcore/attributes/TargetBound.py +2 -4
  18. framcore/attributes/__init__.py +2 -4
  19. framcore/attributes/hydro/HydroBypass.py +9 -2
  20. framcore/attributes/hydro/HydroGenerator.py +24 -7
  21. framcore/attributes/hydro/HydroPump.py +32 -10
  22. framcore/attributes/hydro/HydroReservoir.py +4 -4
  23. framcore/attributes/level_profile_attributes.py +250 -53
  24. framcore/components/Component.py +27 -3
  25. framcore/components/Demand.py +18 -4
  26. framcore/components/Flow.py +26 -4
  27. framcore/components/HydroModule.py +45 -4
  28. framcore/components/Node.py +32 -9
  29. framcore/components/Thermal.py +12 -8
  30. framcore/components/Transmission.py +17 -2
  31. framcore/components/wind_solar.py +25 -10
  32. framcore/curves/LoadedCurve.py +0 -9
  33. framcore/expressions/Expr.py +137 -36
  34. framcore/expressions/__init__.py +3 -1
  35. framcore/expressions/_get_constant_from_expr.py +14 -20
  36. framcore/expressions/queries.py +121 -84
  37. framcore/expressions/units.py +30 -3
  38. framcore/fingerprints/fingerprint.py +0 -1
  39. framcore/juliamodels/JuliaModel.py +13 -3
  40. framcore/loaders/loaders.py +0 -2
  41. framcore/metadata/ExprMeta.py +13 -7
  42. framcore/metadata/LevelExprMeta.py +16 -1
  43. framcore/metadata/Member.py +7 -7
  44. framcore/metadata/__init__.py +1 -1
  45. framcore/querydbs/CacheDB.py +1 -1
  46. framcore/solvers/Solver.py +21 -6
  47. framcore/solvers/SolverConfig.py +4 -4
  48. framcore/timeindexes/AverageYearRange.py +9 -2
  49. framcore/timeindexes/ConstantTimeIndex.py +7 -2
  50. framcore/timeindexes/DailyIndex.py +14 -2
  51. framcore/timeindexes/FixedFrequencyTimeIndex.py +105 -53
  52. framcore/timeindexes/HourlyIndex.py +14 -2
  53. framcore/timeindexes/IsoCalendarDay.py +5 -3
  54. framcore/timeindexes/ListTimeIndex.py +103 -23
  55. framcore/timeindexes/ModelYear.py +8 -2
  56. framcore/timeindexes/ModelYears.py +11 -2
  57. framcore/timeindexes/OneYearProfileTimeIndex.py +10 -2
  58. framcore/timeindexes/ProfileTimeIndex.py +14 -3
  59. framcore/timeindexes/SinglePeriodTimeIndex.py +1 -1
  60. framcore/timeindexes/TimeIndex.py +16 -3
  61. framcore/timeindexes/WeeklyIndex.py +14 -2
  62. framcore/{expressions → timeindexes}/_time_vector_operations.py +76 -2
  63. framcore/timevectors/ConstantTimeVector.py +12 -16
  64. framcore/timevectors/LinearTransformTimeVector.py +20 -3
  65. framcore/timevectors/ListTimeVector.py +18 -14
  66. framcore/timevectors/LoadedTimeVector.py +1 -8
  67. framcore/timevectors/ReferencePeriod.py +13 -3
  68. framcore/timevectors/TimeVector.py +26 -12
  69. framcore/utils/__init__.py +0 -1
  70. framcore/utils/get_regional_volumes.py +21 -3
  71. framcore/utils/get_supported_components.py +1 -1
  72. framcore/utils/global_energy_equivalent.py +22 -5
  73. framcore/utils/isolate_subnodes.py +12 -3
  74. framcore/utils/loaders.py +7 -7
  75. framcore/utils/node_flow_utils.py +4 -4
  76. framcore/utils/storage_subsystems.py +3 -4
  77. fram_core-0.1.0a1.dist-info/RECORD +0 -100
  78. {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
- """HydroModule class representing a hydro module component."""
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
- """Initialize the HydroModule with its parameters."""
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.get_energy_eq(),
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.get_energy_eq(),
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()
@@ -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, ShaddowPrice, Storage
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
- """Node class. Subclass of Component."""
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: ShaddowPrice | None = None,
32
+ price: ShadowPrice | None = None,
20
33
  storage: Storage | None = None,
21
34
  ) -> None:
22
- """Initialize the Node class."""
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, (ShaddowPrice, type(None)))
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 internal is_exogenous flag to True."""
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 internal is_exogenous flag to False."""
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) -> ShaddowPrice:
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 # noqa: PLC0415
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())
@@ -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): Startup cost.
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
- """Initialize object of the Transmission class. Perform type checks and convert arguments to expressions."""
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 solarpower plant with various attributes inherited from the parent class PowerPlant.
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
- """Initialize the Wind and Solar class."""
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
- """Wind power component."""
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
- """Solar power component."""
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
@@ -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()
@@ -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
- """Expressions for data manipulation of curves and timevectors."""
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) expression.
63
+ Create new (immutable) Expression.
30
64
 
31
65
  Args:
32
- src (str | None, optional): _description_. Defaults to None.
33
- is_stock (bool, optional): _description_. Defaults to False.
34
- is_flow (bool, optional): _description_. Defaults to False.
35
- is_profile (bool, optional): _description_. Defaults to False.
36
- is_level (bool, optional): _description_. Defaults to False.
37
- profile (Expr | None, optional): _description_. Defaults to None.
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
- """ # TODO: write detailed description of what the labels of Expr means (level, profile, flow, stock)
41
- self._src: str | TimeVector | None = src
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 usercode or key in model) or None if self is an operation expression."""
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
- break
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: object,
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 | Curve):
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
- # Following checks could be moved to Expr.
459
- if is_level and is_profile:
460
- message = "Expr cannot be both level and a profile. Set either is_level or is_profile True or both False."
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
@@ -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",