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.
- power_grid_model_ds/__init__.py +9 -0
- power_grid_model_ds/_core/__init__.py +0 -0
- power_grid_model_ds/_core/data_source/__init__.py +0 -0
- power_grid_model_ds/_core/data_source/generator/__init__.py +0 -0
- power_grid_model_ds/_core/data_source/generator/arrays/__init__.py +0 -0
- power_grid_model_ds/_core/data_source/generator/arrays/base.py +25 -0
- power_grid_model_ds/_core/data_source/generator/arrays/line.py +133 -0
- power_grid_model_ds/_core/data_source/generator/arrays/node.py +37 -0
- power_grid_model_ds/_core/data_source/generator/arrays/source.py +30 -0
- power_grid_model_ds/_core/data_source/generator/arrays/transformer.py +37 -0
- power_grid_model_ds/_core/data_source/generator/grid_generators.py +78 -0
- power_grid_model_ds/_core/fancypy.py +66 -0
- power_grid_model_ds/_core/load_flow.py +140 -0
- power_grid_model_ds/_core/model/__init__.py +0 -0
- power_grid_model_ds/_core/model/arrays/__init__.py +43 -0
- power_grid_model_ds/_core/model/arrays/base/__init__.py +0 -0
- power_grid_model_ds/_core/model/arrays/base/_build.py +166 -0
- power_grid_model_ds/_core/model/arrays/base/_filters.py +115 -0
- power_grid_model_ds/_core/model/arrays/base/_modify.py +64 -0
- power_grid_model_ds/_core/model/arrays/base/_optional.py +11 -0
- power_grid_model_ds/_core/model/arrays/base/_string.py +94 -0
- power_grid_model_ds/_core/model/arrays/base/array.py +325 -0
- power_grid_model_ds/_core/model/arrays/base/errors.py +17 -0
- power_grid_model_ds/_core/model/arrays/pgm_arrays.py +122 -0
- power_grid_model_ds/_core/model/constants.py +27 -0
- power_grid_model_ds/_core/model/containers/__init__.py +0 -0
- power_grid_model_ds/_core/model/containers/base.py +244 -0
- power_grid_model_ds/_core/model/containers/grid_protocol.py +22 -0
- power_grid_model_ds/_core/model/dtypes/__init__.py +0 -0
- power_grid_model_ds/_core/model/dtypes/appliances.py +39 -0
- power_grid_model_ds/_core/model/dtypes/branches.py +117 -0
- power_grid_model_ds/_core/model/dtypes/id.py +19 -0
- power_grid_model_ds/_core/model/dtypes/nodes.py +27 -0
- power_grid_model_ds/_core/model/dtypes/regulators.py +30 -0
- power_grid_model_ds/_core/model/dtypes/sensors.py +63 -0
- power_grid_model_ds/_core/model/enums/__init__.py +0 -0
- power_grid_model_ds/_core/model/enums/nodes.py +16 -0
- power_grid_model_ds/_core/model/graphs/__init__.py +0 -0
- power_grid_model_ds/_core/model/graphs/container.py +158 -0
- power_grid_model_ds/_core/model/graphs/errors.py +19 -0
- power_grid_model_ds/_core/model/graphs/models/__init__.py +7 -0
- power_grid_model_ds/_core/model/graphs/models/_rustworkx_search.py +63 -0
- power_grid_model_ds/_core/model/graphs/models/base.py +326 -0
- power_grid_model_ds/_core/model/graphs/models/rustworkx.py +119 -0
- power_grid_model_ds/_core/model/grids/__init__.py +0 -0
- power_grid_model_ds/_core/model/grids/_text_sources.py +119 -0
- power_grid_model_ds/_core/model/grids/base.py +434 -0
- power_grid_model_ds/_core/model/grids/helpers.py +122 -0
- power_grid_model_ds/_core/utils/__init__.py +0 -0
- power_grid_model_ds/_core/utils/misc.py +41 -0
- power_grid_model_ds/_core/utils/pickle.py +47 -0
- power_grid_model_ds/_core/utils/zip.py +72 -0
- power_grid_model_ds/arrays.py +39 -0
- power_grid_model_ds/constants.py +7 -0
- power_grid_model_ds/enums.py +7 -0
- power_grid_model_ds/errors.py +27 -0
- power_grid_model_ds/fancypy.py +9 -0
- power_grid_model_ds/generators.py +11 -0
- power_grid_model_ds/graph_models.py +8 -0
- power_grid_model_ds-0.0.1a11709467271.dist-info/LICENSE +292 -0
- power_grid_model_ds-0.0.1a11709467271.dist-info/METADATA +80 -0
- power_grid_model_ds-0.0.1a11709467271.dist-info/RECORD +64 -0
- power_grid_model_ds-0.0.1a11709467271.dist-info/WHEEL +5 -0
- 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
|