fram-core 0.0.0__py3-none-any.whl → 0.1.0a1__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 (103) hide show
  1. fram_core-0.1.0a1.dist-info/METADATA +41 -0
  2. fram_core-0.1.0a1.dist-info/RECORD +100 -0
  3. {fram_core-0.0.0.dist-info → fram_core-0.1.0a1.dist-info}/WHEEL +1 -2
  4. fram_core-0.1.0a1.dist-info/licenses/LICENSE.md +8 -0
  5. framcore/Base.py +142 -0
  6. framcore/Model.py +73 -0
  7. framcore/__init__.py +9 -0
  8. framcore/aggregators/Aggregator.py +153 -0
  9. framcore/aggregators/HydroAggregator.py +837 -0
  10. framcore/aggregators/NodeAggregator.py +495 -0
  11. framcore/aggregators/WindSolarAggregator.py +323 -0
  12. framcore/aggregators/__init__.py +13 -0
  13. framcore/aggregators/_utils.py +184 -0
  14. framcore/attributes/Arrow.py +305 -0
  15. framcore/attributes/ElasticDemand.py +90 -0
  16. framcore/attributes/ReservoirCurve.py +37 -0
  17. framcore/attributes/SoftBound.py +19 -0
  18. framcore/attributes/StartUpCost.py +54 -0
  19. framcore/attributes/Storage.py +146 -0
  20. framcore/attributes/TargetBound.py +18 -0
  21. framcore/attributes/__init__.py +65 -0
  22. framcore/attributes/hydro/HydroBypass.py +42 -0
  23. framcore/attributes/hydro/HydroGenerator.py +83 -0
  24. framcore/attributes/hydro/HydroPump.py +156 -0
  25. framcore/attributes/hydro/HydroReservoir.py +27 -0
  26. framcore/attributes/hydro/__init__.py +13 -0
  27. framcore/attributes/level_profile_attributes.py +714 -0
  28. framcore/components/Component.py +112 -0
  29. framcore/components/Demand.py +130 -0
  30. framcore/components/Flow.py +167 -0
  31. framcore/components/HydroModule.py +330 -0
  32. framcore/components/Node.py +76 -0
  33. framcore/components/Thermal.py +204 -0
  34. framcore/components/Transmission.py +183 -0
  35. framcore/components/_PowerPlant.py +81 -0
  36. framcore/components/__init__.py +22 -0
  37. framcore/components/wind_solar.py +67 -0
  38. framcore/curves/Curve.py +44 -0
  39. framcore/curves/LoadedCurve.py +155 -0
  40. framcore/curves/__init__.py +9 -0
  41. framcore/events/__init__.py +21 -0
  42. framcore/events/events.py +51 -0
  43. framcore/expressions/Expr.py +490 -0
  44. framcore/expressions/__init__.py +28 -0
  45. framcore/expressions/_get_constant_from_expr.py +483 -0
  46. framcore/expressions/_time_vector_operations.py +615 -0
  47. framcore/expressions/_utils.py +73 -0
  48. framcore/expressions/queries.py +423 -0
  49. framcore/expressions/units.py +207 -0
  50. framcore/fingerprints/__init__.py +11 -0
  51. framcore/fingerprints/fingerprint.py +293 -0
  52. framcore/juliamodels/JuliaModel.py +161 -0
  53. framcore/juliamodels/__init__.py +7 -0
  54. framcore/loaders/__init__.py +10 -0
  55. framcore/loaders/loaders.py +407 -0
  56. framcore/metadata/Div.py +73 -0
  57. framcore/metadata/ExprMeta.py +50 -0
  58. framcore/metadata/LevelExprMeta.py +17 -0
  59. framcore/metadata/Member.py +55 -0
  60. framcore/metadata/Meta.py +44 -0
  61. framcore/metadata/__init__.py +15 -0
  62. framcore/populators/Populator.py +108 -0
  63. framcore/populators/__init__.py +7 -0
  64. framcore/querydbs/CacheDB.py +50 -0
  65. framcore/querydbs/ModelDB.py +34 -0
  66. framcore/querydbs/QueryDB.py +45 -0
  67. framcore/querydbs/__init__.py +11 -0
  68. framcore/solvers/Solver.py +48 -0
  69. framcore/solvers/SolverConfig.py +272 -0
  70. framcore/solvers/__init__.py +9 -0
  71. framcore/timeindexes/AverageYearRange.py +20 -0
  72. framcore/timeindexes/ConstantTimeIndex.py +17 -0
  73. framcore/timeindexes/DailyIndex.py +21 -0
  74. framcore/timeindexes/FixedFrequencyTimeIndex.py +762 -0
  75. framcore/timeindexes/HourlyIndex.py +21 -0
  76. framcore/timeindexes/IsoCalendarDay.py +31 -0
  77. framcore/timeindexes/ListTimeIndex.py +197 -0
  78. framcore/timeindexes/ModelYear.py +17 -0
  79. framcore/timeindexes/ModelYears.py +18 -0
  80. framcore/timeindexes/OneYearProfileTimeIndex.py +21 -0
  81. framcore/timeindexes/ProfileTimeIndex.py +32 -0
  82. framcore/timeindexes/SinglePeriodTimeIndex.py +37 -0
  83. framcore/timeindexes/TimeIndex.py +90 -0
  84. framcore/timeindexes/WeeklyIndex.py +21 -0
  85. framcore/timeindexes/__init__.py +36 -0
  86. framcore/timevectors/ConstantTimeVector.py +135 -0
  87. framcore/timevectors/LinearTransformTimeVector.py +114 -0
  88. framcore/timevectors/ListTimeVector.py +123 -0
  89. framcore/timevectors/LoadedTimeVector.py +104 -0
  90. framcore/timevectors/ReferencePeriod.py +41 -0
  91. framcore/timevectors/TimeVector.py +94 -0
  92. framcore/timevectors/__init__.py +17 -0
  93. framcore/utils/__init__.py +36 -0
  94. framcore/utils/get_regional_volumes.py +369 -0
  95. framcore/utils/get_supported_components.py +60 -0
  96. framcore/utils/global_energy_equivalent.py +46 -0
  97. framcore/utils/isolate_subnodes.py +163 -0
  98. framcore/utils/loaders.py +97 -0
  99. framcore/utils/node_flow_utils.py +236 -0
  100. framcore/utils/storage_subsystems.py +107 -0
  101. fram_core-0.0.0.dist-info/METADATA +0 -5
  102. fram_core-0.0.0.dist-info/RECORD +0 -4
  103. fram_core-0.0.0.dist-info/top_level.txt +0 -1
@@ -0,0 +1,112 @@
1
+ from __future__ import annotations
2
+
3
+ from abc import ABC, abstractmethod
4
+ from collections.abc import Iterable
5
+
6
+ from framcore import Base
7
+ from framcore.metadata import Meta
8
+
9
+
10
+ class Component(Base, ABC):
11
+ """Component interface class."""
12
+
13
+ def __init__(self) -> None:
14
+ """Set mandatory private variables."""
15
+ self._parent: Component | None = None
16
+ self._meta: dict[str, Meta] = dict()
17
+
18
+ def add_meta(self, key: str, value: Meta) -> None:
19
+ """Add metadata to component. Overwrite if already exist."""
20
+ self._check_type(key, str)
21
+ self._check_type(value, Meta)
22
+ self._meta[key] = value
23
+
24
+ def get_meta(self, key: str) -> Meta | None:
25
+ """Get metadata from component or return None if not exist."""
26
+ self._check_type(key, str)
27
+ return self._meta.get(key, None)
28
+
29
+ def get_meta_keys(self) -> Iterable[str]:
30
+ """Get iterable with all metakeys in component."""
31
+ return self._meta.keys()
32
+
33
+ def get_simpler_components(
34
+ self,
35
+ base_name: str,
36
+ ) -> dict[str, Component]:
37
+ """
38
+ Return representation of self as dict of named simpler components.
39
+
40
+ The base_name should be unique within a model instance, and should
41
+ be used to prefix name of all simpler components.
42
+
43
+ Insert self as parent in each child.
44
+
45
+ Transfer metadata to each child.
46
+ """
47
+ self._check_type(base_name, str)
48
+ components = self._get_simpler_components(base_name)
49
+ assert base_name not in components, f"base_name: {base_name}\ncomponent: {self}"
50
+ components: dict[str, Component]
51
+ self._check_type(components, dict)
52
+ for name, c in components.items():
53
+ self._check_type(name, str)
54
+ self._check_type(c, Component)
55
+ self._check_component_not_self(c)
56
+ c: Component
57
+ c._parent = self # noqa: SLF001
58
+ for key in self.get_meta_keys():
59
+ value = self.get_meta(key)
60
+ for c in components.values():
61
+ c.add_meta(key, value)
62
+ return components
63
+
64
+ def get_parent(self) -> Component | None:
65
+ """Return parent if any, else None."""
66
+ self._check_type(self._parent, (Component, type(None)))
67
+ self._check_component_not_self(self._parent)
68
+ return self._parent
69
+
70
+ def get_parents(self) -> list[Component]:
71
+ """Return list of all parents, including self."""
72
+ child = self
73
+ parent = child.get_parent()
74
+ parents = [child]
75
+ while parent is not None:
76
+ child = parent
77
+ parent = child.get_parent()
78
+ parents.append(child)
79
+ self._check_unique_parents(parents)
80
+ return parents
81
+
82
+ def get_top_parent(self) -> Component:
83
+ """Return topmost parent. (May be object self)."""
84
+ parents = self.get_parents()
85
+ return parents[-1]
86
+
87
+ def replace_node(self, old: str, new: str) -> None:
88
+ """Replace old node with new. Not error if no match."""
89
+ self._check_type(old, str)
90
+ self._check_type(new, str)
91
+ self._replace_node(old, new)
92
+
93
+ def _check_component_not_self(self, other: Component | None) -> None:
94
+ if not isinstance(other, Component):
95
+ return
96
+ if self != other:
97
+ return
98
+ message = f"Expected other component than {self}."
99
+ raise TypeError(message)
100
+
101
+ def _check_unique_parents(self, parents: list[Component]) -> None:
102
+ if len(parents) > len(set(parents)):
103
+ message = f"Parents for {self} are not unique."
104
+ raise TypeError(message)
105
+
106
+ @abstractmethod
107
+ def _replace_node(self, old: str, new: str) -> None:
108
+ pass
109
+
110
+ @abstractmethod
111
+ def _get_simpler_components(self, base_name: str) -> dict[str, Component]:
112
+ pass
@@ -0,0 +1,130 @@
1
+ """Demand class."""
2
+
3
+ from framcore.attributes import Arrow, AvgFlowVolume, Conversion, ElasticDemand, FlowVolume, ReservePrice
4
+ from framcore.components import Component, Flow
5
+ from framcore.expressions import Expr, ensure_expr
6
+ from framcore.timevectors import TimeVector
7
+
8
+
9
+ class Demand(Component):
10
+ """Demand class representing a simple demand with possible reserve price. Subclass of Component."""
11
+
12
+ def __init__(
13
+ self,
14
+ node: str,
15
+ capacity: FlowVolume | None = None,
16
+ reserve_price: ReservePrice | None = None,
17
+ elastic_demand: ElasticDemand | None = None,
18
+ temperature_profile: Expr | str | TimeVector | None = None,
19
+ consumption: AvgFlowVolume | None = None,
20
+ ) -> None:
21
+ """Initialize the Demand class."""
22
+ super().__init__()
23
+ self._check_type(node, str)
24
+ self._check_type(capacity, (FlowVolume, type(None)))
25
+ self._check_type(reserve_price, (ReservePrice, type(None)))
26
+ self._check_type(elastic_demand, (ElasticDemand, type(None)))
27
+ self._check_type(consumption, (AvgFlowVolume, type(None)))
28
+
29
+ if reserve_price is not None and elastic_demand is not None:
30
+ message = "Cannot have 'reserve_price' and 'elastic_demand' at the same time."
31
+ raise ValueError(message)
32
+
33
+ self._node = node
34
+ self._capacity = capacity
35
+ self._reserve_price = reserve_price
36
+ self._elastic_demand = elastic_demand
37
+ self._temperature_profile = ensure_expr(temperature_profile, is_profile=True)
38
+
39
+ if consumption is None:
40
+ consumption = AvgFlowVolume()
41
+
42
+ self._consumption: AvgFlowVolume = consumption
43
+
44
+ def get_capacity(self) -> FlowVolume:
45
+ """Get the capacity of the demand component."""
46
+ return self._capacity
47
+
48
+ def get_consumption(self) -> AvgFlowVolume:
49
+ """Get the consumption of the demand component."""
50
+ return self._consumption
51
+
52
+ def get_node(self) -> str:
53
+ """Get the node of the demand component."""
54
+ return self._node
55
+
56
+ def set_node(self, node: str) -> None:
57
+ """Set the node of the demand component."""
58
+ self._check_type(node, str)
59
+ self.node = node
60
+
61
+ def get_reserve_price(self) -> ReservePrice | None:
62
+ """Get the reserve price level of the demand component."""
63
+ return self._reserve_price
64
+
65
+ def set_reserve_price_level(self, reserve_price: ReservePrice | None) -> None: # TODO: Update
66
+ """Set the reserve price level of the demand component."""
67
+ self._check_type(reserve_price, (Expr, type(None)))
68
+ if self._elastic_demand and reserve_price:
69
+ message = "Cannot set reserve_price when elastic_demand is not None."
70
+ raise ValueError(message)
71
+ self._reserve_price = reserve_price
72
+
73
+ def get_elastic_demand(self) -> ElasticDemand | None:
74
+ """Get the elastic demand of the demand component."""
75
+ return self._elastic_demand
76
+
77
+ def set_elastic_demand(self, elastic_demand: ElasticDemand | None) -> None:
78
+ """Set the elastic demand of the demand component."""
79
+ self._check_type(elastic_demand, (ElasticDemand, type(None)))
80
+ if self._reserve_price is not None and elastic_demand is not None:
81
+ message = "Cannot set elastic_demand when reserve_price_level is not None."
82
+ raise ValueError(message)
83
+ self._elastic_demand = elastic_demand
84
+
85
+ def get_temperature_profile(self) -> Expr | None:
86
+ """Get the temperature profile of the demand component."""
87
+ return self._temperature_profile
88
+
89
+ def set_temperature_profile(self, temperature_profile: Expr | str | None) -> None:
90
+ """Set the temperature profile of the demand component."""
91
+ self._check_type(temperature_profile, (Expr, str, TimeVector, type(None)))
92
+ self._temperature_profile = ensure_expr(temperature_profile, is_profile=True)
93
+
94
+ """Implementation of Component interface"""
95
+
96
+ def _replace_node(self, old: str, new: str) -> None:
97
+ if old == self._node:
98
+ self._node = new
99
+ else:
100
+ message = f"{old} not found in {self}. Expected existing node {self._node}."
101
+ raise ValueError(message)
102
+
103
+ def _get_simpler_components(self, base_name: str) -> dict[str, Component]:
104
+ return {base_name + "_Flow": self._create_flow()}
105
+
106
+ def _create_flow(self) -> Flow:
107
+ is_exogenous = self._elastic_demand is None and self._reserve_price is None
108
+
109
+ flow = Flow(
110
+ main_node=self._node,
111
+ max_capacity=self._capacity,
112
+ min_capacity=self._capacity if is_exogenous else None,
113
+ volume=self._consumption,
114
+ arrow_volumes=None,
115
+ is_exogenous=is_exogenous,
116
+ )
117
+
118
+ power_arrow = Arrow(self._node, False, conversion=Conversion(value=1))
119
+ flow.add_arrow(power_arrow)
120
+
121
+ if self._reserve_price is not None:
122
+ flow.add_cost_term("reserve_price", self._reserve_price)
123
+
124
+ # TODO: Implement correctly when Curve is ready. For now, model as inelastic consumer w. reserve_price
125
+ elif self._elastic_demand is not None:
126
+ price = self._elastic_demand.get_max_price()
127
+ reserve_price = ReservePrice(level=price.get_level(), profile=price.get_profile())
128
+ flow.add_cost_term("reserve_price", cost_term=reserve_price)
129
+
130
+ return flow
@@ -0,0 +1,167 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import TYPE_CHECKING
4
+
5
+ from framcore.attributes import Arrow, AvgFlowVolume, FlowVolume, ObjectiveCoefficient, StartUpCost
6
+ from framcore.components import Component
7
+ from framcore.fingerprints import Fingerprint
8
+ from framcore.loaders import Loader
9
+
10
+ if TYPE_CHECKING:
11
+ from framcore.loaders import Loader
12
+
13
+
14
+ class Flow(Component):
15
+ """Represents a commodity flow in or out of one or more nodes."""
16
+
17
+ def __init__(
18
+ self,
19
+ main_node: str,
20
+ max_capacity: FlowVolume | None = None,
21
+ min_capacity: FlowVolume | None = None,
22
+ startupcost: StartUpCost | None = None,
23
+ volume: AvgFlowVolume | None = None,
24
+ arrow_volumes: dict[Arrow, AvgFlowVolume] | None = None,
25
+ is_exogenous: bool = False,
26
+ ) -> None:
27
+ """Initialize Flow with main node, capacity, and startup cost."""
28
+ super().__init__()
29
+ self._check_type(main_node, str)
30
+ self._check_type(max_capacity, (FlowVolume, type(None)))
31
+ self._check_type(min_capacity, (FlowVolume, type(None)))
32
+ self._check_type(startupcost, (StartUpCost, type(None)))
33
+ self._check_type(volume, (FlowVolume, type(None)))
34
+ self._check_type(arrow_volumes, (dict, type(None)))
35
+ self._main_node: str = main_node
36
+ self._max_capacity = max_capacity
37
+ self._min_capacity = min_capacity
38
+ self._startupcost = startupcost
39
+ self._arrows: set[Arrow] = set()
40
+ self._cost_terms: dict[str, ObjectiveCoefficient] = dict()
41
+ self._is_exogenous: bool = is_exogenous
42
+
43
+ if not volume:
44
+ volume = AvgFlowVolume()
45
+ self._volume: AvgFlowVolume = volume
46
+
47
+ if arrow_volumes is None:
48
+ arrow_volumes = dict()
49
+ self._arrow_volumes: dict[Arrow, AvgFlowVolume] = arrow_volumes
50
+
51
+ def is_exogenous(self) -> bool:
52
+ """Return True if Flow is exogenous."""
53
+ return self._is_exogenous
54
+
55
+ def set_exogenous(self) -> None:
56
+ """
57
+ Treat flow as rhs term.
58
+
59
+ Use volume if it exists.
60
+ If no volume, then try to use
61
+ min_capacity and max_capacity, which must
62
+ be equal. Error if this fails.
63
+ """
64
+ self._is_exogenous = True
65
+
66
+ def set_endogenous(self) -> None:
67
+ """
68
+ Treat flow as decision variable.
69
+
70
+ Volume should be updated with results after a solve.
71
+ """
72
+ self._is_exogenous = False
73
+
74
+ def get_main_node(self) -> str:
75
+ """Get the main node of the flow."""
76
+ return self._main_node
77
+
78
+ def get_volume(self) -> AvgFlowVolume:
79
+ """Get the volume of the flow."""
80
+ return self._volume
81
+
82
+ def get_arrow_volumes(self) -> dict[Arrow, AvgFlowVolume]:
83
+ """Get dict of volume converted to volume at node pointed to by Arrow."""
84
+ return self._arrow_volumes
85
+
86
+ def get_max_capacity(self) -> FlowVolume | None:
87
+ """Get the maximum capacity of the flow."""
88
+ return self._max_capacity
89
+
90
+ def set_max_capacity(self, capacity: FlowVolume | None) -> None:
91
+ """Set the maximum capacity of the flow."""
92
+ self._check_type(capacity, (FlowVolume, type(None)))
93
+ self._max_capacity = capacity
94
+
95
+ def get_min_capacity(self) -> FlowVolume | None:
96
+ """Get the minimum capacity of the flow."""
97
+ return self._min_capacity
98
+
99
+ def set_min_capacity(self, capacity: FlowVolume | None) -> None:
100
+ """Set the minimum capacity of the flow."""
101
+ self._check_type(capacity, (FlowVolume, type(None)))
102
+ self._min_capacity = capacity
103
+
104
+ def get_startupcost(self) -> StartUpCost | None:
105
+ """Get the startup cost of the flow."""
106
+ self._check_type(self._startupcost, (StartUpCost, type(None)))
107
+ return self._startupcost
108
+
109
+ def set_startupcost(self, startupcost: StartUpCost | None) -> None:
110
+ """Set the startup cost of the flow."""
111
+ self._check_type(startupcost, (StartUpCost, type(None)))
112
+ self._startupcost = startupcost
113
+
114
+ def get_arrows(self) -> set[Arrow]:
115
+ """Get the arrows of the flow."""
116
+ return self._arrows
117
+
118
+ def add_arrow(self, arrow: Arrow) -> None:
119
+ """Add an arrow to the flow."""
120
+ self._check_type(arrow, Arrow)
121
+ self._arrows.add(arrow)
122
+
123
+ def add_cost_term(self, key: str, cost_term: ObjectiveCoefficient) -> None:
124
+ """Add a cost term to the flow."""
125
+ self._check_type(key, str)
126
+ self._check_type(cost_term, ObjectiveCoefficient)
127
+ self._cost_terms[key] = cost_term
128
+
129
+ def get_cost_terms(self) -> dict[str, ObjectiveCoefficient]:
130
+ """Get the cost terms of the flow."""
131
+ return self._cost_terms
132
+
133
+ def add_loaders(self, loaders: set[Loader]) -> None:
134
+ """Add loaders stored in attributes to loaders."""
135
+ from framcore.utils import add_loaders_if # noqa: PLC0415
136
+
137
+ add_loaders_if(loaders, self.get_volume())
138
+ add_loaders_if(loaders, self.get_max_capacity())
139
+ add_loaders_if(loaders, self.get_min_capacity())
140
+
141
+ for cost in self.get_cost_terms().values():
142
+ add_loaders_if(loaders, cost)
143
+
144
+ for arrow in self.get_arrows():
145
+ add_loaders_if(loaders, arrow)
146
+
147
+ for volume in self.get_arrow_volumes().values():
148
+ add_loaders_if(loaders, volume)
149
+
150
+ def _replace_node(self, old: str, new: str) -> None:
151
+ # Component.replace_node does input type check
152
+ if old == self._main_node:
153
+ self._main_node = new
154
+ for a in self._arrows:
155
+ a: Arrow
156
+ if a.get_node() == old:
157
+ a.set_node(new)
158
+ return
159
+
160
+ def _get_simpler_components(self, _: str) -> dict[str, Component]:
161
+ return dict()
162
+
163
+ def _get_fingerprint(self) -> Fingerprint:
164
+ refs = {}
165
+ refs["_main_node"] = self._main_node
166
+
167
+ return self.get_fingerprint_default()