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,119 @@
|
|
1
|
+
# SPDX-FileCopyrightText: Contributors to the Power Grid Model project <powergridmodel@lfenergy.org>
|
2
|
+
#
|
3
|
+
# SPDX-License-Identifier: MPL-2.0
|
4
|
+
|
5
|
+
import logging
|
6
|
+
|
7
|
+
import rustworkx as rx
|
8
|
+
from rustworkx import NoEdgeBetweenNodes
|
9
|
+
from rustworkx.visit import BFSVisitor, PruneSearch
|
10
|
+
|
11
|
+
from power_grid_model_ds._core.model.graphs.errors import MissingBranchError, MissingNodeError, NoPathBetweenNodes
|
12
|
+
from power_grid_model_ds._core.model.graphs.models._rustworkx_search import find_fundamental_cycles_rustworkx
|
13
|
+
from power_grid_model_ds._core.model.graphs.models.base import BaseGraphModel
|
14
|
+
|
15
|
+
_logger = logging.getLogger(__name__)
|
16
|
+
|
17
|
+
|
18
|
+
class RustworkxGraphModel(BaseGraphModel):
|
19
|
+
"""A wrapper around the graph from the 'rustworkx' package"""
|
20
|
+
|
21
|
+
def __init__(self, active_only=False) -> None:
|
22
|
+
super().__init__(active_only=active_only)
|
23
|
+
self._graph: rx.PyGraph = rx.PyGraph()
|
24
|
+
self._internal_to_external: dict[int, int] = {}
|
25
|
+
self._external_to_internal: dict[int, int] = {}
|
26
|
+
|
27
|
+
@property
|
28
|
+
def nr_nodes(self):
|
29
|
+
return self._graph.num_nodes()
|
30
|
+
|
31
|
+
@property
|
32
|
+
def nr_branches(self):
|
33
|
+
return self._graph.num_edges()
|
34
|
+
|
35
|
+
@property
|
36
|
+
def external_ids(self) -> list[int]:
|
37
|
+
return list(self._external_to_internal.keys())
|
38
|
+
|
39
|
+
# pylint: disable=duplicate-code
|
40
|
+
def external_to_internal(self, ext_node_id: int):
|
41
|
+
try:
|
42
|
+
return self._external_to_internal[ext_node_id]
|
43
|
+
except KeyError as error:
|
44
|
+
raise MissingNodeError(f"External node id '{ext_node_id}' does NOT exist!") from error
|
45
|
+
|
46
|
+
def internal_to_external(self, int_node_id: int):
|
47
|
+
return self._internal_to_external[int_node_id]
|
48
|
+
|
49
|
+
def _add_node(self, ext_node_id: int):
|
50
|
+
graph_node_id = self._graph.add_node(ext_node_id)
|
51
|
+
self._external_to_internal[ext_node_id] = graph_node_id
|
52
|
+
self._internal_to_external[graph_node_id] = ext_node_id
|
53
|
+
|
54
|
+
def _delete_node(self, node_id: int):
|
55
|
+
self._graph.remove_node(node_id)
|
56
|
+
external_node_id = self._internal_to_external.pop(node_id)
|
57
|
+
self._external_to_internal.pop(external_node_id)
|
58
|
+
|
59
|
+
def _has_branch(self, from_node_id: int, to_node_id: int) -> bool:
|
60
|
+
return self._graph.has_edge(from_node_id, to_node_id)
|
61
|
+
|
62
|
+
def _has_node(self, node_id: int) -> bool:
|
63
|
+
return self._graph.has_node(node_id)
|
64
|
+
|
65
|
+
def _add_branch(self, from_node_id: int, to_node_id: int):
|
66
|
+
self._graph.add_edge(from_node_id, to_node_id, None)
|
67
|
+
|
68
|
+
def _delete_branch(self, from_node_id: int, to_node_id: int) -> None:
|
69
|
+
try:
|
70
|
+
self._graph.remove_edge(from_node_id, to_node_id)
|
71
|
+
except NoEdgeBetweenNodes as error:
|
72
|
+
raise MissingBranchError(f"No edge between (internal) nodes {from_node_id} and {to_node_id}") from error
|
73
|
+
|
74
|
+
def _get_shortest_path(self, source: int, target: int) -> tuple[list[int], int]:
|
75
|
+
path_mapping = rx.dijkstra_shortest_paths(self._graph, source, target)
|
76
|
+
|
77
|
+
if target not in path_mapping:
|
78
|
+
raise NoPathBetweenNodes(f"No path between internal nodes {source} and {target}")
|
79
|
+
|
80
|
+
path_nodes = list(path_mapping[target])
|
81
|
+
return path_nodes, len(path_nodes) - 1
|
82
|
+
|
83
|
+
def _get_all_paths(self, source: int, target: int) -> list[list[int]]:
|
84
|
+
return list(rx.all_simple_paths(self._graph, source, target))
|
85
|
+
|
86
|
+
def _get_components(self, substation_nodes: list[int]) -> list[list[int]]:
|
87
|
+
no_os_graph = self._graph.copy()
|
88
|
+
for os_node in substation_nodes:
|
89
|
+
no_os_graph.remove_node(os_node)
|
90
|
+
components = rx.connected_components(no_os_graph)
|
91
|
+
return [list(component) for component in components]
|
92
|
+
|
93
|
+
def _get_connected(self, node_id: int, nodes_to_ignore: list[int], inclusive: bool = False) -> list[int]:
|
94
|
+
visitor = _NodeVisitor(nodes_to_ignore)
|
95
|
+
rx.bfs_search(self._graph, [node_id], visitor)
|
96
|
+
connected_nodes = visitor.nodes
|
97
|
+
if not inclusive:
|
98
|
+
connected_nodes.remove(node_id)
|
99
|
+
|
100
|
+
return connected_nodes
|
101
|
+
|
102
|
+
def _find_fundamental_cycles(self) -> list[list[int]]:
|
103
|
+
"""Find all fundamental cycles in the graph using Rustworkx.
|
104
|
+
|
105
|
+
Returns:
|
106
|
+
list[list[int]]: A list of cycles, each cycle is a list of node IDs.
|
107
|
+
"""
|
108
|
+
return find_fundamental_cycles_rustworkx(self._graph)
|
109
|
+
|
110
|
+
|
111
|
+
class _NodeVisitor(BFSVisitor):
|
112
|
+
def __init__(self, nodes_to_ignore: list[int]):
|
113
|
+
self.nodes_to_ignore = nodes_to_ignore
|
114
|
+
self.nodes: list[int] = []
|
115
|
+
|
116
|
+
def discover_vertex(self, v):
|
117
|
+
if v in self.nodes_to_ignore:
|
118
|
+
raise PruneSearch
|
119
|
+
self.nodes.append(v)
|
File without changes
|
@@ -0,0 +1,119 @@
|
|
1
|
+
# SPDX-FileCopyrightText: Contributors to the Power Grid Model project <powergridmodel@lfenergy.org>
|
2
|
+
#
|
3
|
+
# SPDX-License-Identifier: MPL-2.0
|
4
|
+
|
5
|
+
"""Create a grid from text a text file"""
|
6
|
+
|
7
|
+
import logging
|
8
|
+
from pathlib import Path
|
9
|
+
from typing import TYPE_CHECKING
|
10
|
+
|
11
|
+
from power_grid_model_ds._core.model.enums.nodes import NodeType
|
12
|
+
|
13
|
+
if TYPE_CHECKING:
|
14
|
+
from power_grid_model_ds._core.model.grids.base import Grid
|
15
|
+
|
16
|
+
_logger = logging.getLogger(__name__)
|
17
|
+
|
18
|
+
|
19
|
+
class TextSource:
|
20
|
+
"""Class for handling text sources.
|
21
|
+
|
22
|
+
Text sources are only intended for test purposes so that a grid can quickly be designed from a text file.
|
23
|
+
Moreover, these text sources are compatible with the grid editor at https://csacademy.com/app/graph_editor/
|
24
|
+
|
25
|
+
Example of a text file:
|
26
|
+
S1 2
|
27
|
+
2 3
|
28
|
+
3 4 transformer
|
29
|
+
4 5
|
30
|
+
S1 7
|
31
|
+
|
32
|
+
See docs/examples/3_drawing_a_grid.md for more information.
|
33
|
+
"""
|
34
|
+
|
35
|
+
def __init__(self, grid_class: type["Grid"]):
|
36
|
+
self.grid = grid_class.empty()
|
37
|
+
|
38
|
+
def load_grid_from_path(self, path: Path):
|
39
|
+
"""Load assets from text file & sort them by id so that
|
40
|
+
they are ready to be appended to the grid"""
|
41
|
+
|
42
|
+
txt_nodes, txt_branches = self.read_txt(path)
|
43
|
+
self.add_nodes(txt_nodes)
|
44
|
+
self.add_branches(txt_branches)
|
45
|
+
self.grid.set_feeder_ids()
|
46
|
+
return self.grid
|
47
|
+
|
48
|
+
@staticmethod
|
49
|
+
def read_txt(path: Path) -> tuple[set, dict]:
|
50
|
+
"""Extract assets from text"""
|
51
|
+
with open(path, "r", encoding="utf-8") as f:
|
52
|
+
txt_rows = f.readlines()
|
53
|
+
|
54
|
+
txt_nodes = set()
|
55
|
+
txt_branches = {}
|
56
|
+
for text_line in txt_rows:
|
57
|
+
if not text_line.strip() or text_line.startswith("#"):
|
58
|
+
continue # skip empty lines and comments
|
59
|
+
try:
|
60
|
+
from_node_str, to_node_str, *comments = text_line.strip().split(" ")
|
61
|
+
except ValueError as err:
|
62
|
+
raise ValueError(f"Text line '{text_line}' is invalid. Skipping...") from err
|
63
|
+
comments = comments[0].split(",") if comments else []
|
64
|
+
|
65
|
+
txt_nodes |= {from_node_str, to_node_str}
|
66
|
+
txt_branches[(from_node_str, to_node_str)] = comments
|
67
|
+
return txt_nodes, txt_branches
|
68
|
+
|
69
|
+
def add_nodes(self, nodes: set[str]):
|
70
|
+
"""Add nodes to the grid"""
|
71
|
+
source_nodes = {int(node[1:]) for node in nodes if node.startswith("S")}
|
72
|
+
regular_nodes = {int(node) for node in nodes if not node.startswith("S")}
|
73
|
+
|
74
|
+
if source_nodes.intersection(regular_nodes):
|
75
|
+
raise ValueError("Source nodes and regular nodes have overlapping ids")
|
76
|
+
|
77
|
+
for node_id in source_nodes:
|
78
|
+
new_node = self.grid.node.empty(1)
|
79
|
+
new_node.id = node_id
|
80
|
+
new_node.node_type = NodeType.SUBSTATION_NODE
|
81
|
+
self.grid.append(new_node, check_max_id=False)
|
82
|
+
|
83
|
+
for node_id in regular_nodes:
|
84
|
+
new_node = self.grid.node.empty(1)
|
85
|
+
new_node.id = node_id
|
86
|
+
self.grid.append(new_node, check_max_id=False)
|
87
|
+
|
88
|
+
def add_branches(self, branches: dict[tuple[str, str], list[str]]):
|
89
|
+
"""Add branches to the grid"""
|
90
|
+
for branch, comments in branches.items():
|
91
|
+
self.add_branch(branch, comments)
|
92
|
+
|
93
|
+
def add_branch(self, branch: tuple[str, str], comments: list[str]):
|
94
|
+
"""Add a branch to the grid"""
|
95
|
+
from_node_str, to_node_str = branch
|
96
|
+
from_node = int(from_node_str.replace("S", ""))
|
97
|
+
to_node = int(to_node_str.replace("S", ""))
|
98
|
+
|
99
|
+
if "transformer" in comments:
|
100
|
+
new_branch = self.grid.transformer.empty(1)
|
101
|
+
elif "link" in comments:
|
102
|
+
new_branch = self.grid.link.empty(1)
|
103
|
+
else: # assume it is a line
|
104
|
+
new_branch = self.grid.line.empty(1)
|
105
|
+
|
106
|
+
branch_ids = [branch_id for branch_id in comments if branch_id.isdigit()]
|
107
|
+
if branch_ids:
|
108
|
+
if len(branch_ids) > 1:
|
109
|
+
raise ValueError(f"Multiple branch ids found in row {branch} {','.join(comments)}")
|
110
|
+
new_branch.id = int(branch_ids[0])
|
111
|
+
|
112
|
+
new_branch.from_node = from_node
|
113
|
+
new_branch.to_node = to_node
|
114
|
+
new_branch.from_status = 1
|
115
|
+
if "open" in comments:
|
116
|
+
new_branch.to_status = 0
|
117
|
+
else:
|
118
|
+
new_branch.to_status = 1
|
119
|
+
self.grid.append(new_branch)
|
@@ -0,0 +1,434 @@
|
|
1
|
+
# SPDX-FileCopyrightText: Contributors to the Power Grid Model project <powergridmodel@lfenergy.org>
|
2
|
+
#
|
3
|
+
# SPDX-License-Identifier: MPL-2.0
|
4
|
+
|
5
|
+
"""Base grid classes"""
|
6
|
+
|
7
|
+
import dataclasses
|
8
|
+
import itertools
|
9
|
+
import logging
|
10
|
+
from copy import copy
|
11
|
+
from dataclasses import dataclass
|
12
|
+
from pathlib import Path
|
13
|
+
from typing import Type, TypeVar
|
14
|
+
|
15
|
+
import numpy as np
|
16
|
+
import numpy.typing as npt
|
17
|
+
|
18
|
+
from power_grid_model_ds._core import fancypy as fp
|
19
|
+
from power_grid_model_ds._core.model.arrays import (
|
20
|
+
AsymVoltageSensorArray,
|
21
|
+
Branch3Array,
|
22
|
+
BranchArray,
|
23
|
+
LineArray,
|
24
|
+
LinkArray,
|
25
|
+
NodeArray,
|
26
|
+
SourceArray,
|
27
|
+
SymGenArray,
|
28
|
+
SymLoadArray,
|
29
|
+
SymPowerSensorArray,
|
30
|
+
SymVoltageSensorArray,
|
31
|
+
ThreeWindingTransformerArray,
|
32
|
+
TransformerArray,
|
33
|
+
TransformerTapRegulatorArray,
|
34
|
+
)
|
35
|
+
from power_grid_model_ds._core.model.arrays.base.array import FancyArray
|
36
|
+
from power_grid_model_ds._core.model.arrays.base.errors import RecordDoesNotExist
|
37
|
+
from power_grid_model_ds._core.model.containers.base import FancyArrayContainer
|
38
|
+
from power_grid_model_ds._core.model.enums.nodes import NodeType
|
39
|
+
from power_grid_model_ds._core.model.graphs.container import GraphContainer
|
40
|
+
from power_grid_model_ds._core.model.graphs.models import RustworkxGraphModel
|
41
|
+
from power_grid_model_ds._core.model.graphs.models.base import BaseGraphModel
|
42
|
+
from power_grid_model_ds._core.model.grids._text_sources import TextSource
|
43
|
+
from power_grid_model_ds._core.model.grids.helpers import set_feeder_ids, set_is_feeder
|
44
|
+
from power_grid_model_ds._core.utils.pickle import get_pickle_path, load_from_pickle, save_to_pickle
|
45
|
+
from power_grid_model_ds._core.utils.zip import file2gzip
|
46
|
+
|
47
|
+
Self = TypeVar("Self", bound="Grid")
|
48
|
+
|
49
|
+
# pylint: disable=too-many-instance-attributes
|
50
|
+
# pylint: disable=too-many-public-methods
|
51
|
+
|
52
|
+
|
53
|
+
@dataclass
|
54
|
+
class Grid(FancyArrayContainer):
|
55
|
+
"""Grid object containing the entire network and interface to interact with it.
|
56
|
+
|
57
|
+
Examples:
|
58
|
+
|
59
|
+
>>> from power_grid_model_ds import Grid
|
60
|
+
>>> grid = Grid.empty()
|
61
|
+
>>> grid
|
62
|
+
"""
|
63
|
+
|
64
|
+
graphs: GraphContainer
|
65
|
+
"""The graph representations of the grid."""
|
66
|
+
|
67
|
+
# nodes
|
68
|
+
node: NodeArray
|
69
|
+
|
70
|
+
# branches
|
71
|
+
transformer: TransformerArray
|
72
|
+
three_winding_transformer: ThreeWindingTransformerArray
|
73
|
+
line: LineArray
|
74
|
+
link: LinkArray
|
75
|
+
|
76
|
+
source: SourceArray
|
77
|
+
sym_load: SymLoadArray
|
78
|
+
sym_gen: SymGenArray
|
79
|
+
|
80
|
+
# regulators
|
81
|
+
transformer_tap_regulator: TransformerTapRegulatorArray
|
82
|
+
|
83
|
+
# sensors
|
84
|
+
sym_power_sensor: SymPowerSensorArray
|
85
|
+
sym_voltage_sensor: SymVoltageSensorArray
|
86
|
+
asym_voltage_sensor: AsymVoltageSensorArray
|
87
|
+
|
88
|
+
def __str__(self) -> str:
|
89
|
+
"""String representation of the grid.
|
90
|
+
|
91
|
+
Compatible with https://csacademy.com/app/graph_editor/
|
92
|
+
"""
|
93
|
+
grid_str = ""
|
94
|
+
|
95
|
+
for transformer3 in self.three_winding_transformer:
|
96
|
+
nodes = [transformer3.node_1.item(), transformer3.node_2.item(), transformer3.node_3.item()]
|
97
|
+
for combo in itertools.combinations(nodes, 2):
|
98
|
+
grid_str += f"S{combo[0]} S{combo[1]} {transformer3.id.item()},3-transformer\n"
|
99
|
+
|
100
|
+
for branch in self.branches:
|
101
|
+
from_node = self.node.get(id=branch.from_node).record
|
102
|
+
to_node = self.node.get(id=branch.to_node).record
|
103
|
+
|
104
|
+
from_node_str = f"S{from_node.id}" if from_node.node_type == NodeType.SUBSTATION_NODE else str(from_node.id)
|
105
|
+
to_node_str = f"S{to_node.id}" if to_node.node_type == NodeType.SUBSTATION_NODE else str(to_node.id)
|
106
|
+
|
107
|
+
suffix_str = str(branch.id.item())
|
108
|
+
if branch.from_status.item() == 0 or branch.to_status.item() == 0:
|
109
|
+
suffix_str = f"{suffix_str},open"
|
110
|
+
|
111
|
+
if branch.id in self.transformer.id:
|
112
|
+
suffix_str = f"{suffix_str},transformer"
|
113
|
+
elif branch.id in self.link.id:
|
114
|
+
suffix_str = f"{suffix_str},link"
|
115
|
+
elif branch.id in self.line.id:
|
116
|
+
pass # no suffix needed
|
117
|
+
else:
|
118
|
+
raise ValueError(f"Branch {branch.id} is not a transformer, link or line")
|
119
|
+
|
120
|
+
grid_str += f"{from_node_str} {to_node_str} {suffix_str}\n"
|
121
|
+
return grid_str
|
122
|
+
|
123
|
+
@property
|
124
|
+
def branches(self) -> BranchArray:
|
125
|
+
"""Converts all branch arrays into a single BranchArray."""
|
126
|
+
branch_dtype = BranchArray.get_dtype()
|
127
|
+
branches = BranchArray()
|
128
|
+
for array in self.branch_arrays:
|
129
|
+
new_branch = BranchArray(data=array.data[list(branch_dtype.names)])
|
130
|
+
branches = fp.concatenate(branches, new_branch)
|
131
|
+
return branches
|
132
|
+
|
133
|
+
@property
|
134
|
+
def branch_arrays(self) -> list[BranchArray]:
|
135
|
+
"""Returns all branch arrays"""
|
136
|
+
branch_arrays = []
|
137
|
+
for field in dataclasses.fields(self):
|
138
|
+
array = getattr(self, field.name)
|
139
|
+
if isinstance(array, BranchArray):
|
140
|
+
branch_arrays.append(array)
|
141
|
+
return branch_arrays
|
142
|
+
|
143
|
+
def get_typed_branches(self, branch_ids: list[int] | npt.NDArray[np.int32]) -> BranchArray:
|
144
|
+
"""Find a matching LineArray, LinkArray or TransformerArray for the given branch_ids
|
145
|
+
|
146
|
+
Raises:
|
147
|
+
ValueError:
|
148
|
+
- If no branch_ids are provided.
|
149
|
+
- If not all branch_ids are of the same type.
|
150
|
+
"""
|
151
|
+
if not np.any(branch_ids):
|
152
|
+
raise ValueError("No branch_ids provided.")
|
153
|
+
for branch_array in self.branch_arrays:
|
154
|
+
array = branch_array.filter(branch_ids)
|
155
|
+
if 0 < array.size != len(branch_ids):
|
156
|
+
raise ValueError("Branches are not of the same type.")
|
157
|
+
if array.size:
|
158
|
+
return array
|
159
|
+
raise RecordDoesNotExist(f"Branches {branch_ids} not found in grid.")
|
160
|
+
|
161
|
+
def reverse_branches(self, branches: BranchArray):
|
162
|
+
"""Reverse the direction of the branches."""
|
163
|
+
if not branches.size:
|
164
|
+
return
|
165
|
+
if not isinstance(branches, (LineArray, LinkArray, TransformerArray)):
|
166
|
+
try:
|
167
|
+
branches = self.get_typed_branches(branches.id)
|
168
|
+
except ValueError:
|
169
|
+
# If the branches are not of the same type, reverse them per type (though this is slower)
|
170
|
+
for array in self.branch_arrays:
|
171
|
+
self.reverse_branches(array.filter(branches.id))
|
172
|
+
return
|
173
|
+
|
174
|
+
from_nodes = branches.from_node
|
175
|
+
to_nodes = branches.to_node
|
176
|
+
|
177
|
+
array_field = self.find_array_field(branches.__class__)
|
178
|
+
array = getattr(self, array_field.name)
|
179
|
+
array.update_by_id(branches.id, from_node=to_nodes, to_node=from_nodes)
|
180
|
+
|
181
|
+
@classmethod
|
182
|
+
def empty(cls: Type[Self], graph_model: type[BaseGraphModel] = RustworkxGraphModel) -> Self:
|
183
|
+
"""Create an empty grid
|
184
|
+
|
185
|
+
Args:
|
186
|
+
graph_model (type[BaseGraphModel], optional): The graph model to use. Defaults to RustworkxGraphModel.
|
187
|
+
|
188
|
+
Returns:
|
189
|
+
Grid: An empty grid
|
190
|
+
"""
|
191
|
+
empty_fields = cls._get_empty_fields()
|
192
|
+
empty_fields["graphs"] = GraphContainer.empty(graph_model=graph_model)
|
193
|
+
return cls(**empty_fields)
|
194
|
+
|
195
|
+
def append(self, array: FancyArray, check_max_id: bool = True):
|
196
|
+
"""Append an array to the grid. Both 'grid arrays' and 'grid.graphs' will be updated.
|
197
|
+
|
198
|
+
Args:
|
199
|
+
array (FancyArray): The array to append.
|
200
|
+
check_max_id (bool, optional): Whether to check if the array id is the maximum id. Defaults to True.
|
201
|
+
"""
|
202
|
+
self._append(array, check_max_id=check_max_id) # noqa
|
203
|
+
for row in array:
|
204
|
+
# pylint: disable=protected-access
|
205
|
+
self.graphs._append(row)
|
206
|
+
|
207
|
+
def add_branch(self, branch: BranchArray) -> None:
|
208
|
+
"""Add a branch to the grid
|
209
|
+
|
210
|
+
Args:
|
211
|
+
branch (BranchArray): The branch to add
|
212
|
+
"""
|
213
|
+
self._append(array=branch)
|
214
|
+
self.graphs.add_branch(branch=branch)
|
215
|
+
|
216
|
+
logging.debug(f"added branch {branch.id} from {branch.from_node} to {branch.to_node}")
|
217
|
+
|
218
|
+
def delete_branch(self, branch: BranchArray) -> None:
|
219
|
+
"""Remove a branch from the grid
|
220
|
+
|
221
|
+
Args:
|
222
|
+
branch (BranchArray): The branch to remove
|
223
|
+
"""
|
224
|
+
_add_branch_array(branch=branch, grid=self)
|
225
|
+
self.graphs.delete_branch(branch=branch)
|
226
|
+
logging.debug(
|
227
|
+
f"""deleted branch {branch.id.item()} from {branch.from_node.item()} to {branch.to_node.item()}"""
|
228
|
+
)
|
229
|
+
|
230
|
+
def delete_branch3(self, branch: Branch3Array) -> None:
|
231
|
+
"""Remove a branch3 from the grid
|
232
|
+
|
233
|
+
Args:
|
234
|
+
branch (Branch3Array): The branch3 to remove
|
235
|
+
"""
|
236
|
+
_add_branch_array(branch=branch, grid=self)
|
237
|
+
self.graphs.delete_branch3(branch=branch)
|
238
|
+
|
239
|
+
def add_node(self, node: NodeArray) -> None:
|
240
|
+
"""Add a new node to the grid
|
241
|
+
|
242
|
+
Args:
|
243
|
+
node (NodeArray): The node to add
|
244
|
+
"""
|
245
|
+
self._append(array=node)
|
246
|
+
self.graphs.add_node(node=node)
|
247
|
+
logging.debug(f"added rail {node.id}")
|
248
|
+
|
249
|
+
def delete_node(self, node: NodeArray) -> None:
|
250
|
+
"""Remove a node from the grid
|
251
|
+
|
252
|
+
Args:
|
253
|
+
node (NodeArray): The node to remove
|
254
|
+
"""
|
255
|
+
self.node = self.node.exclude(id=node.id)
|
256
|
+
self.sym_load = self.sym_load.exclude(node=node.id)
|
257
|
+
self.source = self.source.exclude(node=node.id)
|
258
|
+
|
259
|
+
for branch_array in self.branch_arrays:
|
260
|
+
matching_branches = branch_array.filter(from_node=node.id, to_node=node.id, mode_="OR")
|
261
|
+
for branch in matching_branches:
|
262
|
+
self.delete_branch(branch)
|
263
|
+
|
264
|
+
self.graphs.delete_node(node=node)
|
265
|
+
logging.debug(f"deleted rail {node.id}")
|
266
|
+
|
267
|
+
def make_active(self, branch: BranchArray) -> None:
|
268
|
+
"""Make a branch active
|
269
|
+
|
270
|
+
Args:
|
271
|
+
branch (BranchArray): The branch to make active
|
272
|
+
"""
|
273
|
+
array_field = self.find_array_field(branch.__class__)
|
274
|
+
array_attr = getattr(self, array_field.name)
|
275
|
+
branch_mask = array_attr.id == branch.id
|
276
|
+
array_attr.from_status[branch_mask] = 1
|
277
|
+
array_attr.to_status[branch_mask] = 1
|
278
|
+
setattr(self, array_field.name, array_attr)
|
279
|
+
|
280
|
+
self.graphs.make_active(branch=branch)
|
281
|
+
logging.debug(f"activated branch {branch.id}")
|
282
|
+
|
283
|
+
def make_inactive(self, branch: BranchArray, at_to_side: bool = True) -> None:
|
284
|
+
"""Make a branch inactive. This is done by setting from or to status to 0.
|
285
|
+
|
286
|
+
Args:
|
287
|
+
branch (BranchArray): The branch to make inactive
|
288
|
+
at_to_side (bool, optional): Whether to deactivate the to_status instead of the from_status.
|
289
|
+
Defaults to True.
|
290
|
+
"""
|
291
|
+
array_field = self.find_array_field(branch.__class__)
|
292
|
+
array_attr = getattr(self, array_field.name)
|
293
|
+
branch_mask = array_attr.id == branch.id
|
294
|
+
status_side = "to_status" if at_to_side else "from_status"
|
295
|
+
array_attr[status_side][branch_mask] = 0
|
296
|
+
setattr(self, array_field.name, array_attr)
|
297
|
+
|
298
|
+
self.graphs.make_inactive(branch=branch)
|
299
|
+
logging.debug(f"deactivated branch {branch.id}")
|
300
|
+
|
301
|
+
def get_branches_in_path(self, nodes_in_path: list[int]) -> BranchArray:
|
302
|
+
"""Returns all branches within a path of nodes
|
303
|
+
|
304
|
+
Args:
|
305
|
+
nodes_in_path (list[int]): The nodes in the path
|
306
|
+
|
307
|
+
Returns:
|
308
|
+
BranchArray: The branches in the path
|
309
|
+
"""
|
310
|
+
return self.branches.filter(from_node=nodes_in_path, to_node=nodes_in_path, from_status=1, to_status=1)
|
311
|
+
|
312
|
+
def get_nearest_substation_node(self, node_id: int):
|
313
|
+
"""Find the nearest substation node.
|
314
|
+
|
315
|
+
Args:
|
316
|
+
node_id(int): The id of the node to find the nearest substation node for.
|
317
|
+
|
318
|
+
Returns:
|
319
|
+
NodeArray: The nearest substation node.
|
320
|
+
|
321
|
+
Raises:
|
322
|
+
RecordDoesNotExist: If no substation node is connected to the input node.
|
323
|
+
"""
|
324
|
+
connected_nodes = self.graphs.active_graph.get_connected(node_id=node_id, inclusive=True)
|
325
|
+
substation_nodes = self.node.filter(node_type=NodeType.SUBSTATION_NODE.value)
|
326
|
+
|
327
|
+
for node in connected_nodes:
|
328
|
+
if node in substation_nodes.id:
|
329
|
+
return substation_nodes.get(node)
|
330
|
+
raise RecordDoesNotExist(f"No {NodeType.SUBSTATION_NODE.name} connected to node {node_id}")
|
331
|
+
|
332
|
+
def get_downstream_nodes(self, node_id: int, inclusive: bool = False):
|
333
|
+
"""Get the downstream nodes from a node.
|
334
|
+
|
335
|
+
Example:
|
336
|
+
given this graph: [1] - [2] - [3] - [4], with 1 being a substation node
|
337
|
+
|
338
|
+
>>> graph.get_downstream_nodes(2) == [3, 4]
|
339
|
+
>>> graph.get_downstream_nodes(3) == [4]
|
340
|
+
>>> graph.get_downstream_nodes(3, inclusive=True) == [3, 4]
|
341
|
+
|
342
|
+
Args:
|
343
|
+
node_id(int): The id of the node to get the downstream nodes from.
|
344
|
+
inclusive(bool): Whether to include the input node in the result.
|
345
|
+
|
346
|
+
Raises:
|
347
|
+
NotImplementedError: If the input node is a substation node.
|
348
|
+
|
349
|
+
Returns:
|
350
|
+
list[int]: The downstream nodes.
|
351
|
+
"""
|
352
|
+
substation_node_id = self.get_nearest_substation_node(node_id).id.item()
|
353
|
+
|
354
|
+
if node_id == substation_node_id:
|
355
|
+
raise NotImplementedError("get_downstream_nodes is not implemented for substation nodes!")
|
356
|
+
|
357
|
+
path_to_substation, _ = self.graphs.active_graph.get_shortest_path(node_id, substation_node_id)
|
358
|
+
upstream_node = path_to_substation[1]
|
359
|
+
|
360
|
+
return self.graphs.active_graph.get_connected(node_id, nodes_to_ignore=[upstream_node], inclusive=inclusive)
|
361
|
+
|
362
|
+
def cache(self, cache_dir: Path, cache_name: str, compress: bool = True):
|
363
|
+
"""Cache Grid to a folder
|
364
|
+
|
365
|
+
Args:
|
366
|
+
cache_dir (Path): The directory to save the cache to.
|
367
|
+
cache_name (str): The name of the cache.
|
368
|
+
compress (bool, optional): Whether to compress the cache. Defaults to True.
|
369
|
+
"""
|
370
|
+
tmp_graphs = copy(self.graphs)
|
371
|
+
self.graphs = None # noqa
|
372
|
+
cache_dir.mkdir(parents=True, exist_ok=True)
|
373
|
+
|
374
|
+
pickle_path = cache_dir / f"{cache_name}.pickle"
|
375
|
+
save_to_pickle(path=pickle_path, python_object=self)
|
376
|
+
|
377
|
+
if compress:
|
378
|
+
gzip_path = file2gzip(pickle_path)
|
379
|
+
pickle_path.unlink()
|
380
|
+
return gzip_path
|
381
|
+
|
382
|
+
self.graphs = tmp_graphs
|
383
|
+
return pickle_path
|
384
|
+
|
385
|
+
@classmethod
|
386
|
+
# pylint: disable=arguments-differ
|
387
|
+
def from_cache(cls: Type[Self], cache_path: Path, load_graphs: bool = True) -> Self:
|
388
|
+
"""Read from cache and build .graphs from arrays
|
389
|
+
|
390
|
+
Args:
|
391
|
+
cache_path (Path): The path to the cache
|
392
|
+
load_graphs (bool, optional): Whether to load the graphs. Defaults to True.
|
393
|
+
|
394
|
+
Returns:
|
395
|
+
Self: The grid loaded from cache
|
396
|
+
"""
|
397
|
+
pickle_path = get_pickle_path(cache_path)
|
398
|
+
|
399
|
+
grid = cls._from_pickle(pickle_path=pickle_path)
|
400
|
+
if load_graphs:
|
401
|
+
grid.graphs = GraphContainer.from_arrays(grid)
|
402
|
+
return grid
|
403
|
+
|
404
|
+
@classmethod
|
405
|
+
def _from_pickle(cls, pickle_path: Path):
|
406
|
+
grid = load_from_pickle(path=pickle_path)
|
407
|
+
if not isinstance(grid, Grid):
|
408
|
+
raise TypeError(f"{pickle_path.name} is not a valid {cls.__name__} cache.")
|
409
|
+
return grid
|
410
|
+
|
411
|
+
@classmethod
|
412
|
+
# pylint: disable=arguments-differ
|
413
|
+
def from_txt_file(cls, txt_file_path: Path):
|
414
|
+
"""Load grid from txt file
|
415
|
+
|
416
|
+
Args:
|
417
|
+
txt_file_path (Path): The path to the txt file
|
418
|
+
"""
|
419
|
+
text_source = TextSource(grid_class=cls)
|
420
|
+
return text_source.load_grid_from_path(txt_file_path)
|
421
|
+
|
422
|
+
def set_feeder_ids(self):
|
423
|
+
"""Sets feeder and substation id properties in the grids arrays"""
|
424
|
+
set_is_feeder(grid=self)
|
425
|
+
set_feeder_ids(grid=self)
|
426
|
+
|
427
|
+
|
428
|
+
def _add_branch_array(branch: BranchArray | Branch3Array, grid: Grid):
|
429
|
+
"""Add a branch array to the grid"""
|
430
|
+
array_field = grid.find_array_field(branch.__class__)
|
431
|
+
array_attr = getattr(grid, array_field.name)
|
432
|
+
setattr(grid, array_field.name, array_attr.exclude(id=branch.id))
|
433
|
+
|
434
|
+
grid.transformer_tap_regulator = grid.transformer_tap_regulator.exclude(regulated_object=branch.id)
|