power-grid-model-ds 0.0.1a11709467271__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 (64) hide show
  1. power_grid_model_ds/__init__.py +9 -0
  2. power_grid_model_ds/_core/__init__.py +0 -0
  3. power_grid_model_ds/_core/data_source/__init__.py +0 -0
  4. power_grid_model_ds/_core/data_source/generator/__init__.py +0 -0
  5. power_grid_model_ds/_core/data_source/generator/arrays/__init__.py +0 -0
  6. power_grid_model_ds/_core/data_source/generator/arrays/base.py +25 -0
  7. power_grid_model_ds/_core/data_source/generator/arrays/line.py +133 -0
  8. power_grid_model_ds/_core/data_source/generator/arrays/node.py +37 -0
  9. power_grid_model_ds/_core/data_source/generator/arrays/source.py +30 -0
  10. power_grid_model_ds/_core/data_source/generator/arrays/transformer.py +37 -0
  11. power_grid_model_ds/_core/data_source/generator/grid_generators.py +78 -0
  12. power_grid_model_ds/_core/fancypy.py +66 -0
  13. power_grid_model_ds/_core/load_flow.py +140 -0
  14. power_grid_model_ds/_core/model/__init__.py +0 -0
  15. power_grid_model_ds/_core/model/arrays/__init__.py +43 -0
  16. power_grid_model_ds/_core/model/arrays/base/__init__.py +0 -0
  17. power_grid_model_ds/_core/model/arrays/base/_build.py +166 -0
  18. power_grid_model_ds/_core/model/arrays/base/_filters.py +115 -0
  19. power_grid_model_ds/_core/model/arrays/base/_modify.py +64 -0
  20. power_grid_model_ds/_core/model/arrays/base/_optional.py +11 -0
  21. power_grid_model_ds/_core/model/arrays/base/_string.py +94 -0
  22. power_grid_model_ds/_core/model/arrays/base/array.py +325 -0
  23. power_grid_model_ds/_core/model/arrays/base/errors.py +17 -0
  24. power_grid_model_ds/_core/model/arrays/pgm_arrays.py +122 -0
  25. power_grid_model_ds/_core/model/constants.py +27 -0
  26. power_grid_model_ds/_core/model/containers/__init__.py +0 -0
  27. power_grid_model_ds/_core/model/containers/base.py +244 -0
  28. power_grid_model_ds/_core/model/containers/grid_protocol.py +22 -0
  29. power_grid_model_ds/_core/model/dtypes/__init__.py +0 -0
  30. power_grid_model_ds/_core/model/dtypes/appliances.py +39 -0
  31. power_grid_model_ds/_core/model/dtypes/branches.py +117 -0
  32. power_grid_model_ds/_core/model/dtypes/id.py +19 -0
  33. power_grid_model_ds/_core/model/dtypes/nodes.py +27 -0
  34. power_grid_model_ds/_core/model/dtypes/regulators.py +30 -0
  35. power_grid_model_ds/_core/model/dtypes/sensors.py +63 -0
  36. power_grid_model_ds/_core/model/enums/__init__.py +0 -0
  37. power_grid_model_ds/_core/model/enums/nodes.py +16 -0
  38. power_grid_model_ds/_core/model/graphs/__init__.py +0 -0
  39. power_grid_model_ds/_core/model/graphs/container.py +158 -0
  40. power_grid_model_ds/_core/model/graphs/errors.py +19 -0
  41. power_grid_model_ds/_core/model/graphs/models/__init__.py +7 -0
  42. power_grid_model_ds/_core/model/graphs/models/_rustworkx_search.py +63 -0
  43. power_grid_model_ds/_core/model/graphs/models/base.py +326 -0
  44. power_grid_model_ds/_core/model/graphs/models/rustworkx.py +119 -0
  45. power_grid_model_ds/_core/model/grids/__init__.py +0 -0
  46. power_grid_model_ds/_core/model/grids/_text_sources.py +119 -0
  47. power_grid_model_ds/_core/model/grids/base.py +434 -0
  48. power_grid_model_ds/_core/model/grids/helpers.py +122 -0
  49. power_grid_model_ds/_core/utils/__init__.py +0 -0
  50. power_grid_model_ds/_core/utils/misc.py +41 -0
  51. power_grid_model_ds/_core/utils/pickle.py +47 -0
  52. power_grid_model_ds/_core/utils/zip.py +72 -0
  53. power_grid_model_ds/arrays.py +39 -0
  54. power_grid_model_ds/constants.py +7 -0
  55. power_grid_model_ds/enums.py +7 -0
  56. power_grid_model_ds/errors.py +27 -0
  57. power_grid_model_ds/fancypy.py +9 -0
  58. power_grid_model_ds/generators.py +11 -0
  59. power_grid_model_ds/graph_models.py +8 -0
  60. power_grid_model_ds-0.0.1a11709467271.dist-info/LICENSE +292 -0
  61. power_grid_model_ds-0.0.1a11709467271.dist-info/METADATA +80 -0
  62. power_grid_model_ds-0.0.1a11709467271.dist-info/RECORD +64 -0
  63. power_grid_model_ds-0.0.1a11709467271.dist-info/WHEEL +5 -0
  64. power_grid_model_ds-0.0.1a11709467271.dist-info/top_level.txt +1 -0
@@ -0,0 +1,158 @@
1
+ # SPDX-FileCopyrightText: Contributors to the Power Grid Model project <powergridmodel@lfenergy.org>
2
+ #
3
+ # SPDX-License-Identifier: MPL-2.0
4
+
5
+ """Stores the GraphContainer class"""
6
+
7
+ import dataclasses
8
+ from dataclasses import dataclass
9
+ from pathlib import PosixPath
10
+ from typing import Generator
11
+
12
+ import numpy as np
13
+
14
+ from power_grid_model_ds._core.model.arrays import Branch3Array, BranchArray, NodeArray
15
+ from power_grid_model_ds._core.model.arrays.base.array import FancyArray
16
+ from power_grid_model_ds._core.model.arrays.base.errors import RecordDoesNotExist
17
+ from power_grid_model_ds._core.model.containers.grid_protocol import MinimalGridArrays
18
+ from power_grid_model_ds._core.model.graphs.models import RustworkxGraphModel
19
+ from power_grid_model_ds._core.model.graphs.models.base import BaseGraphModel
20
+
21
+
22
+ @dataclass
23
+ class GraphContainer:
24
+ """Contains graphs"""
25
+
26
+ active_graph: BaseGraphModel
27
+ """The graph containing only active branches."""
28
+
29
+ complete_graph: BaseGraphModel
30
+ """The graph containing all branches."""
31
+
32
+ @property
33
+ def graph_attributes(self) -> Generator:
34
+ """Get all graph attributes of the container.
35
+
36
+ Yield:
37
+ BaseGraphModel: The next graph attribute.
38
+ """
39
+ return (field for field in dataclasses.fields(self) if isinstance(getattr(self, field.name), BaseGraphModel))
40
+
41
+ @classmethod
42
+ def empty(cls, graph_model: type[BaseGraphModel] = RustworkxGraphModel) -> "GraphContainer":
43
+ """Get empty instance of GraphContainer.
44
+
45
+ Args:
46
+ graph_model (type[BaseGraphModel]): The graph model to use. Defaults to RustworkxGraphModel.
47
+ An alternative graph model can be passed as an argument.
48
+
49
+ Returns:
50
+ GraphContainer: The empty graph container.
51
+ """
52
+
53
+ return cls(
54
+ active_graph=graph_model(active_only=True),
55
+ complete_graph=graph_model(active_only=False),
56
+ )
57
+
58
+ def add_branch(self, branch: BranchArray) -> None:
59
+ """Add a branch to all graphs"""
60
+ for field in self.graph_attributes:
61
+ graph = getattr(self, field.name)
62
+ graph.add_branch_array(branch_array=branch)
63
+ setattr(self, field.name, graph)
64
+
65
+ def add_branch3(self, branch: Branch3Array) -> None:
66
+ """Add a branch to all graphs"""
67
+ for field in self.graph_attributes:
68
+ graph = getattr(self, field.name)
69
+ graph.add_branch3_array(branch3_array=branch)
70
+ setattr(self, field.name, graph)
71
+
72
+ def delete_branch(self, branch: BranchArray) -> None:
73
+ """Remove a branch from all graphs"""
74
+ for field in self.graph_attributes:
75
+ graph = getattr(self, field.name)
76
+ graph.delete_branch_array(branch_array=branch)
77
+ setattr(self, field.name, graph)
78
+
79
+ def delete_branch3(self, branch: Branch3Array) -> None:
80
+ """Remove a branch from all graphs"""
81
+ for field in self.graph_attributes:
82
+ graph = getattr(self, field.name)
83
+ graph.delete_branch3_array(branch3_array=branch)
84
+ setattr(self, field.name, graph)
85
+
86
+ def add_node(self, node: NodeArray) -> None:
87
+ """Add a node to all graphs"""
88
+ for field in dataclasses.fields(self):
89
+ graph = getattr(self, field.name)
90
+ graph.add_node_array(node_array=node, raise_on_fail=False)
91
+ setattr(self, field.name, graph)
92
+
93
+ def delete_node(self, node: NodeArray) -> None:
94
+ """Remove a node from all graphs"""
95
+ for field in dataclasses.fields(self):
96
+ graph = getattr(self, field.name)
97
+ graph.delete_node_array(node_array=node)
98
+ setattr(self, field.name, graph)
99
+
100
+ def make_active(self, branch: BranchArray) -> None:
101
+ """Add branch to all active_only graphs"""
102
+
103
+ from_node = branch.from_node.item()
104
+ to_node = branch.to_node.item()
105
+ for field in dataclasses.fields(self):
106
+ graph = getattr(self, field.name)
107
+ if graph.active_only:
108
+ graph.add_branch(from_ext_node_id=from_node, to_ext_node_id=to_node)
109
+ setattr(self, field.name, graph)
110
+
111
+ def make_inactive(self, branch: BranchArray) -> None:
112
+ """Remove a branch from all active_only graphs"""
113
+
114
+ from_node = branch.from_node.item()
115
+ to_node = branch.to_node.item()
116
+ for field in dataclasses.fields(self):
117
+ graph = getattr(self, field.name)
118
+ if graph.active_only:
119
+ graph.delete_branch(from_ext_node_id=from_node, to_ext_node_id=to_node)
120
+ setattr(self, field.name, graph)
121
+
122
+ def cache(self, cache_dir: PosixPath, compress: bool) -> PosixPath:
123
+ """Cache the container into a folder with .pkl and graph files"""
124
+ cache_dir.mkdir(parents=True, exist_ok=True)
125
+
126
+ for field in self.graph_attributes:
127
+ graph = getattr(self, field.name)
128
+ graph.cache(cache_dir=cache_dir, graph_name=field.name, compress=compress)
129
+ return cache_dir
130
+
131
+ @classmethod
132
+ def from_arrays(cls, arrays: MinimalGridArrays) -> "GraphContainer":
133
+ """Build from arrays"""
134
+ cls._validate_branches(arrays=arrays)
135
+
136
+ new_container = cls.empty()
137
+ for graph_field in new_container.graph_attributes:
138
+ graph = getattr(new_container, graph_field.name)
139
+ new_graph = graph.from_arrays(arrays, active_only=graph.active_only)
140
+ setattr(new_container, graph_field.name, new_graph)
141
+
142
+ return new_container
143
+
144
+ @staticmethod
145
+ def _validate_branches(arrays: MinimalGridArrays):
146
+ for array in arrays.branch_arrays:
147
+ if any(~np.isin(array.from_node, arrays.node.id)):
148
+ raise RecordDoesNotExist(f"Found invalid .from_node values in {array.__class__.__name__}")
149
+ if any(~np.isin(array.to_node, arrays.node.id)):
150
+ raise RecordDoesNotExist(f"Found invalid .to_node values in {array.__class__.__name__}")
151
+
152
+ def _append(self, array: FancyArray) -> None:
153
+ if isinstance(array, BranchArray):
154
+ self.add_branch(array)
155
+ if isinstance(array, Branch3Array):
156
+ self.add_branch3(array)
157
+ if isinstance(array, NodeArray):
158
+ self.add_node(array)
@@ -0,0 +1,19 @@
1
+ # SPDX-FileCopyrightText: Contributors to the Power Grid Model project <powergridmodel@lfenergy.org>
2
+ #
3
+ # SPDX-License-Identifier: MPL-2.0
4
+
5
+
6
+ class GraphError(Exception):
7
+ """Raised when there is an error in the graph"""
8
+
9
+
10
+ class MissingNodeError(GraphError):
11
+ """Raised when a node is missing"""
12
+
13
+
14
+ class MissingBranchError(GraphError):
15
+ """Raised when a branch is missing"""
16
+
17
+
18
+ class NoPathBetweenNodes(GraphError):
19
+ """Raised when there is no path between two nodes"""
@@ -0,0 +1,7 @@
1
+ # SPDX-FileCopyrightText: Contributors to the Power Grid Model project <powergridmodel@lfenergy.org>
2
+ #
3
+ # SPDX-License-Identifier: MPL-2.0
4
+
5
+ from power_grid_model_ds._core.model.graphs.models.rustworkx import RustworkxGraphModel
6
+
7
+ __all__ = ["RustworkxGraphModel"]
@@ -0,0 +1,63 @@
1
+ # SPDX-FileCopyrightText: Contributors to the Power Grid Model project <powergridmodel@lfenergy.org>
2
+ #
3
+ # SPDX-License-Identifier: MPL-2.0
4
+
5
+ from collections import Counter
6
+
7
+ import rustworkx as rx
8
+
9
+
10
+ def find_fundamental_cycles_rustworkx(graph):
11
+ """Detect fundamental cycles in the graph and returns the node cycle paths.
12
+
13
+ The nodes in cycles are found by:
14
+ 1. Creating a minimum spanning forest (MSF) of the (possibly disconnected) graph, which is a set of minimum
15
+ spanning trees (MST's). Here, a MST is a subset of the original graph that connects all nodes with the minimum
16
+ possible amount of edges. (Also, see https://mathworld.wolfram.com/SpanningTree.html).
17
+
18
+ For a connected graph, all nodes are connected by edges, leading to a single MST. In that case,
19
+ the MSF consists of a single MST. (See https://mathworld.wolfram.com/ConnectedGraph.html for more information).
20
+
21
+ For a disconnected graph, there are multiple separated subgraphs; in other words, not all nodes are connected.
22
+ (ALso, see https://mathworld.wolfram.com/DisconnectedGraph.html). Each of the subgraphs will then form a MST.
23
+ The MSF is then the collection of these MST's.
24
+
25
+ NOTE: If there are cycles in the graph, the MSF is not unique. One of the options will be chosen.
26
+ Regardless of the chosen MSF, the algorithm works.
27
+
28
+ 2. Determining which edges in the original graph are not part of the MSF. A propery of MST's is that adding
29
+ a single edge between nodes will always form a cycle.
30
+
31
+ 3. Looping through the unused edges and determining the path of nodes of the cycle it would form.
32
+ This is done by determining the shortest path between the nodes on both sides of an unused edge within the MSF.
33
+ A combination of this shortest path and the unused edge forms the cycle path. A list of these paths is returned.
34
+
35
+ Returns:
36
+ node_cycle_paths(list[list[[int]]): a list of node paths, which are each a list of node_ids in a path.
37
+ """
38
+ mst = rx.minimum_spanning_tree(graph)
39
+ unused_edges = _find_unused_edges_rustworkx(graph, mst)
40
+ node_cycle_paths = _get_cycle_paths_rustworkx(unused_edges, mst)
41
+
42
+ return node_cycle_paths
43
+
44
+
45
+ def _find_unused_edges_rustworkx(full_graph, subset_graph):
46
+ """Determine unused edges by comparing all edges with a subset of edges in the MST."""
47
+ full_edges = Counter(full_graph.edge_list())
48
+ subset_edges = Counter(subset_graph.edge_list())
49
+ unused_edges = full_edges - subset_edges
50
+ return unused_edges
51
+
52
+
53
+ def _get_cycle_paths_rustworkx(unused_edges, spanning_forest_graph):
54
+ """Find nodes that are part of a cycle in the graph using the MST."""
55
+ node_cycle_paths = []
56
+ for source, target in unused_edges:
57
+ path_mapping = rx.dijkstra_shortest_paths(spanning_forest_graph, source, target, weight_fn=lambda x: 1)
58
+
59
+ path_nodes = list(path_mapping[target])
60
+ path_nodes.append(source)
61
+
62
+ node_cycle_paths.append(path_nodes)
63
+ return node_cycle_paths
@@ -0,0 +1,326 @@
1
+ # SPDX-FileCopyrightText: Contributors to the Power Grid Model project <powergridmodel@lfenergy.org>
2
+ #
3
+ # SPDX-License-Identifier: MPL-2.0
4
+
5
+ from abc import ABC, abstractmethod
6
+
7
+ import numpy as np
8
+ from numpy._typing import NDArray
9
+
10
+ from power_grid_model_ds._core.model.arrays.pgm_arrays import Branch3Array, BranchArray, NodeArray
11
+ from power_grid_model_ds._core.model.containers.grid_protocol import MinimalGridArrays
12
+ from power_grid_model_ds._core.model.graphs.errors import (
13
+ GraphError,
14
+ MissingBranchError,
15
+ MissingNodeError,
16
+ NoPathBetweenNodes,
17
+ )
18
+
19
+
20
+ # pylint: disable=too-many-public-methods
21
+ class BaseGraphModel(ABC):
22
+ """Base class for graph models"""
23
+
24
+ def __init__(self, active_only=False) -> None:
25
+ self.active_only = active_only
26
+
27
+ @property
28
+ @abstractmethod
29
+ def nr_nodes(self):
30
+ """Returns the number of nodes in the graph"""
31
+
32
+ @property
33
+ @abstractmethod
34
+ def nr_branches(self):
35
+ """Returns the number of branches in the graph"""
36
+
37
+ @abstractmethod
38
+ def external_to_internal(self, ext_node_id: int) -> int:
39
+ """Convert external node id to internal node id (internal)
40
+
41
+ Raises:
42
+ MissingNodeError: if the external node id does not exist in the graph
43
+ """
44
+
45
+ @abstractmethod
46
+ def internal_to_external(self, int_node_id: int) -> int:
47
+ """Convert internal id (internal) to external node id"""
48
+
49
+ @property
50
+ @abstractmethod
51
+ def external_ids(self) -> list[int]:
52
+ """Return all external node ids
53
+
54
+ Warning: Depending on graph engine, performance could be slow for large graphs
55
+ """
56
+
57
+ def has_node(self, node_id: int) -> bool:
58
+ """Check if a node exists."""
59
+ try:
60
+ internal_node_id = self.external_to_internal(ext_node_id=node_id)
61
+ except MissingNodeError:
62
+ return False
63
+
64
+ return self._has_node(node_id=internal_node_id)
65
+
66
+ def add_node(self, ext_node_id: int, raise_on_fail: bool = True) -> None:
67
+ """Add a node to the graph."""
68
+ if self.has_node(ext_node_id):
69
+ if raise_on_fail:
70
+ raise GraphError(f"External node id '{ext_node_id}' already exists!")
71
+ return
72
+
73
+ self._add_node(ext_node_id)
74
+
75
+ def delete_node(self, ext_node_id: int, raise_on_fail: bool = True) -> None:
76
+ """Remove a node from the graph.
77
+
78
+ Args:
79
+ ext_node_id(int): id of the node to remove
80
+ raise_on_fail(bool): whether to raise an error if the node does not exist. Defaults to True
81
+
82
+ Raises:
83
+ MissingNodeError: if the node does not exist in the graph and ``raise_on_fail=True``
84
+ """
85
+ try:
86
+ internal_node_id = self.external_to_internal(ext_node_id)
87
+ except MissingNodeError as error:
88
+ if raise_on_fail:
89
+ raise error
90
+ return
91
+
92
+ self._delete_node(node_id=internal_node_id)
93
+
94
+ def add_node_array(self, node_array: NodeArray, raise_on_fail: bool = True) -> None:
95
+ """Add all nodes in the node array to the graph."""
96
+ for node in node_array:
97
+ self.add_node(ext_node_id=node.id.item(), raise_on_fail=raise_on_fail)
98
+
99
+ def delete_node_array(self, node_array: NodeArray, raise_on_fail: bool = True) -> None:
100
+ """Delete all nodes in node_array from the graph"""
101
+ for node in node_array:
102
+ self.delete_node(node.id.item(), raise_on_fail=raise_on_fail)
103
+
104
+ def has_branch(self, from_ext_node_id: int, to_ext_node_id: int) -> bool:
105
+ """Check if a branch exists between two nodes."""
106
+ try:
107
+ int_from_node_id = self.external_to_internal(from_ext_node_id)
108
+ int_to_node_id = self.external_to_internal(to_ext_node_id)
109
+ except MissingNodeError:
110
+ return False
111
+
112
+ return self._has_branch(from_node_id=int_from_node_id, to_node_id=int_to_node_id)
113
+
114
+ def add_branch(self, from_ext_node_id: int, to_ext_node_id: int) -> None:
115
+ """Add a new branch to the graph."""
116
+ self._add_branch(
117
+ from_node_id=self.external_to_internal(from_ext_node_id),
118
+ to_node_id=self.external_to_internal(to_ext_node_id),
119
+ )
120
+
121
+ def delete_branch(self, from_ext_node_id: int, to_ext_node_id: int, raise_on_fail: bool = True) -> None:
122
+ """Remove an existing branch from the graph.
123
+
124
+ Args:
125
+ from_ext_node_id: id of the from node
126
+ to_ext_node_id: id of the to node
127
+ raise_on_fail: whether to raise an error if the branch does not exist
128
+
129
+ Raises:
130
+ MissingBranchError: if branch does not exist in the graph and ``raise_on_fail=True``
131
+ """
132
+ try:
133
+ self._delete_branch(
134
+ from_node_id=self.external_to_internal(from_ext_node_id),
135
+ to_node_id=self.external_to_internal(to_ext_node_id),
136
+ )
137
+ except (MissingNodeError, MissingBranchError) as error:
138
+ if raise_on_fail:
139
+ raise MissingBranchError(
140
+ f"Branch between nodes {from_ext_node_id} and {to_ext_node_id} does NOT exist!"
141
+ ) from error
142
+
143
+ def add_branch_array(self, branch_array: BranchArray) -> None:
144
+ """Add all branches in the branch array to the graph."""
145
+ for branch in branch_array:
146
+ if self._branch_is_relevant(branch):
147
+ self.add_branch(branch.from_node.item(), branch.to_node.item())
148
+
149
+ def add_branch3_array(self, branch3_array: Branch3Array) -> None:
150
+ """Add all branch3s in the branch3 array to the graph."""
151
+ for branch3 in branch3_array:
152
+ branches = _get_branch3_branches(branch3)
153
+ self.add_branch_array(branches)
154
+
155
+ def delete_branch_array(self, branch_array: BranchArray, raise_on_fail: bool = True) -> None:
156
+ """Delete all branches in branch_array from the graph."""
157
+ for branch in branch_array:
158
+ if self._branch_is_relevant(branch):
159
+ self.delete_branch(branch.from_node.item(), branch.to_node.item(), raise_on_fail=raise_on_fail)
160
+
161
+ def delete_branch3_array(self, branch_array: Branch3Array, raise_on_fail: bool = True) -> None:
162
+ """Delete all branch3s in the branch3 array from the graph."""
163
+ for branch3 in branch_array:
164
+ branches = _get_branch3_branches(branch3)
165
+ self.delete_branch_array(branches, raise_on_fail=raise_on_fail)
166
+
167
+ def get_shortest_path(self, ext_start_node_id: int, ext_end_node_id: int) -> tuple[list[int], int]:
168
+ """Calculate the shortest path between two nodes
169
+
170
+ Example:
171
+ given this graph: [1] - [2] - [3] - [4]
172
+
173
+ >>> graph.get_shortest_path(1, 4) == [1, 2, 3, 4], 3
174
+ >>> graph.get_shortest_path(1, 1) == [1], 0
175
+
176
+ Returns:
177
+ tuple[list[int], int]: a tuple where the first element is a list of external nodes from start to end.
178
+ The second element is the distance of the path in number of edges.
179
+
180
+ Raises:
181
+ NoPathBetweenNodes: if no path exists between the given nodes
182
+ """
183
+ if ext_start_node_id == ext_end_node_id:
184
+ return [ext_start_node_id], 0
185
+
186
+ try:
187
+ internal_path, distance = self._get_shortest_path(
188
+ source=self.external_to_internal(ext_start_node_id), target=self.external_to_internal(ext_end_node_id)
189
+ )
190
+ return self._internals_to_externals(internal_path), distance
191
+ except NoPathBetweenNodes as e:
192
+ raise NoPathBetweenNodes(f"No path between nodes {ext_start_node_id} and {ext_end_node_id}") from e
193
+
194
+ def get_all_paths(self, ext_start_node_id: int, ext_end_node_id: int) -> list[list[int]]:
195
+ """Retrieves all paths between two (external) nodes.
196
+ Returns a list of paths, each path containing a list of external nodes.
197
+ """
198
+ if ext_start_node_id == ext_end_node_id:
199
+ return []
200
+
201
+ internal_paths = self._get_all_paths(
202
+ source=self.external_to_internal(ext_start_node_id),
203
+ target=self.external_to_internal(ext_end_node_id),
204
+ )
205
+
206
+ if internal_paths == []:
207
+ raise NoPathBetweenNodes(f"No path between nodes {ext_start_node_id} and {ext_end_node_id}")
208
+
209
+ return [self._internals_to_externals(path) for path in internal_paths]
210
+
211
+ def get_components(self, substation_nodes: NDArray[np.int32]) -> list[list[int]]:
212
+ """Returns all separate components when the substation_nodes are removed of the graph as lists"""
213
+ internal_components = self._get_components(substation_nodes=self._externals_to_internals(substation_nodes))
214
+ return [self._internals_to_externals(component) for component in internal_components]
215
+
216
+ def get_connected(
217
+ self, node_id: int, nodes_to_ignore: list[int] | None = None, inclusive: bool = False
218
+ ) -> list[int]:
219
+ """Find all nodes connected to the node_id
220
+
221
+ Args:
222
+ node_id: node id to start the search from
223
+ inclusive: whether to include the given node id in the result
224
+ nodes_to_ignore: list of node ids to ignore while traversing the graph.
225
+ Any nodes connected to `node_id` (solely) through these nodes will
226
+ not be included in the result
227
+ Returns:
228
+ nodes: list of node ids sorted by distance, connected to the node id
229
+ """
230
+ if nodes_to_ignore is None:
231
+ nodes_to_ignore = []
232
+
233
+ nodes = self._get_connected(
234
+ node_id=self.external_to_internal(node_id),
235
+ nodes_to_ignore=self._externals_to_internals(nodes_to_ignore),
236
+ inclusive=inclusive,
237
+ )
238
+ return self._internals_to_externals(nodes)
239
+
240
+ def find_fundamental_cycles(self) -> list[list[int]]:
241
+ """Find all fundamental cycles in the graph.
242
+ Returns:
243
+ list[list[int]]: list of cycles, each cycle is a list of (external) node ids
244
+ """
245
+ internal_cycles = self._find_fundamental_cycles()
246
+ return [self._internals_to_externals(nodes) for nodes in internal_cycles]
247
+
248
+ @classmethod
249
+ def from_arrays(cls, arrays: MinimalGridArrays, active_only=False) -> "BaseGraphModel":
250
+ """Build from arrays"""
251
+ new_graph = cls(active_only=active_only)
252
+
253
+ new_graph.add_node_array(node_array=arrays.node)
254
+ new_graph.add_branch_array(arrays.branches)
255
+ new_graph.add_branch3_array(arrays.three_winding_transformer)
256
+
257
+ return new_graph
258
+
259
+ def _internals_to_externals(self, internal_nodes: list[int]) -> list[int]:
260
+ """Convert a list of internal nodes to external nodes"""
261
+ return [self.internal_to_external(node_id) for node_id in internal_nodes]
262
+
263
+ def _externals_to_internals(self, external_nodes: list[int] | NDArray) -> list[int]:
264
+ """Convert a list of external nodes to internal nodes"""
265
+ return [self.external_to_internal(node_id) for node_id in external_nodes]
266
+
267
+ def _branch_is_relevant(self, branch: BranchArray) -> bool:
268
+ """Check if a branch is relevant"""
269
+ if self.active_only:
270
+ return branch.is_active.item()
271
+ return True
272
+
273
+ @abstractmethod
274
+ def _get_connected(self, node_id: int, nodes_to_ignore: list[int], inclusive: bool = False) -> list[int]: ...
275
+
276
+ @abstractmethod
277
+ def _has_branch(self, from_node_id, to_node_id) -> bool: ...
278
+
279
+ @abstractmethod
280
+ def _has_node(self, node_id) -> bool: ...
281
+
282
+ @abstractmethod
283
+ def _add_node(self, ext_node_id: int) -> None: ...
284
+
285
+ @abstractmethod
286
+ def _delete_node(self, node_id: int): ...
287
+
288
+ @abstractmethod
289
+ def _add_branch(self, from_node_id, to_node_id) -> None: ...
290
+
291
+ @abstractmethod
292
+ def _delete_branch(self, from_node_id, to_node_id) -> None:
293
+ """
294
+ Raises:
295
+ MissingBranchError: if the branch does not exist
296
+ """
297
+
298
+ @abstractmethod
299
+ def _get_shortest_path(self, source, target): ...
300
+
301
+ @abstractmethod
302
+ def _get_all_paths(self, source, target) -> list[list[int]]: ...
303
+
304
+ @abstractmethod
305
+ def _get_components(self, substation_nodes: list[int]) -> list[list[int]]: ...
306
+
307
+ @abstractmethod
308
+ def _find_fundamental_cycles(self) -> list[list[int]]: ...
309
+
310
+
311
+ def _get_branch3_branches(branch3: Branch3Array) -> BranchArray:
312
+ node_1 = branch3.node_1.item()
313
+ node_2 = branch3.node_2.item()
314
+ node_3 = branch3.node_3.item()
315
+
316
+ status_1 = branch3.status_1.item()
317
+ status_2 = branch3.status_2.item()
318
+ status_3 = branch3.status_3.item()
319
+
320
+ branches = BranchArray.zeros(3)
321
+ branches.from_node = [node_1, node_1, node_2]
322
+ branches.to_node = [node_2, node_3, node_3]
323
+ branches.from_status = [status_1, status_1, status_2]
324
+ branches.to_status = [status_2, status_3, status_3]
325
+
326
+ return branches