fram-core 0.0.0__py3-none-any.whl → 0.1.0a2__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.0a2.dist-info/METADATA +42 -0
  2. fram_core-0.1.0a2.dist-info/RECORD +100 -0
  3. {fram_core-0.0.0.dist-info → fram_core-0.1.0a2.dist-info}/WHEEL +1 -2
  4. fram_core-0.1.0a2.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,97 @@
1
+ from __future__ import annotations
2
+
3
+ from pathlib import Path
4
+ from typing import TYPE_CHECKING
5
+
6
+ from framcore.events import send_warning_event
7
+
8
+ if TYPE_CHECKING:
9
+ from framcore import Model
10
+ from framcore.loaders import Loader
11
+
12
+
13
+ def add_loaders_if(loaders: set, value: object | None) -> None:
14
+ """Call value.add_loaders(loaders) if value is not None."""
15
+ _check_type(loaders, "loaders", set)
16
+ if value is None:
17
+ return
18
+ value.add_loaders(loaders)
19
+
20
+
21
+ def add_loaders(loaders: set[Loader], model: Model) -> None:
22
+ """Add all loaders stored in Model to loaders set."""
23
+ from framcore import Model # noqa: PLC0415
24
+ from framcore.components import Component, Flow, Node # noqa: PLC0415
25
+ from framcore.curves import Curve # noqa: PLC0415
26
+ from framcore.expressions import Expr # noqa: PLC0415
27
+ from framcore.timevectors import TimeVector # noqa: PLC0415
28
+ from framcore.utils import get_supported_components # noqa: PLC0415
29
+
30
+ _check_type(loaders, "loaders", set)
31
+ _check_type(model, "model", Model)
32
+
33
+ data = model.get_data()
34
+ components = dict()
35
+
36
+ for key, value in data.items():
37
+ if isinstance(value, Expr):
38
+ value.add_loaders(loaders)
39
+
40
+ elif isinstance(value, TimeVector | Curve):
41
+ loader = value.get_loader()
42
+ if loader is not None:
43
+ loaders.add(loader)
44
+
45
+ elif isinstance(value, Component):
46
+ components[key] = value
47
+
48
+ graph: dict[str, Flow | Node] = get_supported_components(components, (Flow, Node), tuple())
49
+
50
+ for c in graph.values():
51
+ c.add_loaders(loaders)
52
+
53
+
54
+ def replace_loader_path(loaders: set[Loader], old: Path, new: Path) -> None:
55
+ """Replace old path with new for all loaders using old path."""
56
+ from framcore.loaders import FileLoader # noqa: PLC0415
57
+
58
+ _check_type(loaders, "loaders", set)
59
+
60
+ new = _check_path(new, "new", make_absolute=True)
61
+ old = _check_path(old, "old", error_if_not_absolute=True)
62
+
63
+ for loader in loaders:
64
+ try:
65
+ source = loader.get_source()
66
+ except Exception:
67
+ send_warning_event(f"loader.get_source() failed for {loader}. Skipping this one.")
68
+ continue
69
+
70
+ if isinstance(source, Path) and old in source.parents:
71
+ loader.set_source(new_source=new / source.relative_to(old))
72
+
73
+ if isinstance(loader, FileLoader) and not isinstance(source, Path):
74
+ send_warning_event(f"FileLoader.get_source() does not return Path as it should for loader {loader}. Instead of Path, got {source}")
75
+
76
+
77
+ def _check_type(value, name, expected) -> None: # noqa: ANN001
78
+ if not isinstance(value, expected):
79
+ message = f"Expected {name} to be {type(expected).__name__}. Got Got {type(value).__name__}"
80
+ raise TypeError(message)
81
+
82
+
83
+ def _check_path(
84
+ path: Path,
85
+ new_old: str,
86
+ make_absolute: bool = False,
87
+ error_if_not_absolute: bool = False,
88
+ ) -> Path:
89
+ if not isinstance(path, Path):
90
+ message = f"{new_old} must be Path. Got {path}"
91
+ raise ValueError(message)
92
+ if make_absolute and not path.is_absolute():
93
+ path = path.resolve()
94
+ if error_if_not_absolute and not path.is_absolute():
95
+ message = f"{new_old} must be an absolute Path. Got {path}"
96
+ raise ValueError(message)
97
+ return path
@@ -0,0 +1,236 @@
1
+ from __future__ import annotations # NB! added for type hint to work
2
+
3
+ from collections import defaultdict
4
+ from typing import TYPE_CHECKING
5
+
6
+ from framcore import Base
7
+ from framcore.components import Component, Flow, Node
8
+ from framcore.utils import get_supported_components
9
+
10
+ if TYPE_CHECKING:
11
+ from framcore import Model
12
+
13
+
14
+ class FlowInfo(Base):
15
+ """Holds info about one or two related Arrows of a Flow."""
16
+
17
+ def __init__(
18
+ self,
19
+ category: str,
20
+ node_out: str | None = None,
21
+ commodity_out: str | None = None,
22
+ node_in: str | None = None,
23
+ commodity_in: str | None = None,
24
+ ) -> None:
25
+ """
26
+ Based on its arrows, we derive properties about a Flow.
27
+
28
+ We use this class to store such info.
29
+ """
30
+ self.category = category
31
+ self.node_out = node_out
32
+ self.commodity_out = commodity_out
33
+ self.node_in = node_in
34
+ self.commodity_in = commodity_in
35
+
36
+
37
+ def _check_type(value: object, expected) -> None: # noqa: ANN001
38
+ assert isinstance(value, expected), f"Expected {expected}. Got {type(value.__name__)}."
39
+
40
+
41
+ def get_node_to_commodity(data: dict[str, object]) -> dict[str, str]:
42
+ """Return dict with commodity (str) for each node id (str) in data."""
43
+ _check_type(data, dict)
44
+
45
+ components = {k: v for k, v in data.items() if isinstance(v, Component)}
46
+ for k in components:
47
+ assert isinstance(k, str), f"Got invalid key {k}"
48
+
49
+ g = get_supported_components(components, (Node, Flow), tuple())
50
+
51
+ out = dict()
52
+ for k, v in g.items():
53
+ if isinstance(v, Node):
54
+ _check_type(k, str)
55
+ out[k] = v.get_commodity()
56
+ return out
57
+
58
+
59
+ def get_flow_infos(flow: Flow, node_to_commodity: dict[str, str]) -> list[FlowInfo]:
60
+ """Get flow infos from analysis of all its arrows."""
61
+ _check_type(flow, Flow)
62
+ _check_type(node_to_commodity, dict)
63
+
64
+ arrows = flow.get_arrows()
65
+
66
+ if len(arrows) == 1:
67
+ arrow = next(iter(arrows))
68
+ node_id = arrow.get_node()
69
+
70
+ if node_id not in node_to_commodity:
71
+ message = f"node_id {node_id} missing from node_to_commodity for flow\n{flow}"
72
+ raise RuntimeError(message)
73
+
74
+ commodity = node_to_commodity[node_id]
75
+ if arrow.is_ingoing():
76
+ info = FlowInfo(
77
+ "direct_in",
78
+ node_in=node_id,
79
+ commodity_in=commodity,
80
+ )
81
+ else:
82
+ info = FlowInfo(
83
+ "direct_out",
84
+ node_out=node_id,
85
+ commodity_out=commodity,
86
+ )
87
+ return [info]
88
+
89
+ seen: set[tuple[str, str]] = set()
90
+ infos: list[FlowInfo] = []
91
+ for x in arrows:
92
+ for y in arrows:
93
+ if x is y:
94
+ continue
95
+
96
+ if x.is_ingoing() != y.is_ingoing():
97
+ arrow_in = x if x.is_ingoing() else y
98
+ arrow_out = x if y.is_ingoing() else y
99
+
100
+ node_in = arrow_in.get_node()
101
+ node_out = arrow_out.get_node()
102
+
103
+ if node_in not in node_to_commodity:
104
+ message = f"node_in {node_in} missing from node_to_commodity for flow\n{flow}"
105
+ raise RuntimeError(message)
106
+
107
+ if node_out not in node_to_commodity:
108
+ message = f"node_out {node_out} missing from node_to_commodity for flow\n{flow}"
109
+ raise RuntimeError(message)
110
+
111
+ commodity_in = node_to_commodity[node_in]
112
+ commodity_out = node_to_commodity[node_out]
113
+
114
+ info = FlowInfo(
115
+ category="transport" if commodity_in == commodity_out else "conversion",
116
+ node_in=node_in,
117
+ commodity_in=commodity_in,
118
+ node_out=node_out,
119
+ commodity_out=commodity_out,
120
+ )
121
+ key = (node_in, node_out)
122
+ if key in seen:
123
+ continue
124
+
125
+ infos.append(info)
126
+ seen.add(key)
127
+
128
+ for arrow in arrows:
129
+ node = arrow.get_node()
130
+ if any(node in [info.node_in, info.node_out] for info in infos):
131
+ continue
132
+ node_id = arrow.get_node()
133
+ commodity = node_to_commodity[node_id]
134
+ if arrow.is_ingoing():
135
+ info = FlowInfo(
136
+ "direct_in",
137
+ node_in=node_id,
138
+ commodity_in=commodity,
139
+ )
140
+ else:
141
+ info = FlowInfo(
142
+ "direct_out",
143
+ node_out=node_id,
144
+ commodity_out=commodity,
145
+ )
146
+ infos.append(info)
147
+
148
+ return infos
149
+
150
+
151
+ def get_component_to_nodes(data: Model | dict[str, object]) -> dict[str, set[str]]:
152
+ """For each str key in data where value is a Comonent find all Node id str in data directly connected to the Component."""
153
+ from framcore import Model # noqa: PLC0415
154
+
155
+ _check_type(data, Model | dict)
156
+
157
+ if isinstance(data, Model):
158
+ data = data.get_data()
159
+
160
+ components = {k: v for k, v in data.items() if isinstance(v, Component)}
161
+ for k in components:
162
+ assert isinstance(k, str), f"Got invalid key {k}"
163
+
164
+ g = get_supported_components(components, (Node, Flow), tuple())
165
+
166
+ nodes = {k: v for k, v in g.items() if isinstance(v, Node)}
167
+ flows = {k: v for k, v in g.items() if isinstance(v, Flow)}
168
+
169
+ domain_nodes = {k: v for k, v in nodes.items() if (k in components) and isinstance(v, Node)}
170
+ assert all(isinstance(v, Node) for v in domain_nodes.values())
171
+
172
+ parent_keys = {v: k for k, v in components.items()}
173
+
174
+ out = defaultdict(set)
175
+ for flow in flows.values():
176
+ parent_key = parent_keys[flow.get_top_parent()]
177
+ for a in flow.get_arrows():
178
+ node_id = a.get_node()
179
+ if node_id in domain_nodes:
180
+ out[parent_key].add(node_id)
181
+
182
+ return out
183
+
184
+
185
+ def get_transports_by_commodity(data: Model | dict[str, object], commodity: str) -> dict[str, tuple[str, str]]:
186
+ """Return dict with key component_id and value (from_node_id, to_node_id) where both nodes belong to given commodity."""
187
+ from framcore import Model # noqa: PLC0415
188
+
189
+ _check_type(data, Model | dict)
190
+ _check_type(commodity, str)
191
+
192
+ if isinstance(data, Model):
193
+ data = data.get_data()
194
+
195
+ components = {k: v for k, v in data.items() if isinstance(v, Component)}
196
+ for k in components:
197
+ assert isinstance(k, str), f"Got invalid key {k}"
198
+
199
+ node_to_commodity = get_node_to_commodity(components)
200
+
201
+ g = get_supported_components(components, (Node, Flow), tuple())
202
+
203
+ flows = {k: v for k, v in g.items() if isinstance(v, Flow)}
204
+
205
+ parent_keys = {v: k for k, v in components.items()}
206
+
207
+ out = dict()
208
+ for flow in flows.values():
209
+ parent_key = parent_keys[flow.get_top_parent()]
210
+ infos = get_flow_infos(flow, node_to_commodity)
211
+ if len(infos) != 1:
212
+ continue
213
+ info = infos[0]
214
+ if info.category != "transport":
215
+ continue
216
+ if info.commodity_in != commodity:
217
+ continue
218
+ out[parent_key] = (info.node_out, info.node_in)
219
+
220
+ return out
221
+
222
+
223
+ def is_transport_by_commodity(flow: Flow, node_to_commodity: dict[str, str], commodity: str) -> bool:
224
+ """Return True if flow is a transport of the given commodity."""
225
+ _check_type(flow, Flow)
226
+ _check_type(node_to_commodity, dict)
227
+ arrows = flow.get_arrows()
228
+ try:
229
+ x, y = tuple(arrows)
230
+ opposite_directions = x.is_ingoing() != y.is_ingoing()
231
+ x_commodity = node_to_commodity[x.get_node()]
232
+ y_commodity = node_to_commodity[y.get_node()]
233
+ correct_commodity = x_commodity == y_commodity == commodity
234
+ return opposite_directions and correct_commodity
235
+ except Exception:
236
+ return False
@@ -0,0 +1,107 @@
1
+ from __future__ import annotations # NB! added for type hint to work
2
+
3
+ from collections import defaultdict
4
+
5
+ from framcore import Model
6
+ from framcore.components import Component, Flow, Node
7
+ from framcore.utils import get_supported_components
8
+
9
+
10
+ # TODO: Finish implementation, test and demo
11
+ def get_storage_subsystems(domain_components: dict[str, Component] | Model) -> dict[str, set[str]]:
12
+ if isinstance(domain_components, Model):
13
+ domain_components = {k: v for k, v in domain_components.get_data() if isinstance(v, Component)}
14
+
15
+ # translate domain_components to graph consisting of just Flow and Node components
16
+ graph: dict[str, Flow | Node] = get_supported_components(
17
+ components=domain_components,
18
+ supported_types=(Node, Flow),
19
+ )
20
+
21
+ abstract_subsystems, __ = get_one_commodity_storage_subsystems(graph, include_boundaries=True)
22
+
23
+ # TODO: Use Component.get_top_level to lift abstract_subsystems back to domain_components
24
+ domain_subsystems = abstract_subsystems
25
+
26
+ return domain_subsystems
27
+
28
+
29
+ def get_one_commodity_storage_subsystems(
30
+ graph: dict[str, Node | Flow],
31
+ include_boundaries: bool,
32
+ ) -> dict[str, tuple[str, set[str], set[str]]]:
33
+ """
34
+ Group all storage subsystems belonging to same commodity.
35
+
36
+ Returns dict[subsystem_id, (domain_commodity, member_component_ids, boundary_domain_commodities)]
37
+
38
+ The boundary_domain_commodities of the output is a set of boundary commodities.
39
+ Some algorithms can only handle one boundary commodity, so this output is useful
40
+ to verify that those conditions apply, and to derive conversion factor unit,
41
+ which need both storage_commodity unit and boundray_commodity unit.
42
+
43
+ If include_boundaries is False only nodes with same commodity as storage_node will
44
+ be included in the subsystem.
45
+ """
46
+ if not all(isinstance(c, Flow | Node) for c in graph.values()):
47
+ invalid = {k: v for k, v in graph.items() if not isinstance(v, Flow | Node)}
48
+ message = f"All values in graph must be Flow or Node objects. Found invalid objects: {invalid}"
49
+ raise ValueError(message)
50
+
51
+ flows: dict[str, Flow] = {k: v for k, v in graph.items() if isinstance(v, Flow)}
52
+ nodes: dict[str, Node] = {k: v for k, v in graph.items() if isinstance(v, Node)}
53
+
54
+ storage_nodes: dict[str, Node] = {k: v for k, v in nodes.items() if v.get_storage()}
55
+
56
+ node_to_flows: dict[str, set[str]] = defaultdict(set)
57
+ flow_to_nodes: dict[str, set[str]] = defaultdict(set)
58
+ for flow_id, flow in flows.items():
59
+ for arrow in flow.get_arrows():
60
+ node_id = arrow.get_node()
61
+ node_to_flows[node_id].add(flow_id)
62
+ flow_to_nodes[flow_id].add(node_id)
63
+
64
+ out = dict()
65
+ allocated: set[str] = set()
66
+ for storage_node_id, storage_node in storage_nodes.items():
67
+ if storage_node_id in allocated:
68
+ continue
69
+
70
+ subsystem_id = storage_node_id
71
+ storage_commodity = storage_node.get_commodity()
72
+
73
+ member_component_ids: set[str] = set()
74
+ boundary_commodities: set[str] = set()
75
+
76
+ visited: set[str] = set()
77
+ remaining: set[str] = set()
78
+
79
+ remaining.add(storage_node_id)
80
+
81
+ while remaining:
82
+ component_id = remaining.pop()
83
+ if component_id in visited:
84
+ continue
85
+
86
+ visited.add(component_id)
87
+
88
+ if component_id in nodes:
89
+ node: Node = nodes[component_id]
90
+ node_commodity = node.get_commodity()
91
+
92
+ if node_commodity == storage_commodity:
93
+ allocated.add(component_id)
94
+ remaining.update(node_to_flows.get(component_id, set()))
95
+ else:
96
+ boundary_commodities.add(node_commodity)
97
+
98
+ if include_boundaries or node_commodity == storage_commodity:
99
+ member_component_ids.add(component_id)
100
+
101
+ else:
102
+ remaining.update(flow_to_nodes.get(component_id, set()))
103
+ member_component_ids.add(component_id)
104
+
105
+ out[subsystem_id] = (storage_commodity, member_component_ids, boundary_commodities)
106
+
107
+ return out
@@ -1,5 +0,0 @@
1
- Metadata-Version: 2.4
2
- Name: fram-core
3
- Version: 0.0.0
4
- Summary: Coming soon...
5
- Author-email: NVE <alro@nve.no>
@@ -1,4 +0,0 @@
1
- fram_core-0.0.0.dist-info/METADATA,sha256=MXr1hQAqr_E4d7YDIhkuSeF-G0jYcTZJKTqr5fwOxPo,114
2
- fram_core-0.0.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
3
- fram_core-0.0.0.dist-info/top_level.txt,sha256=AbpHGcgLb-kRsJGnwFEktk7uzpZOCcBY74-YBdrKVGs,1
4
- fram_core-0.0.0.dist-info/RECORD,,
@@ -1 +0,0 @@
1
-