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,9 @@
|
|
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.load_flow import PowerGridModelInterface
|
6
|
+
from power_grid_model_ds._core.model.graphs.container import GraphContainer
|
7
|
+
from power_grid_model_ds._core.model.grids.base import Grid
|
8
|
+
|
9
|
+
__all__ = ["Grid", "GraphContainer", "PowerGridModelInterface"]
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
@@ -0,0 +1,25 @@
|
|
1
|
+
# SPDX-FileCopyrightText: Contributors to the Power Grid Model project <powergridmodel@lfenergy.org>
|
2
|
+
#
|
3
|
+
# SPDX-License-Identifier: MPL-2.0
|
4
|
+
|
5
|
+
"""Base generator"""
|
6
|
+
|
7
|
+
import numpy as np
|
8
|
+
|
9
|
+
from power_grid_model_ds._core.model.grids.base import Grid
|
10
|
+
|
11
|
+
|
12
|
+
class BaseGenerator:
|
13
|
+
"""Base class to build a generator for grid elements"""
|
14
|
+
|
15
|
+
def __init__(self, grid: Grid, seed: int) -> None:
|
16
|
+
"""Initializes generator with grid and amount"""
|
17
|
+
self.grid = grid
|
18
|
+
|
19
|
+
self.starting_seed = seed
|
20
|
+
self.rng = np.random.default_rng(seed)
|
21
|
+
|
22
|
+
def reset_rng(self, seed: int):
|
23
|
+
"""Sets the rng for a generator"""
|
24
|
+
rng = np.random.default_rng(seed)
|
25
|
+
self.rng = rng
|
@@ -0,0 +1,133 @@
|
|
1
|
+
# SPDX-FileCopyrightText: Contributors to the Power Grid Model project <powergridmodel@lfenergy.org>
|
2
|
+
#
|
3
|
+
# SPDX-License-Identifier: MPL-2.0
|
4
|
+
|
5
|
+
"""Generator for LineArray"""
|
6
|
+
|
7
|
+
import numpy as np
|
8
|
+
|
9
|
+
from power_grid_model_ds._core import fancypy as fp
|
10
|
+
from power_grid_model_ds._core.data_source.generator.arrays.base import BaseGenerator
|
11
|
+
from power_grid_model_ds._core.model.arrays import LineArray, TransformerArray
|
12
|
+
from power_grid_model_ds._core.model.grids.base import Grid
|
13
|
+
|
14
|
+
AVERAGE_ROUTE_SIZE = 20
|
15
|
+
|
16
|
+
|
17
|
+
class LineGenerator(BaseGenerator):
|
18
|
+
"""Generator for line elements in the grid"""
|
19
|
+
|
20
|
+
def __init__(self, grid: Grid, seed: int) -> None:
|
21
|
+
super().__init__(grid=grid, seed=seed)
|
22
|
+
self.connected_nodes: list = []
|
23
|
+
self.unconnected_nodes: list = []
|
24
|
+
self.line_array: LineArray = self.grid.line.__class__()
|
25
|
+
self.trafo_array: TransformerArray = self.grid.transformer.__class__()
|
26
|
+
|
27
|
+
# pylint: disable=arguments-differ
|
28
|
+
def run(self, amount: int, number_of_routes: int | None = None) -> LineArray:
|
29
|
+
"""Generate routes, lines and normally open points (NOPs)"""
|
30
|
+
|
31
|
+
self.trafo_array = self.grid.transformer
|
32
|
+
if number_of_routes is None:
|
33
|
+
number_of_routes = self.determine_number_of_routes()
|
34
|
+
if number_of_routes > 0:
|
35
|
+
self.create_routes(number_of_routes)
|
36
|
+
else:
|
37
|
+
self.line_array = self.grid.line
|
38
|
+
|
39
|
+
# while not all connected, add lines from a connected node to an unconnected node
|
40
|
+
self.set_unconnected_nodes()
|
41
|
+
while any(self.unconnected_nodes):
|
42
|
+
self.connect_nodes()
|
43
|
+
self.set_unconnected_nodes()
|
44
|
+
|
45
|
+
number_of_nops = amount
|
46
|
+
if number_of_nops > 0:
|
47
|
+
self.create_nop_lines(number_of_nops)
|
48
|
+
|
49
|
+
return self.line_array
|
50
|
+
|
51
|
+
def create_routes(self, number_of_routes: int):
|
52
|
+
"""Create a number of lines from the substation to unconnected nodes"""
|
53
|
+
# each source should have at least one route
|
54
|
+
number_of_sources = len(self.grid.source)
|
55
|
+
from_nodes = self.rng.choice(self.grid.source.node, number_of_routes - number_of_sources, replace=True)
|
56
|
+
not_source_mask = ~np.isin(self.grid.node.id, self.grid.source.node)
|
57
|
+
to_nodes = self.rng.choice(self.grid.node.id[not_source_mask], number_of_routes, replace=False)
|
58
|
+
capacities = 100 + self.rng.exponential(200, number_of_routes)
|
59
|
+
line_array = self.grid.line.__class__.zeros(number_of_routes)
|
60
|
+
line_array.id = 1 + self.grid.max_id + np.arange(number_of_routes)
|
61
|
+
line_array.from_node = np.concatenate((self.grid.source.node, from_nodes))
|
62
|
+
line_array.to_node = to_nodes
|
63
|
+
line_array.from_status = [1] * number_of_routes
|
64
|
+
line_array.to_status = [1] * number_of_routes
|
65
|
+
line_array.r1 = self.rng.exponential(0.2, number_of_routes)
|
66
|
+
line_array.x1 = self.rng.exponential(0.02, number_of_routes)
|
67
|
+
line_array.i_n = capacities
|
68
|
+
self.line_array = line_array
|
69
|
+
|
70
|
+
def determine_number_of_routes(self) -> int:
|
71
|
+
"""Decide on a number of routes based on expected route-size"""
|
72
|
+
expected_number_of_routes = int(np.ceil(len(self.grid.node) / AVERAGE_ROUTE_SIZE))
|
73
|
+
number_of_sources = len(self.grid.source)
|
74
|
+
# The number of routes is the max of the number of sources
|
75
|
+
# and the expected number based on size
|
76
|
+
return max(expected_number_of_routes, number_of_sources)
|
77
|
+
|
78
|
+
def connect_nodes(self):
|
79
|
+
"""Add a new line between an active and inactive line"""
|
80
|
+
to_node = self.rng.choice(self.unconnected_nodes)
|
81
|
+
to_voltage = self.grid.node[self.grid.node.id == to_node].u_rated[0]
|
82
|
+
same_voltage_mask = self.grid.node.u_rated == to_voltage
|
83
|
+
same_voltage_nodes = self.grid.node[same_voltage_mask]
|
84
|
+
options_mask = np.isin(self.connected_nodes, same_voltage_nodes.id)
|
85
|
+
from_node = self.rng.choice(np.array(self.connected_nodes)[options_mask])
|
86
|
+
capacity = 100 + self.rng.exponential(200, 1)
|
87
|
+
new_line = self.grid.line.__class__.zeros(1)
|
88
|
+
new_line.id = 1 + max(max(self.line_array.id), self.grid.max_id) # pylint: disable=nested-min-max
|
89
|
+
new_line.from_node = from_node
|
90
|
+
new_line.to_node = to_node
|
91
|
+
new_line.from_status = [1]
|
92
|
+
new_line.to_status = [1]
|
93
|
+
new_line.r1 = self.rng.exponential(0.2, 1)
|
94
|
+
new_line.x1 = self.rng.exponential(0.02, 1)
|
95
|
+
new_line.i_n = capacity
|
96
|
+
self.line_array = fp.concatenate(self.line_array, new_line)
|
97
|
+
|
98
|
+
def create_nop_lines(self, number_of_nops: int):
|
99
|
+
"""Create the inactive lines between different routes (Normally Open Points)"""
|
100
|
+
nops = [self.rng.choice(self.grid.node.id, 2, replace=False) for _ in range(number_of_nops)]
|
101
|
+
from_nodes = [nop[0] for nop in nops]
|
102
|
+
to_nodes = [nop[1] for nop in nops]
|
103
|
+
capacities = 100 + self.rng.exponential(200, number_of_nops)
|
104
|
+
nop_lines = self.grid.line.__class__.zeros(number_of_nops)
|
105
|
+
nop_lines.id = 1 + self.line_array.id.max() + np.arange(number_of_nops)
|
106
|
+
nop_lines.from_node = from_nodes
|
107
|
+
nop_lines.to_node = to_nodes
|
108
|
+
nop_lines.from_status = [1] * number_of_nops
|
109
|
+
nop_lines.to_status = [0] * number_of_nops
|
110
|
+
nop_lines.r1 = self.rng.exponential(0.2, number_of_nops)
|
111
|
+
nop_lines.x1 = self.rng.exponential(0.02, number_of_nops)
|
112
|
+
nop_lines.i_n = capacities
|
113
|
+
self.line_array = fp.concatenate(self.line_array, nop_lines)
|
114
|
+
|
115
|
+
def set_unconnected_nodes(self) -> None:
|
116
|
+
"""From a line array and total set of nodes determine which are not yet connected"""
|
117
|
+
connected_link_mask = np.logical_or(
|
118
|
+
np.isin(self.grid.node.id, self.line_array.from_node),
|
119
|
+
np.isin(self.grid.node.id, self.line_array.to_node),
|
120
|
+
)
|
121
|
+
connected_trafo_mask = np.logical_or(
|
122
|
+
np.isin(self.grid.node.id, self.trafo_array.from_node),
|
123
|
+
np.isin(self.grid.node.id, self.trafo_array.to_node),
|
124
|
+
)
|
125
|
+
connected_mask = np.logical_or(
|
126
|
+
connected_link_mask,
|
127
|
+
connected_trafo_mask,
|
128
|
+
)
|
129
|
+
connected_nodes = self.grid.node.id[connected_mask]
|
130
|
+
unconnected_nodes = self.grid.node.id[~connected_mask]
|
131
|
+
|
132
|
+
self.unconnected_nodes = unconnected_nodes.tolist()
|
133
|
+
self.connected_nodes = connected_nodes.tolist()
|
@@ -0,0 +1,37 @@
|
|
1
|
+
# SPDX-FileCopyrightText: Contributors to the Power Grid Model project <powergridmodel@lfenergy.org>
|
2
|
+
#
|
3
|
+
# SPDX-License-Identifier: MPL-2.0
|
4
|
+
|
5
|
+
"""Generator for NodeArray"""
|
6
|
+
|
7
|
+
import numpy as np
|
8
|
+
|
9
|
+
from power_grid_model_ds._core.data_source.generator.arrays.base import BaseGenerator
|
10
|
+
|
11
|
+
|
12
|
+
class NodeGenerator(BaseGenerator):
|
13
|
+
"""Generator for node elements in the grid"""
|
14
|
+
|
15
|
+
# pylint: disable=arguments-differ
|
16
|
+
def run(self, amount: int, voltage_level: int = 10_500):
|
17
|
+
"""Generate nodes in a grid with two possible load scenarios"""
|
18
|
+
node_array = self.grid.node.__class__.zeros(amount)
|
19
|
+
node_array.id = 1 + self.grid.max_id + np.arange(amount)
|
20
|
+
node_array.u_rated = voltage_level
|
21
|
+
|
22
|
+
load_low_array = self.grid.sym_load.__class__.zeros(amount)
|
23
|
+
load_low_array.id = 1 + node_array.id.max() + np.arange(amount)
|
24
|
+
load_low_array.node = node_array.id
|
25
|
+
load_low_array.status = 1
|
26
|
+
load_high_array = self.grid.sym_load.__class__.zeros(amount)
|
27
|
+
load_high_array.id = 1 + load_low_array.id.max() + np.arange(amount)
|
28
|
+
load_high_array.node = node_array.id
|
29
|
+
load_high_array.status = 1
|
30
|
+
|
31
|
+
# power consumption in Watt
|
32
|
+
load_low_array.p_specified = np.round(self.rng.normal(200_000, 150_000, amount))
|
33
|
+
load_low_array.q_specified = np.round(self.rng.normal(20_000, 15_000, amount))
|
34
|
+
load_high_array.p_specified = np.round(self.rng.normal(-100_000, 350_000, amount))
|
35
|
+
load_high_array.q_specified = np.round(self.rng.normal(-5_000, 35_000, amount))
|
36
|
+
|
37
|
+
return node_array, load_low_array, load_high_array
|
@@ -0,0 +1,30 @@
|
|
1
|
+
# SPDX-FileCopyrightText: Contributors to the Power Grid Model project <powergridmodel@lfenergy.org>
|
2
|
+
#
|
3
|
+
# SPDX-License-Identifier: MPL-2.0
|
4
|
+
|
5
|
+
"""Generator for SourceArray"""
|
6
|
+
|
7
|
+
import numpy as np
|
8
|
+
|
9
|
+
from power_grid_model_ds._core.data_source.generator.arrays.base import BaseGenerator
|
10
|
+
from power_grid_model_ds._core.model.arrays import NodeArray, SourceArray
|
11
|
+
from power_grid_model_ds._core.model.enums.nodes import NodeType
|
12
|
+
|
13
|
+
|
14
|
+
class SourceGenerator(BaseGenerator):
|
15
|
+
"""Generator for source elements in the grid (substations)"""
|
16
|
+
|
17
|
+
def run(self, amount: int) -> tuple[NodeArray, SourceArray]:
|
18
|
+
"""Generate nodes in a grid which are sources (substations)"""
|
19
|
+
substation_node_array = self.grid.node.__class__.empty(amount)
|
20
|
+
substation_node_array.id = 1 + self.grid.max_id + np.arange(amount)
|
21
|
+
substation_node_array.u_rated = 10_500
|
22
|
+
substation_node_array.node_type = NodeType.SUBSTATION_NODE.value
|
23
|
+
|
24
|
+
source_array = self.grid.source.__class__.empty(amount)
|
25
|
+
source_array.id = 1 + substation_node_array.id.max() + np.arange(amount)
|
26
|
+
source_array.node = substation_node_array.id
|
27
|
+
source_array.status = 1
|
28
|
+
source_array.u_ref = 1
|
29
|
+
|
30
|
+
return substation_node_array, source_array
|
@@ -0,0 +1,37 @@
|
|
1
|
+
# SPDX-FileCopyrightText: Contributors to the Power Grid Model project <powergridmodel@lfenergy.org>
|
2
|
+
#
|
3
|
+
# SPDX-License-Identifier: MPL-2.0
|
4
|
+
|
5
|
+
"""Generator for LineArray"""
|
6
|
+
|
7
|
+
import numpy as np
|
8
|
+
|
9
|
+
from power_grid_model_ds._core.data_source.generator.arrays.base import BaseGenerator
|
10
|
+
from power_grid_model_ds._core.model.arrays import TransformerArray
|
11
|
+
|
12
|
+
|
13
|
+
class TransformerGenerator(BaseGenerator):
|
14
|
+
"""Generator for tranformer elements in the grid"""
|
15
|
+
|
16
|
+
def run(self, amount: int) -> TransformerArray:
|
17
|
+
"""Generate transformers"""
|
18
|
+
|
19
|
+
# Create transformers from 10kV to 3kV
|
20
|
+
from_mask = self.grid.node.u_rated == 10_500
|
21
|
+
from_nodes = self.rng.choice(self.grid.node.id[from_mask], amount, replace=True)
|
22
|
+
to_mask = self.grid.node.u_rated == 3_000
|
23
|
+
to_nodes = self.rng.choice(self.grid.node.id[to_mask], amount, replace=False)
|
24
|
+
transformer_array = self.grid.transformer.__class__.zeros(amount)
|
25
|
+
transformer_array.id = 1 + self.grid.max_id + np.arange(amount)
|
26
|
+
transformer_array.from_node = from_nodes
|
27
|
+
transformer_array.to_node = to_nodes
|
28
|
+
transformer_array.from_status = [1] * amount
|
29
|
+
transformer_array.to_status = [1] * amount
|
30
|
+
transformer_array.u1 = [10_500] * amount
|
31
|
+
transformer_array.u2 = [3_000] * amount
|
32
|
+
transformer_array.sn = [30e6] * amount
|
33
|
+
transformer_array.clock = [12] * amount
|
34
|
+
transformer_array.uk = [0.203] * amount
|
35
|
+
transformer_array.pk = [100e3] * amount
|
36
|
+
|
37
|
+
return transformer_array
|
@@ -0,0 +1,78 @@
|
|
1
|
+
# SPDX-FileCopyrightText: Contributors to the Power Grid Model project <powergridmodel@lfenergy.org>
|
2
|
+
#
|
3
|
+
# SPDX-License-Identifier: MPL-2.0
|
4
|
+
|
5
|
+
"""Generators for the grid"""
|
6
|
+
|
7
|
+
from typing import Type
|
8
|
+
|
9
|
+
import numpy as np
|
10
|
+
|
11
|
+
from power_grid_model_ds._core.data_source.generator.arrays.line import LineGenerator
|
12
|
+
from power_grid_model_ds._core.data_source.generator.arrays.node import NodeGenerator
|
13
|
+
from power_grid_model_ds._core.data_source.generator.arrays.source import SourceGenerator
|
14
|
+
from power_grid_model_ds._core.data_source.generator.arrays.transformer import TransformerGenerator
|
15
|
+
from power_grid_model_ds._core.model.graphs.models.base import BaseGraphModel
|
16
|
+
from power_grid_model_ds._core.model.graphs.models.rustworkx import RustworkxGraphModel
|
17
|
+
from power_grid_model_ds._core.model.grids.base import Grid
|
18
|
+
|
19
|
+
# pylint: disable=too-few-public-methods,too-many-arguments,too-many-positional-arguments
|
20
|
+
|
21
|
+
|
22
|
+
class RadialGridGenerator:
|
23
|
+
"""Generates a random but structurally correct radial grid with the given specifications"""
|
24
|
+
|
25
|
+
def __init__(
|
26
|
+
self,
|
27
|
+
grid_class: Type[Grid],
|
28
|
+
nr_nodes: int = 100,
|
29
|
+
nr_sources: int = 2,
|
30
|
+
nr_nops: int = 10,
|
31
|
+
graph_model: type[BaseGraphModel] = RustworkxGraphModel,
|
32
|
+
):
|
33
|
+
self.grid_class = grid_class
|
34
|
+
self.graph_model = graph_model
|
35
|
+
self.nr_nodes = nr_nodes
|
36
|
+
self.nr_sources = nr_sources
|
37
|
+
self.nr_nops = nr_nops
|
38
|
+
|
39
|
+
def run(self, seed=None, create_10_3_kv_net: bool = False):
|
40
|
+
"""Run the generator to create a random radial grid.
|
41
|
+
|
42
|
+
if a seed is provided, this will be used to set rng.
|
43
|
+
"""
|
44
|
+
grid = self.grid_class.empty(graph_model=self.graph_model)
|
45
|
+
|
46
|
+
# create nodeArray
|
47
|
+
node_generator = NodeGenerator(grid=grid, seed=seed)
|
48
|
+
|
49
|
+
nodes, _loads_low, loads_high = node_generator.run(amount=self.nr_nodes)
|
50
|
+
grid.append(nodes)
|
51
|
+
grid.append(loads_high)
|
52
|
+
|
53
|
+
# create sourceArray
|
54
|
+
source_generator = SourceGenerator(grid=grid, seed=seed)
|
55
|
+
nodes, sources = source_generator.run(amount=self.nr_sources)
|
56
|
+
grid.append(nodes)
|
57
|
+
grid.append(sources)
|
58
|
+
|
59
|
+
# create lineArray
|
60
|
+
line_generator = LineGenerator(grid=grid, seed=seed)
|
61
|
+
lines = line_generator.run(amount=self.nr_nops)
|
62
|
+
grid.append(lines)
|
63
|
+
|
64
|
+
if create_10_3_kv_net:
|
65
|
+
# create 3kV nodes
|
66
|
+
nodes, _loads_low, _loads_high = node_generator.run(amount=10, voltage_level=3_000)
|
67
|
+
grid.append(nodes)
|
68
|
+
grid.append(_loads_high)
|
69
|
+
|
70
|
+
# create transformerArray
|
71
|
+
transformer_generator = TransformerGenerator(grid=grid, seed=seed)
|
72
|
+
transformers = transformer_generator.run(amount=2)
|
73
|
+
grid.append(transformers)
|
74
|
+
|
75
|
+
lines = line_generator.run(amount=0, number_of_routes=0)
|
76
|
+
grid.append(lines[~np.isin(lines.id, grid.line.id)])
|
77
|
+
|
78
|
+
return grid
|
@@ -0,0 +1,66 @@
|
|
1
|
+
# SPDX-FileCopyrightText: Contributors to the Power Grid Model project <powergridmodel@lfenergy.org>
|
2
|
+
#
|
3
|
+
# SPDX-License-Identifier: MPL-2.0
|
4
|
+
|
5
|
+
"""A set of helper functions that mimic numpy functions but are specifically designed for FancyArrays."""
|
6
|
+
|
7
|
+
from typing import TYPE_CHECKING, Union
|
8
|
+
|
9
|
+
import numpy as np
|
10
|
+
|
11
|
+
if TYPE_CHECKING:
|
12
|
+
from power_grid_model_ds._core.model.arrays.base.array import FancyArray
|
13
|
+
|
14
|
+
|
15
|
+
def concatenate(fancy_array: "FancyArray", *other_arrays: Union["FancyArray", np.ndarray]) -> "FancyArray":
|
16
|
+
"""Concatenate arrays."""
|
17
|
+
np_arrays = [array if isinstance(array, np.ndarray) else array.data for array in other_arrays]
|
18
|
+
try:
|
19
|
+
concatenated = np.concatenate([fancy_array.data] + np_arrays)
|
20
|
+
except TypeError as error:
|
21
|
+
raise TypeError("Cannot append arrays: mismatching dtypes.") from error
|
22
|
+
return fancy_array.__class__(data=concatenated)
|
23
|
+
|
24
|
+
|
25
|
+
def unique(array: "FancyArray", **kwargs):
|
26
|
+
"""Return the unique elements of the array."""
|
27
|
+
for column in array.columns:
|
28
|
+
if np.issubdtype(array.dtype[column], np.floating) and np.isnan(array[column]).any():
|
29
|
+
raise NotImplementedError("Finding unique records in array with NaN values is not supported.")
|
30
|
+
# see https://github.com/numpy/numpy/issues/23286
|
31
|
+
unique_data = np.unique(array.data, **kwargs)
|
32
|
+
if isinstance(unique_data, tuple):
|
33
|
+
unique_data, *other = unique_data
|
34
|
+
return array.__class__(data=unique_data), *other
|
35
|
+
return array.__class__(data=unique_data)
|
36
|
+
|
37
|
+
|
38
|
+
def sort(array: "FancyArray", axis=-1, kind=None, order=None) -> "FancyArray":
|
39
|
+
"""Sort the array in-place and return sorted array."""
|
40
|
+
array.data.sort(axis=axis, kind=kind, order=order)
|
41
|
+
return array
|
42
|
+
|
43
|
+
|
44
|
+
def array_equal(array1: "FancyArray", array2: "FancyArray", equal_nan: bool = True) -> bool:
|
45
|
+
"""Return True if two arrays are equal."""
|
46
|
+
if equal_nan:
|
47
|
+
return _array_equal_with_nan(array1, array2)
|
48
|
+
return np.array_equal(array1.data, array2.data)
|
49
|
+
|
50
|
+
|
51
|
+
def _array_equal_with_nan(array1: "FancyArray", array2: "FancyArray") -> bool:
|
52
|
+
# np.array_equal does not work with NaN values in structured arrays, so we need to compare column by column.
|
53
|
+
# related issue: https://github.com/numpy/numpy/issues/21539
|
54
|
+
|
55
|
+
if array1.columns != array2.columns:
|
56
|
+
return False
|
57
|
+
|
58
|
+
for column in array1.columns:
|
59
|
+
column_dtype = array1.dtype[column]
|
60
|
+
if np.issubdtype(column_dtype, np.str_):
|
61
|
+
if not np.array_equal(array1[column], array2[column]):
|
62
|
+
return False
|
63
|
+
continue
|
64
|
+
if not np.array_equal(array1[column], array2[column], equal_nan=True):
|
65
|
+
return False
|
66
|
+
return True
|
@@ -0,0 +1,140 @@
|
|
1
|
+
# SPDX-FileCopyrightText: Contributors to the Power Grid Model project <powergridmodel@lfenergy.org>
|
2
|
+
#
|
3
|
+
# SPDX-License-Identifier: MPL-2.0
|
4
|
+
|
5
|
+
"""Load flow functions and classes"""
|
6
|
+
|
7
|
+
from typing import Dict, Optional
|
8
|
+
|
9
|
+
import numpy as np
|
10
|
+
from numpy.typing import NDArray
|
11
|
+
from power_grid_model import CalculationMethod, PowerGridModel, initialize_array
|
12
|
+
|
13
|
+
from power_grid_model_ds._core.model.grids.base import Grid
|
14
|
+
|
15
|
+
PGM_ARRAYS = [
|
16
|
+
"node",
|
17
|
+
"line",
|
18
|
+
"link",
|
19
|
+
"transformer",
|
20
|
+
"three_winding_transformer",
|
21
|
+
"sym_load",
|
22
|
+
"sym_gen",
|
23
|
+
"source",
|
24
|
+
"transformer_tap_regulator",
|
25
|
+
"sym_power_sensor",
|
26
|
+
"sym_voltage_sensor",
|
27
|
+
"asym_voltage_sensor",
|
28
|
+
]
|
29
|
+
|
30
|
+
|
31
|
+
class PGMCoreException(Exception):
|
32
|
+
"""Raised when there is an error in running the power grid model"""
|
33
|
+
|
34
|
+
|
35
|
+
class PowerGridModelInterface:
|
36
|
+
"""Interface between the Grid and the PowerGridModel (pgm).
|
37
|
+
|
38
|
+
- Can convert grid data to pgm input
|
39
|
+
- Can calculate power flow
|
40
|
+
- Can do batch calculations using pgm
|
41
|
+
- Can update grid with output from power flow
|
42
|
+
"""
|
43
|
+
|
44
|
+
def __init__(
|
45
|
+
self,
|
46
|
+
grid: Grid,
|
47
|
+
input_data: Optional[Dict] = None,
|
48
|
+
system_frequency: float = 50.0,
|
49
|
+
):
|
50
|
+
self.grid = grid
|
51
|
+
self.system_frequency = system_frequency
|
52
|
+
|
53
|
+
self.input_data = input_data or {}
|
54
|
+
self.output_data: dict[str, NDArray] = {}
|
55
|
+
self.model: Optional[PowerGridModel] = None
|
56
|
+
|
57
|
+
def create_input_from_grid(self):
|
58
|
+
"""
|
59
|
+
Create input for the PowerGridModel
|
60
|
+
"""
|
61
|
+
for array_name in PGM_ARRAYS:
|
62
|
+
pgm_array = self._create_power_grid_array(array_name=array_name)
|
63
|
+
self.input_data[array_name] = pgm_array
|
64
|
+
return self.input_data
|
65
|
+
|
66
|
+
def calculate_power_flow(
|
67
|
+
self,
|
68
|
+
calculation_method: CalculationMethod = CalculationMethod.newton_raphson,
|
69
|
+
update_data: Optional[Dict] = None,
|
70
|
+
**kwargs,
|
71
|
+
):
|
72
|
+
"""Initialize the PowerGridModel and calculate power flow over input data.
|
73
|
+
|
74
|
+
If input data is not available, self.create_input_from_grid() will be called to create it.
|
75
|
+
|
76
|
+
Returns output of the power flow calculation (also stored in self.output_data)
|
77
|
+
"""
|
78
|
+
self.model = self.model or self._setup_model()
|
79
|
+
|
80
|
+
self.output_data = self.model.calculate_power_flow(
|
81
|
+
calculation_method=calculation_method, update_data=update_data, **kwargs
|
82
|
+
)
|
83
|
+
return self.output_data
|
84
|
+
|
85
|
+
def _create_power_grid_array(self, array_name: str) -> np.ndarray:
|
86
|
+
"""Create power grid model array"""
|
87
|
+
internal_array = getattr(self.grid, array_name)
|
88
|
+
pgm_array = initialize_array("input", array_name, internal_array.size)
|
89
|
+
fields = self._match_dtypes(pgm_array.dtype, internal_array.dtype)
|
90
|
+
pgm_array[fields] = internal_array.data[fields]
|
91
|
+
return pgm_array
|
92
|
+
|
93
|
+
def update_model(self, update_data: Dict):
|
94
|
+
"""
|
95
|
+
Updates the power-grid-model using update_data, this allows for batch calculations
|
96
|
+
|
97
|
+
Example:
|
98
|
+
Example of update_data creation:
|
99
|
+
|
100
|
+
>>> update_sym_load = initialize_array('update', 'sym_load', 2)
|
101
|
+
>>> update_sym_load['id'] = [4, 7] # same ID
|
102
|
+
>>> update_sym_load['p_specified'] = [30e6, 15e6] # change active power
|
103
|
+
>>> # leave reactive power the same, no need to specify
|
104
|
+
>>>
|
105
|
+
>>> update_line = initialize_array('update', 'line', 1)
|
106
|
+
>>> update_line['id'] = [3] # change line ID 3
|
107
|
+
>>> update_line['from_status'] = [0] # switch off at from side
|
108
|
+
>>> # leave to-side swichint status the same, no need to specify
|
109
|
+
>>>
|
110
|
+
>>> update_data = {
|
111
|
+
>>> 'sym_load': update_sym_load,
|
112
|
+
>>> 'line': update_line
|
113
|
+
>>> }
|
114
|
+
|
115
|
+
|
116
|
+
"""
|
117
|
+
self.model = self.model or self._setup_model()
|
118
|
+
self.model.update(update_data=update_data)
|
119
|
+
|
120
|
+
def update_grid(self) -> None:
|
121
|
+
"""
|
122
|
+
Fills the output values in the grid for the values that are present
|
123
|
+
"""
|
124
|
+
if not self.output_data:
|
125
|
+
raise PGMCoreException("Can not update grid without output_data")
|
126
|
+
for array_name in PGM_ARRAYS:
|
127
|
+
if array_name in self.output_data.keys():
|
128
|
+
internal_array = getattr(self.grid, array_name)
|
129
|
+
pgm_output_array = self.output_data[array_name]
|
130
|
+
fields = self._match_dtypes(pgm_output_array.dtype, internal_array.dtype)
|
131
|
+
internal_array[fields] = pgm_output_array[fields]
|
132
|
+
|
133
|
+
@staticmethod
|
134
|
+
def _match_dtypes(first_dtype: np.dtype, second_dtype: np.dtype):
|
135
|
+
return list(set(first_dtype.names).intersection(set(second_dtype.names))) # type: ignore[arg-type]
|
136
|
+
|
137
|
+
def _setup_model(self):
|
138
|
+
self.input_data = self.input_data or self.create_input_from_grid()
|
139
|
+
self.model = PowerGridModel(self.input_data, system_frequency=self.system_frequency)
|
140
|
+
return self.model
|
File without changes
|
@@ -0,0 +1,43 @@
|
|
1
|
+
# SPDX-FileCopyrightText: Contributors to the Power Grid Model project <powergridmodel@lfenergy.org>
|
2
|
+
#
|
3
|
+
# SPDX-License-Identifier: MPL-2.0
|
4
|
+
|
5
|
+
"""Imports all the arrays, so that array can be imported as follows:
|
6
|
+
from power_grid_model_ds._core.model.arrays import MyArray
|
7
|
+
"""
|
8
|
+
|
9
|
+
from power_grid_model_ds._core.model.arrays.pgm_arrays import (
|
10
|
+
AsymVoltageSensorArray,
|
11
|
+
Branch3Array,
|
12
|
+
BranchArray,
|
13
|
+
IdArray,
|
14
|
+
LineArray,
|
15
|
+
LinkArray,
|
16
|
+
NodeArray,
|
17
|
+
SourceArray,
|
18
|
+
SymGenArray,
|
19
|
+
SymLoadArray,
|
20
|
+
SymPowerSensorArray,
|
21
|
+
SymVoltageSensorArray,
|
22
|
+
ThreeWindingTransformerArray,
|
23
|
+
TransformerArray,
|
24
|
+
TransformerTapRegulatorArray,
|
25
|
+
)
|
26
|
+
|
27
|
+
__all__ = [
|
28
|
+
"AsymVoltageSensorArray",
|
29
|
+
"Branch3Array",
|
30
|
+
"BranchArray",
|
31
|
+
"IdArray",
|
32
|
+
"LineArray",
|
33
|
+
"LinkArray",
|
34
|
+
"NodeArray",
|
35
|
+
"SourceArray",
|
36
|
+
"SymLoadArray",
|
37
|
+
"SymGenArray",
|
38
|
+
"SymPowerSensorArray",
|
39
|
+
"SymVoltageSensorArray",
|
40
|
+
"ThreeWindingTransformerArray",
|
41
|
+
"TransformerArray",
|
42
|
+
"TransformerTapRegulatorArray",
|
43
|
+
]
|
File without changes
|