fram-core 0.0.0__py3-none-any.whl → 0.1.0__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.0.dist-info/METADATA +42 -0
  2. fram_core-0.1.0.dist-info/RECORD +100 -0
  3. {fram_core-0.0.0.dist-info → fram_core-0.1.0.dist-info}/WHEEL +1 -2
  4. fram_core-0.1.0.dist-info/licenses/LICENSE.md +8 -0
  5. framcore/Base.py +161 -0
  6. framcore/Model.py +90 -0
  7. framcore/__init__.py +10 -0
  8. framcore/aggregators/Aggregator.py +172 -0
  9. framcore/aggregators/HydroAggregator.py +849 -0
  10. framcore/aggregators/NodeAggregator.py +530 -0
  11. framcore/aggregators/WindSolarAggregator.py +315 -0
  12. framcore/aggregators/__init__.py +13 -0
  13. framcore/aggregators/_utils.py +184 -0
  14. framcore/attributes/Arrow.py +307 -0
  15. framcore/attributes/ElasticDemand.py +90 -0
  16. framcore/attributes/ReservoirCurve.py +23 -0
  17. framcore/attributes/SoftBound.py +16 -0
  18. framcore/attributes/StartUpCost.py +65 -0
  19. framcore/attributes/Storage.py +158 -0
  20. framcore/attributes/TargetBound.py +16 -0
  21. framcore/attributes/__init__.py +63 -0
  22. framcore/attributes/hydro/HydroBypass.py +49 -0
  23. framcore/attributes/hydro/HydroGenerator.py +100 -0
  24. framcore/attributes/hydro/HydroPump.py +178 -0
  25. framcore/attributes/hydro/HydroReservoir.py +27 -0
  26. framcore/attributes/hydro/__init__.py +13 -0
  27. framcore/attributes/level_profile_attributes.py +911 -0
  28. framcore/components/Component.py +136 -0
  29. framcore/components/Demand.py +144 -0
  30. framcore/components/Flow.py +189 -0
  31. framcore/components/HydroModule.py +371 -0
  32. framcore/components/Node.py +99 -0
  33. framcore/components/Thermal.py +208 -0
  34. framcore/components/Transmission.py +198 -0
  35. framcore/components/_PowerPlant.py +81 -0
  36. framcore/components/__init__.py +22 -0
  37. framcore/components/wind_solar.py +82 -0
  38. framcore/curves/Curve.py +44 -0
  39. framcore/curves/LoadedCurve.py +146 -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 +591 -0
  44. framcore/expressions/__init__.py +30 -0
  45. framcore/expressions/_get_constant_from_expr.py +477 -0
  46. framcore/expressions/_utils.py +73 -0
  47. framcore/expressions/queries.py +416 -0
  48. framcore/expressions/units.py +227 -0
  49. framcore/fingerprints/__init__.py +11 -0
  50. framcore/fingerprints/fingerprint.py +292 -0
  51. framcore/juliamodels/JuliaModel.py +171 -0
  52. framcore/juliamodels/__init__.py +7 -0
  53. framcore/loaders/__init__.py +10 -0
  54. framcore/loaders/loaders.py +405 -0
  55. framcore/metadata/Div.py +73 -0
  56. framcore/metadata/ExprMeta.py +56 -0
  57. framcore/metadata/LevelExprMeta.py +32 -0
  58. framcore/metadata/Member.py +55 -0
  59. framcore/metadata/Meta.py +44 -0
  60. framcore/metadata/__init__.py +15 -0
  61. framcore/populators/Populator.py +108 -0
  62. framcore/populators/__init__.py +7 -0
  63. framcore/querydbs/CacheDB.py +50 -0
  64. framcore/querydbs/ModelDB.py +34 -0
  65. framcore/querydbs/QueryDB.py +45 -0
  66. framcore/querydbs/__init__.py +11 -0
  67. framcore/solvers/Solver.py +63 -0
  68. framcore/solvers/SolverConfig.py +272 -0
  69. framcore/solvers/__init__.py +9 -0
  70. framcore/timeindexes/AverageYearRange.py +27 -0
  71. framcore/timeindexes/ConstantTimeIndex.py +22 -0
  72. framcore/timeindexes/DailyIndex.py +33 -0
  73. framcore/timeindexes/FixedFrequencyTimeIndex.py +814 -0
  74. framcore/timeindexes/HourlyIndex.py +33 -0
  75. framcore/timeindexes/IsoCalendarDay.py +33 -0
  76. framcore/timeindexes/ListTimeIndex.py +277 -0
  77. framcore/timeindexes/ModelYear.py +23 -0
  78. framcore/timeindexes/ModelYears.py +27 -0
  79. framcore/timeindexes/OneYearProfileTimeIndex.py +29 -0
  80. framcore/timeindexes/ProfileTimeIndex.py +43 -0
  81. framcore/timeindexes/SinglePeriodTimeIndex.py +37 -0
  82. framcore/timeindexes/TimeIndex.py +103 -0
  83. framcore/timeindexes/WeeklyIndex.py +33 -0
  84. framcore/timeindexes/__init__.py +36 -0
  85. framcore/timeindexes/_time_vector_operations.py +689 -0
  86. framcore/timevectors/ConstantTimeVector.py +131 -0
  87. framcore/timevectors/LinearTransformTimeVector.py +131 -0
  88. framcore/timevectors/ListTimeVector.py +127 -0
  89. framcore/timevectors/LoadedTimeVector.py +97 -0
  90. framcore/timevectors/ReferencePeriod.py +51 -0
  91. framcore/timevectors/TimeVector.py +108 -0
  92. framcore/timevectors/__init__.py +17 -0
  93. framcore/utils/__init__.py +35 -0
  94. framcore/utils/get_regional_volumes.py +387 -0
  95. framcore/utils/get_supported_components.py +60 -0
  96. framcore/utils/global_energy_equivalent.py +63 -0
  97. framcore/utils/isolate_subnodes.py +172 -0
  98. framcore/utils/loaders.py +97 -0
  99. framcore/utils/node_flow_utils.py +236 -0
  100. framcore/utils/storage_subsystems.py +106 -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,172 @@
1
+ """Demo to show how we can use the core to write some functions we need."""
2
+
3
+ from collections import defaultdict
4
+ from copy import copy
5
+ from time import time
6
+
7
+ from framcore import Model
8
+ from framcore.components import Component, Flow, Node
9
+ from framcore.events import send_debug_event
10
+ from framcore.utils import get_node_to_commodity, get_supported_components, is_transport_by_commodity
11
+
12
+
13
+ def _is_boundary_flow(flow: Flow, nodes: set[str]) -> bool:
14
+ arrows = flow.get_arrows()
15
+ x, y = tuple(arrows) # has len 2
16
+ return int(x.get_node() in nodes) + int(y.get_node() in nodes) == 1
17
+
18
+
19
+ def _is_member(node: Node, meta_key: str, members: set[str]) -> bool:
20
+ meta = node.get_meta(meta_key)
21
+ value = meta.get_value()
22
+ return value in members
23
+
24
+
25
+ def isolate_subnodes(model: Model, commodity: str, meta_key: str, members: list[str]) -> None: # noqa: PLR0915, C901
26
+ """
27
+ For components in model, delete all nodes of commodity except member nodes, and their flows and boundary nodes.
28
+
29
+ - Keep member nodes and all flows between them.
30
+ - Set boundary nodes exogenous and keep boundary flows into or out from member nodes.
31
+ - Delete all other nodes of commodity and all other flows pointing to them.
32
+
33
+ Args:
34
+ model (Model): Model to modify
35
+ commodity (str): Commodity of nodes to consider
36
+ meta_key (str): Meta key to use to identify members
37
+ members (List[str]): List of meta key values identifying member nodes
38
+
39
+ """
40
+ t = time()
41
+
42
+ data = model.get_data()
43
+ counts_before = model.get_content_counts()
44
+
45
+ has_not_converged = True
46
+ num_iterations = 0
47
+
48
+ while has_not_converged:
49
+ num_iterations += 1
50
+
51
+ n_data_before = len(data)
52
+
53
+ # We need copy of components to set _parent None so component becomes top_parent in upcoming code
54
+ components: dict[str, Component] = {k: copy(v) for k, v in data.items() if isinstance(v, Component)}
55
+ for c in components.values():
56
+ c: Component
57
+ c._parent = None # noqa: SLF001
58
+
59
+ node_to_commodity = get_node_to_commodity(components)
60
+
61
+ parent_keys: dict[Component, str] = {v: k for k, v in components.items()}
62
+
63
+ graph: dict[str, Node | Flow] = get_supported_components(components, (Node, Flow), tuple())
64
+
65
+ parent_to_components = defaultdict(set)
66
+ for c in graph.values():
67
+ parent_to_components[c.get_top_parent()].add(c)
68
+
69
+ nodes: dict[str, Node] = {k: v for k, v in graph.items() if isinstance(v, Node)}
70
+ flows: dict[str, Flow] = {k: v for k, v in graph.items() if isinstance(v, Flow)}
71
+
72
+ commodity_nodes: dict[str, Node] = {k: v for k, v in nodes.items() if commodity == v.get_commodity()}
73
+ for k, v in commodity_nodes.items():
74
+ assert v.get_meta(meta_key), f"missing meta_key {meta_key} node_id {k}"
75
+
76
+ inside_nodes: dict[str, Node] = {k: v for k, v in commodity_nodes.items() if _is_member(v, meta_key, members)}
77
+
78
+ transports: dict[str, Flow] = {k: v for k, v in flows.items() if is_transport_by_commodity(v, node_to_commodity, commodity)}
79
+
80
+ boundary_flows: dict[str, Flow] = {k: v for k, v in transports.items() if _is_boundary_flow(v, inside_nodes.keys())}
81
+
82
+ boundary_nodes: dict[str, Node] = dict()
83
+ for flow_id, flow in boundary_flows.items():
84
+ for a in flow.get_arrows():
85
+ node_id = a.get_node()
86
+ if node_id not in inside_nodes:
87
+ boundary_nodes[node_id] = nodes[node_id]
88
+
89
+ outside_nodes: dict[str, Node] = {k: v for k, v in commodity_nodes.items() if not (k in inside_nodes or k in boundary_nodes)}
90
+
91
+ deletes: set[str] = set()
92
+
93
+ deletes.update(outside_nodes.keys())
94
+ deletes.update(boundary_nodes.keys()) # will be kept in delete step below
95
+ deletes.update(boundary_flows.keys()) # will be kept in delete step below
96
+
97
+ # delete flows delivering to deleted node
98
+ for k, flow in flows.items():
99
+ for a in flow.get_arrows():
100
+ if a.get_node() in deletes:
101
+ deletes.add(k)
102
+ break # goto next k, flow
103
+
104
+ # needed for next two steps
105
+ node_to_flows: dict[str, set[str]] = defaultdict(set)
106
+ flow_to_nodes: dict[str, set[str]] = defaultdict(set)
107
+ for flow_id, flow in flows.items():
108
+ for arrow in flow.get_arrows():
109
+ node_id = arrow.get_node()
110
+ node_to_flows[node_id].add(flow_id)
111
+ flow_to_nodes[flow_id].add(node_id)
112
+
113
+ # delete disconnected subgraphs
114
+ remaining = set(n for n in nodes if n not in commodity_nodes)
115
+ while remaining:
116
+ is_disconnected_subgraph = True
117
+ subgraph = set()
118
+ possible_members = set()
119
+ possible_members.add(remaining.pop())
120
+ while possible_members:
121
+ member = possible_members.pop()
122
+ if member in subgraph: # avoid cycle
123
+ continue
124
+ if member in flows:
125
+ subgraph.add(member)
126
+ for node in flow_to_nodes[member]:
127
+ if node not in outside_nodes or node not in boundary_nodes:
128
+ possible_members.add(node)
129
+ if node in inside_nodes:
130
+ is_disconnected_subgraph = False
131
+ else:
132
+ subgraph.add(member)
133
+ for flow in node_to_flows[member]:
134
+ possible_members.add(flow)
135
+ if is_disconnected_subgraph:
136
+ deletes.update(subgraph)
137
+
138
+ for key in deletes:
139
+ if (key in boundary_flows) or (key in boundary_nodes):
140
+ continue
141
+
142
+ if key not in graph:
143
+ continue
144
+
145
+ parent_key = parent_keys[graph[key].get_top_parent()]
146
+
147
+ if parent_key in data:
148
+ del data[parent_key]
149
+
150
+ n_data_after = len(data)
151
+
152
+ if n_data_after == n_data_before:
153
+ has_not_converged = False
154
+
155
+ counts_after = model.get_content_counts()
156
+
157
+ added_components = counts_after["components"] - counts_before["components"]
158
+ if added_components.total() > 0:
159
+ message = f"Expected only deleted components. Got additions {added_components}"
160
+ raise RuntimeError(message)
161
+
162
+ deleted_components = counts_before["components"] - counts_after["components"]
163
+
164
+ for node_id in boundary_nodes:
165
+ if node_id in data:
166
+ node: Node = data[node_id]
167
+ node.set_exogenous()
168
+ if not node.get_price().has_level():
169
+ message = f"{node_id} set to be exogenous, but no price is available."
170
+ raise RuntimeError(message)
171
+
172
+ send_debug_event(isolate_subnodes, f"Used {num_iterations} iterations and {round(time() - t, 2)} seconds and deleted {deleted_components}")
@@ -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
24
+ from framcore.components import Component, Flow, Node
25
+ from framcore.curves import Curve
26
+ from framcore.expressions import Expr
27
+ from framcore.timevectors import TimeVector
28
+ from framcore.utils import get_supported_components
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
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]: # noqa: C901
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 Component find all Node id str in data directly connected to the Component."""
153
+ from framcore import Model
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
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,106 @@
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]]: # noqa: D103
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
+ return abstract_subsystems
25
+
26
+
27
+
28
+ def get_one_commodity_storage_subsystems( # noqa: C901
29
+ graph: dict[str, Node | Flow],
30
+ include_boundaries: bool,
31
+ ) -> dict[str, tuple[str, set[str], set[str]]]:
32
+ """
33
+ Group all storage subsystems belonging to same commodity.
34
+
35
+ Returns dict[subsystem_id, (domain_commodity, member_component_ids, boundary_domain_commodities)]
36
+
37
+ The boundary_domain_commodities of the output is a set of boundary commodities.
38
+ Some algorithms can only handle one boundary commodity, so this output is useful
39
+ to verify that those conditions apply, and to derive conversion factor unit,
40
+ which need both storage_commodity unit and boundray_commodity unit.
41
+
42
+ If include_boundaries is False only nodes with same commodity as storage_node will
43
+ be included in the subsystem.
44
+ """
45
+ if not all(isinstance(c, Flow | Node) for c in graph.values()):
46
+ invalid = {k: v for k, v in graph.items() if not isinstance(v, Flow | Node)}
47
+ message = f"All values in graph must be Flow or Node objects. Found invalid objects: {invalid}"
48
+ raise ValueError(message)
49
+
50
+ flows: dict[str, Flow] = {k: v for k, v in graph.items() if isinstance(v, Flow)}
51
+ nodes: dict[str, Node] = {k: v for k, v in graph.items() if isinstance(v, Node)}
52
+
53
+ storage_nodes: dict[str, Node] = {k: v for k, v in nodes.items() if v.get_storage()}
54
+
55
+ node_to_flows: dict[str, set[str]] = defaultdict(set)
56
+ flow_to_nodes: dict[str, set[str]] = defaultdict(set)
57
+ for flow_id, flow in flows.items():
58
+ for arrow in flow.get_arrows():
59
+ node_id = arrow.get_node()
60
+ node_to_flows[node_id].add(flow_id)
61
+ flow_to_nodes[flow_id].add(node_id)
62
+
63
+ out = dict()
64
+ allocated: set[str] = set()
65
+ for storage_node_id, storage_node in storage_nodes.items():
66
+ if storage_node_id in allocated:
67
+ continue
68
+
69
+ subsystem_id = storage_node_id
70
+ storage_commodity = storage_node.get_commodity()
71
+
72
+ member_component_ids: set[str] = set()
73
+ boundary_commodities: set[str] = set()
74
+
75
+ visited: set[str] = set()
76
+ remaining: set[str] = set()
77
+
78
+ remaining.add(storage_node_id)
79
+
80
+ while remaining:
81
+ component_id = remaining.pop()
82
+ if component_id in visited:
83
+ continue
84
+
85
+ visited.add(component_id)
86
+
87
+ if component_id in nodes:
88
+ node: Node = nodes[component_id]
89
+ node_commodity = node.get_commodity()
90
+
91
+ if node_commodity == storage_commodity:
92
+ allocated.add(component_id)
93
+ remaining.update(node_to_flows.get(component_id, set()))
94
+ else:
95
+ boundary_commodities.add(node_commodity)
96
+
97
+ if include_boundaries or node_commodity == storage_commodity:
98
+ member_component_ids.add(component_id)
99
+
100
+ else:
101
+ remaining.update(flow_to_nodes.get(component_id, set()))
102
+ member_component_ids.add(component_id)
103
+
104
+ out[subsystem_id] = (storage_commodity, member_component_ids, boundary_commodities)
105
+
106
+ 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
-