From cbcad425fc7363f61387e9fa1ae42875db822d52 Mon Sep 17 00:00:00 2001 From: jaapschoutenalliander Date: Thu, 23 Jan 2025 20:06:53 +0100 Subject: [PATCH 1/5] feat: improve downstream nodes performance with local search Signed-off-by: jaapschoutenalliander --- .../_core/model/graphs/models/base.py | 21 ++++ .../_core/model/graphs/models/rustworkx.py | 11 ++ .../_core/model/grids/base.py | 12 +-- tests/performance/_constants.py | 18 ++-- tests/performance/_helpers.py | 101 +++++------------- tests/performance/array_performance_tests.py | 54 +++++----- tests/performance/filter_performance_tests.py | 14 +-- tests/performance/graph_performance_tests.py | 19 ++-- tests/performance/grid_performance_tests.py | 30 ++++++ tests/unit/model/grids/test_grid_search.py | 3 + 10 files changed, 156 insertions(+), 127 deletions(-) create mode 100644 tests/performance/grid_performance_tests.py diff --git a/src/power_grid_model_ds/_core/model/graphs/models/base.py b/src/power_grid_model_ds/_core/model/graphs/models/base.py index 2a4a577..0b817df 100644 --- a/src/power_grid_model_ds/_core/model/graphs/models/base.py +++ b/src/power_grid_model_ds/_core/model/graphs/models/base.py @@ -235,8 +235,26 @@ def get_connected( nodes_to_ignore=self._externals_to_internals(nodes_to_ignore), inclusive=inclusive, ) + return self._internals_to_externals(nodes) + def get_downstream_nodes(self, node_id: int, stop_node_ids: list[int], inclusive: bool = False) -> list[int]: + """Find all nodes connected to the node_id + args: + node_id: node id to start the search from + stop_node_ids: list of node ids to stop the search at + inclusive: whether to include the given node id in the result + returns: + list of node ids sorted by distance, downstream of to the node id + """ + downstream_nodes = self._get_downstream_nodes( + node_id=self.external_to_internal(node_id), + stop_node_ids=self._externals_to_internals(stop_node_ids), + inclusive=inclusive, + ) + + return self._internals_to_externals(downstream_nodes) + def find_fundamental_cycles(self) -> list[list[int]]: """Find all fundamental cycles in the graph. Returns: @@ -273,6 +291,9 @@ def _branch_is_relevant(self, branch: BranchArray) -> bool: @abstractmethod def _get_connected(self, node_id: int, nodes_to_ignore: list[int], inclusive: bool = False) -> list[int]: ... + @abstractmethod + def _get_downstream_nodes(self, node_id: int, stop_node_ids: list[int], inclusive: bool = False) -> list[int]: ... + @abstractmethod def _has_branch(self, from_node_id, to_node_id) -> bool: ... diff --git a/src/power_grid_model_ds/_core/model/graphs/models/rustworkx.py b/src/power_grid_model_ds/_core/model/graphs/models/rustworkx.py index 6e1b7cd..2406bc2 100644 --- a/src/power_grid_model_ds/_core/model/graphs/models/rustworkx.py +++ b/src/power_grid_model_ds/_core/model/graphs/models/rustworkx.py @@ -99,6 +99,15 @@ def _get_connected(self, node_id: int, nodes_to_ignore: list[int], inclusive: bo return connected_nodes + def _get_downstream_nodes(self, node_id: int, stop_node_ids: list[int], inclusive: bool = False) -> list[int]: + visitor = _NodeVisitor(stop_node_ids) + rx.bfs_search(self._graph, [node_id], visitor) + connected_nodes = visitor.nodes + path_to_substation, _ = self._get_shortest_path(node_id, visitor.discovered_nodes_to_ignore[0]) + if inclusive: + _ = path_to_substation.pop(0) + return [node for node in connected_nodes if node not in path_to_substation] + def _find_fundamental_cycles(self) -> list[list[int]]: """Find all fundamental cycles in the graph using Rustworkx. @@ -112,8 +121,10 @@ class _NodeVisitor(BFSVisitor): def __init__(self, nodes_to_ignore: list[int]): self.nodes_to_ignore = nodes_to_ignore self.nodes: list[int] = [] + self.discovered_nodes_to_ignore: list[int] = [] def discover_vertex(self, v): if v in self.nodes_to_ignore: + self.discovered_nodes_to_ignore.append(v) raise PruneSearch self.nodes.append(v) diff --git a/src/power_grid_model_ds/_core/model/grids/base.py b/src/power_grid_model_ds/_core/model/grids/base.py index 0183ca1..18c94c8 100644 --- a/src/power_grid_model_ds/_core/model/grids/base.py +++ b/src/power_grid_model_ds/_core/model/grids/base.py @@ -331,6 +331,7 @@ def get_nearest_substation_node(self, node_id: int): def get_downstream_nodes(self, node_id: int, inclusive: bool = False): """Get the downstream nodes from a node. + Assuming each node has a single feeding substation and the grid is radial Example: given this graph: [1] - [2] - [3] - [4], with 1 being a substation node @@ -349,15 +350,14 @@ def get_downstream_nodes(self, node_id: int, inclusive: bool = False): Returns: list[int]: The downstream nodes. """ - substation_node_id = self.get_nearest_substation_node(node_id).id.item() + substation_nodes = self.node.filter(node_type=NodeType.SUBSTATION_NODE.value) - if node_id == substation_node_id: + if node_id in substation_nodes.id: raise NotImplementedError("get_downstream_nodes is not implemented for substation nodes!") - path_to_substation, _ = self.graphs.active_graph.get_shortest_path(node_id, substation_node_id) - upstream_node = path_to_substation[1] - - return self.graphs.active_graph.get_connected(node_id, nodes_to_ignore=[upstream_node], inclusive=inclusive) + return self.graphs.active_graph.get_downstream_nodes( + node_id=node_id, stop_node_ids=list(substation_nodes.id), inclusive=inclusive + ) def cache(self, cache_dir: Path, cache_name: str, compress: bool = True): """Cache Grid to a folder diff --git a/tests/performance/_constants.py b/tests/performance/_constants.py index f114754..e531976 100644 --- a/tests/performance/_constants.py +++ b/tests/performance/_constants.py @@ -6,18 +6,18 @@ "dtype = [('id', ' +# +# SPDX-License-Identifier: MPL-2.0 + +from tests.performance._helpers import do_performance_test + +# pylint: disable=missing-function-docstring + + +def test_get_downstream_nodes_performance(): + setup_code = { + "grid": "import numpy as np;" + + "from power_grid_model_ds.enums import NodeType;" + + "from power_grid_model_ds import Grid;" + + "from power_grid_model_ds.generators import RadialGridGenerator;" + + "from power_grid_model_ds.graph_models import RustworkxGraphModel;" + + "grid=RadialGridGenerator(nr_nodes={size}, grid_class=Grid, graph_model=RustworkxGraphModel).run();" + + "non_substation_node = grid.arrays.nodes.filter(node_type=NodeType.UNSPECIFIED).id;" + + "node_id = np.random.choice(non_substation_node)" + } + + code_to_test = [ + "grid.get_downstream_nodes(node_id)", + ] + + do_performance_test(code_to_test, [10, 1000, 5000], 100, setup_code) + + +if __name__ == "__main__": + test_get_downstream_nodes_performance() diff --git a/tests/unit/model/grids/test_grid_search.py b/tests/unit/model/grids/test_grid_search.py index f3ac243..1ed1e06 100644 --- a/tests/unit/model/grids/test_grid_search.py +++ b/tests/unit/model/grids/test_grid_search.py @@ -30,6 +30,9 @@ def test_get_downstream_nodes(basic_grid): downstream_nodes = basic_grid.get_downstream_nodes(node_id=102) assert {103, 106} == set(downstream_nodes) + downstream_nodes = basic_grid.get_downstream_nodes(node_id=102, inclusive=True) + assert {102, 103, 106} == set(downstream_nodes) + def test_get_downstream_nodes_from_substation_node(basic_grid): """Test that get_downstream_nodes raises the expected error when From 3d72bab35f8b639c5c3ba02cb079accce2ac4d08 Mon Sep 17 00:00:00 2001 From: jaapschoutenalliander Date: Fri, 24 Jan 2025 13:17:43 +0100 Subject: [PATCH 2/5] test: add testing on sorting Signed-off-by: jaapschoutenalliander --- tests/unit/model/grids/test_grid_search.py | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/tests/unit/model/grids/test_grid_search.py b/tests/unit/model/grids/test_grid_search.py index 1ed1e06..034f1cd 100644 --- a/tests/unit/model/grids/test_grid_search.py +++ b/tests/unit/model/grids/test_grid_search.py @@ -5,6 +5,7 @@ import numpy as np import pytest +from power_grid_model_ds import Grid from power_grid_model_ds._core.model.arrays.base.errors import RecordDoesNotExist from power_grid_model_ds._core.model.enums.nodes import NodeType @@ -25,13 +26,19 @@ def test_grid_get_nearest_substation_node_no_substation(basic_grid): basic_grid.get_nearest_substation_node(node_id=103) -def test_get_downstream_nodes(basic_grid): +def test_get_downstream_nodes(basic_grid: Grid): """Test that get_downstream_nodes returns the expected nodes.""" + # Move the open line to be able to test sorting of nodes by distance correctly + basic_grid.make_active(basic_grid.line.get(203)) + basic_grid.make_inactive(basic_grid.link.get(601)) downstream_nodes = basic_grid.get_downstream_nodes(node_id=102) - assert {103, 106} == set(downstream_nodes) + assert downstream_nodes[-1] == 104 # Furthest away + assert {103, 104, 106} == set(downstream_nodes) downstream_nodes = basic_grid.get_downstream_nodes(node_id=102, inclusive=True) - assert {102, 103, 106} == set(downstream_nodes) + assert downstream_nodes[0] == 102 + assert downstream_nodes[-1] == 104 + assert {102, 103, 104, 106} == set(downstream_nodes) def test_get_downstream_nodes_from_substation_node(basic_grid): From da0c33d1a39a5897819a3465d26b834c6e33a113 Mon Sep 17 00:00:00 2001 From: jaapschoutenalliander Date: Tue, 28 Jan 2025 19:33:34 +0100 Subject: [PATCH 3/5] chore: update performance test Signed-off-by: jaapschoutenalliander --- tests/performance/grid_performance_tests.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/performance/grid_performance_tests.py b/tests/performance/grid_performance_tests.py index 5d39f93..db2fc28 100644 --- a/tests/performance/grid_performance_tests.py +++ b/tests/performance/grid_performance_tests.py @@ -15,7 +15,7 @@ def test_get_downstream_nodes_performance(): + "from power_grid_model_ds.generators import RadialGridGenerator;" + "from power_grid_model_ds.graph_models import RustworkxGraphModel;" + "grid=RadialGridGenerator(nr_nodes={size}, grid_class=Grid, graph_model=RustworkxGraphModel).run();" - + "non_substation_node = grid.arrays.nodes.filter(node_type=NodeType.UNSPECIFIED).id;" + + "non_substation_node = grid.node.filter(node_type=NodeType.UNSPECIFIED).id;" + "node_id = np.random.choice(non_substation_node)" } From fc99bc0e5c2d1b4e9d1119a69ce6b2e64d5e37a8 Mon Sep 17 00:00:00 2001 From: Thijs Baaijen <13253091+Thijss@users.noreply.github.com> Date: Wed, 29 Jan 2025 11:09:15 +0100 Subject: [PATCH 4/5] Update test_get_downstream_nodes Co-authored-by: jaapschoutenalliander Signed-off-by: Thijs Baaijen <13253091+Thijss@users.noreply.github.com> --- tests/unit/model/grids/test_grid_search.py | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/tests/unit/model/grids/test_grid_search.py b/tests/unit/model/grids/test_grid_search.py index f3ac243..034f1cd 100644 --- a/tests/unit/model/grids/test_grid_search.py +++ b/tests/unit/model/grids/test_grid_search.py @@ -5,6 +5,7 @@ import numpy as np import pytest +from power_grid_model_ds import Grid from power_grid_model_ds._core.model.arrays.base.errors import RecordDoesNotExist from power_grid_model_ds._core.model.enums.nodes import NodeType @@ -25,10 +26,19 @@ def test_grid_get_nearest_substation_node_no_substation(basic_grid): basic_grid.get_nearest_substation_node(node_id=103) -def test_get_downstream_nodes(basic_grid): +def test_get_downstream_nodes(basic_grid: Grid): """Test that get_downstream_nodes returns the expected nodes.""" + # Move the open line to be able to test sorting of nodes by distance correctly + basic_grid.make_active(basic_grid.line.get(203)) + basic_grid.make_inactive(basic_grid.link.get(601)) downstream_nodes = basic_grid.get_downstream_nodes(node_id=102) - assert {103, 106} == set(downstream_nodes) + assert downstream_nodes[-1] == 104 # Furthest away + assert {103, 104, 106} == set(downstream_nodes) + + downstream_nodes = basic_grid.get_downstream_nodes(node_id=102, inclusive=True) + assert downstream_nodes[0] == 102 + assert downstream_nodes[-1] == 104 + assert {102, 103, 104, 106} == set(downstream_nodes) def test_get_downstream_nodes_from_substation_node(basic_grid): From 7cb6fa24a5c6fd5790783dcace41b8e76cf6b2e6 Mon Sep 17 00:00:00 2001 From: Thijs Baaijen <13253091+Thijss@users.noreply.github.com> Date: Wed, 29 Jan 2025 11:09:24 +0100 Subject: [PATCH 5/5] Update performance tests Co-authored-by: jaapschoutenalliander Signed-off-by: Thijs Baaijen <13253091+Thijss@users.noreply.github.com> --- tests/performance/_constants.py | 18 ++-- tests/performance/_helpers.py | 101 +++++------------- tests/performance/array_performance_tests.py | 54 +++++----- tests/performance/filter_performance_tests.py | 14 +-- tests/performance/graph_performance_tests.py | 19 ++-- tests/performance/grid_performance_tests.py | 30 ++++++ 6 files changed, 115 insertions(+), 121 deletions(-) create mode 100644 tests/performance/grid_performance_tests.py diff --git a/tests/performance/_constants.py b/tests/performance/_constants.py index f114754..e531976 100644 --- a/tests/performance/_constants.py +++ b/tests/performance/_constants.py @@ -6,18 +6,18 @@ "dtype = [('id', ' +# +# SPDX-License-Identifier: MPL-2.0 + +from tests.performance._helpers import do_performance_test + +# pylint: disable=missing-function-docstring + + +def test_get_downstream_nodes_performance(): + setup_code = { + "grid": "import numpy as np;" + + "from power_grid_model_ds.enums import NodeType;" + + "from power_grid_model_ds import Grid;" + + "from power_grid_model_ds.generators import RadialGridGenerator;" + + "from power_grid_model_ds.graph_models import RustworkxGraphModel;" + + "grid=RadialGridGenerator(nr_nodes={size}, grid_class=Grid, graph_model=RustworkxGraphModel).run();" + + "non_substation_node = grid.node.filter(node_type=NodeType.UNSPECIFIED).id;" + + "node_id = np.random.choice(non_substation_node)" + } + + code_to_test = [ + "grid.get_downstream_nodes(node_id)", + ] + + do_performance_test(code_to_test, [10, 1000, 5000], 100, setup_code) + + +if __name__ == "__main__": + test_get_downstream_nodes_performance()