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,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
@@ -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
+ ]