From 69600824d25936fa2bbe2a0b4343cb4cf955b025 Mon Sep 17 00:00:00 2001 From: Thijs Baaijen <13253091+Thijss@users.noreply.github.com> Date: Wed, 5 Feb 2025 10:03:49 +0100 Subject: [PATCH 01/13] Refactor Graph tests Signed-off-by: Thijs Baaijen <13253091+Thijss@users.noreply.github.com> --- tests/unit/model/graphs/test_graph_model.py | 435 +++++++++----------- 1 file changed, 190 insertions(+), 245 deletions(-) diff --git a/tests/unit/model/graphs/test_graph_model.py b/tests/unit/model/graphs/test_graph_model.py index 628c77b..e4fe0db 100644 --- a/tests/unit/model/graphs/test_graph_model.py +++ b/tests/unit/model/graphs/test_graph_model.py @@ -1,282 +1,208 @@ # SPDX-FileCopyrightText: Contributors to the Power Grid Model project # # SPDX-License-Identifier: MPL-2.0 - -"""Grid tests""" - import numpy as np import pytest from numpy.testing import assert_array_equal -from power_grid_model_ds._core.model.graphs.errors import MissingBranchError, MissingNodeError, NoPathBetweenNodes +from power_grid_model_ds.errors import MissingBranchError, MissingNodeError, NoPathBetweenNodes # pylint: disable=missing-function-docstring,missing-class-docstring -def test_graph_initialize(graph): - """We test whether we can make a simple graph of 2 nodes and 1 branch""" - graph.add_node(1) - graph.add_node(2) - graph.add_branch(1, 2) - - # both nodes exist in the external2graph mapping - assert 1 in graph.external_ids - assert 2 in graph.external_ids - # the graph has the correct size - assert 2 == graph.nr_nodes - assert 1 == graph.nr_branches - - -def test_graph_has_branch(graph): - graph.add_node(1) - graph.add_node(2) - graph.add_branch(1, 2) - - assert graph.has_branch(1, 2) - assert graph.has_branch(2, 1) # reversed should work too - assert not graph.has_branch(1, 3) - - -def test_graph_delete_branch(graph): - """Test whether a branch is deleted correctly""" - graph.add_node(1) - graph.add_node(2) - graph.add_branch(1, 2) - graph.add_node(3) - - assert graph.has_branch(1, 2) - - assert 0 == graph.external_to_internal(1) - assert 1 == graph.external_to_internal(2) - assert 2 == graph.external_to_internal(3) - - assert 3 == graph.nr_nodes - assert 1 == graph.nr_branches - assert 1 in graph.external_ids - assert 2 in graph.external_ids - - # now delete the 1 -> 2 branch - graph.delete_branch(1, 2) - assert not graph.has_branch(1, 2) - - assert 0 == graph.external_to_internal(1) - assert 1 == graph.external_to_internal(2) - assert 2 == graph.external_to_internal(3) - - # check the graph size - assert 3 == graph.nr_nodes - assert 0 == graph.nr_branches - assert 1 in graph.external_ids - assert 2 in graph.external_ids - - -def test_graph_add_branch(graph): - """Test whether a branch is deleted correctly""" - graph.add_node(1) - graph.add_node(2) - graph.add_branch(1, 2) - assert graph.has_branch(1, 2) - - with pytest.raises(MissingNodeError): - graph.add_branch(1, 3) - - -def test_has_node(graph): - graph.add_node(1) - assert graph.has_node(1) - assert not graph.has_node(2) - - -# pylint: disable=protected-access -def test_graph_mapping_of_ids_after_delete_node(graph): - """Test whether the node mapping stays correct after deleting a node""" - graph.add_node(1) - graph.add_node(2) - graph.add_node(3) - - internal_id_0 = graph.external_to_internal(1) - internal_id_1 = graph.external_to_internal(2) - internal_id_2 = graph.external_to_internal(3) - assert graph._has_node(internal_id_0) - assert graph._has_node(internal_id_1) - assert graph._has_node(internal_id_2) - - # now delete node 2, this can change the internal mapping - graph.delete_node(2) - - internal_id_0 = graph.external_to_internal(1) - internal_id_2 = graph.external_to_internal(3) - - assert graph._has_node(internal_id_0) - assert graph._has_node(internal_id_2) - - -def test_graph_delete_node(graph): - """Test whether a node is deleted correctly""" - graph.add_node(1) - graph.add_node(2) - graph.add_branch(1, 2) - - # now delete the 1 -> 2 branch - graph.delete_node(1) - - # check the graph size, the branch to 1 is also removed! - assert 1 == graph.nr_nodes - assert 0 == graph.nr_branches - # check whether the edge is removed from the graph - assert 2 in graph.external_ids - assert 1 not in graph.external_ids - - assert not graph.has_branch(1, 2) - - -def test_remove_invalid_node_raises_error(graph): - """Test whether an error is raised when nodes are removed incorrectly""" - graph.add_node(1) - graph.add_node(2) - - # removing non existent nodes and branches raises a error by default - with pytest.raises(MissingNodeError): - graph.delete_node(3) - - with pytest.raises(MissingBranchError): - graph.delete_branch(1, 3) - - with pytest.raises(MissingBranchError): - graph.delete_branch(1, 2) - +class TestGraphModifications: + def test_graph_add_node_and_branch(self, graph): + graph.add_node(1) + graph.add_node(2) + graph.add_branch(1, 2) -def test_remove_invalid_node_without_error(graph): - graph.delete_node(3, raise_on_fail=False) - graph.delete_branch(1, 3, raise_on_fail=False) + # both nodes exist in the external2graph mapping + assert 1 in graph.external_ids + assert 2 in graph.external_ids + # the graph has the correct size + assert 2 == graph.nr_nodes + assert 1 == graph.nr_branches + def test_has_node(self, graph): + graph.add_node(1) + assert graph.has_node(1) + assert not graph.has_node(2) -def test_shortest_path(graph_with_5_nodes): - """Test shortest path algorithm on circular network""" - graph_with_5_nodes.add_branch(1, 2) - graph_with_5_nodes.add_branch(2, 3) + def test_has_branch(self, graph): + graph.add_node(1) + graph.add_node(2) + graph.add_branch(1, 2) - path, length = graph_with_5_nodes.get_shortest_path(1, 3) + assert graph.has_branch(1, 2) + assert graph.has_branch(2, 1) # reversed should work too + assert not graph.has_branch(1, 3) - assert path == [1, 2, 3] - assert length == 2 + def test_add_invalid_branch(self, graph): + graph.add_node(1) + graph.add_node(2) + graph.add_branch(1, 2) + assert graph.has_branch(1, 2) - path, length = graph_with_5_nodes.get_shortest_path(1, 1) - assert path == [1] - assert length == 0 + with pytest.raises(MissingNodeError): + graph.add_branch(1, 3) + def test_delete_invalid_node_without_error(self, graph): + graph.delete_node(3, raise_on_fail=False) -def test_shortest_path_no_path(graph_with_5_nodes): - """Test that shortest path algorithm raises an error - when path between two nodes does not exist""" - graph_with_5_nodes.add_branch(1, 2) - graph_with_5_nodes.add_branch(3, 4) - graph_with_5_nodes.add_branch(4, 5) + def test_delete_invalid_branch_without_error(self, graph): + graph.delete_branch(1, 3, raise_on_fail=False) - with pytest.raises(NoPathBetweenNodes): - graph_with_5_nodes.get_shortest_path(1, 5) + def test_graph_delete_connected_node(self, graph): + graph.add_node(1) + graph.add_node(2) + graph.add_branch(1, 2) + graph.delete_node(1) # also deletes branch 1-2 -def test_all_paths(graph_with_5_nodes): - """Test all paths algorithm on circular network""" - graph_with_5_nodes.add_branch(1, 2) - graph_with_5_nodes.add_branch(2, 3) - graph_with_5_nodes.add_branch(3, 4) - graph_with_5_nodes.add_branch(4, 5) - graph_with_5_nodes.add_branch(5, 1) + assert 1 == graph.nr_nodes + assert 0 == graph.nr_branches + assert 2 in graph.external_ids + assert 1 not in graph.external_ids + assert not graph.has_branch(1, 2) - paths = graph_with_5_nodes.get_all_paths(1, 3) + def test_remove_invalid_node(self, graph): + graph.add_node(1) + graph.add_node(2) - assert len(paths) == 2 - assert [1, 2, 3] in paths - assert [1, 5, 4, 3] in paths + with pytest.raises(MissingNodeError): + graph.delete_node(3) + def test_remove_invalid_branch(self, graph): + graph.add_node(1) + graph.add_node(2) -def test_all_paths_no_path(graph_with_5_nodes): - """Test that all paths algorithm raises an error when path between two nodes does not exist""" - with pytest.raises(NoPathBetweenNodes): - graph_with_5_nodes.get_all_paths(1, 2) + with pytest.raises(MissingBranchError): + graph.delete_branch(1, 3) + with pytest.raises(MissingBranchError): + graph.delete_branch(1, 2) -def test_get_components(graph_with_2_routes): - """Test whether routes can be correcty extracted""" - graph = graph_with_2_routes - graph.add_node(99) - graph.add_branch(1, 99) - substation_nodes = np.array([1]) + # pylint: disable=protected-access + def test_internal_ids_after_node_deletion(self, graph): + graph.add_node(1) + graph.add_node(2) + graph.add_node(3) - components = graph.get_components(substation_nodes=substation_nodes) + internal_id_0 = graph.external_to_internal(1) + internal_id_1 = graph.external_to_internal(2) + internal_id_2 = graph.external_to_internal(3) + assert graph._has_node(internal_id_0) + assert graph._has_node(internal_id_1) + assert graph._has_node(internal_id_2) - assert len(components) == 3 - assert set(components[0]) == {2, 3} - assert set(components[1]) == {4, 5} - assert set(components[2]) == {99} + # now delete node 2, this can change the internal mapping + graph.delete_node(2) + internal_id_0 = graph.external_to_internal(1) + internal_id_2 = graph.external_to_internal(3) -def test_from_arrays(basic_grid): - new_graph = basic_grid.graphs.complete_graph.__class__.from_arrays(basic_grid) - assert_array_equal(new_graph.external_ids, basic_grid.node.id) + assert graph._has_node(internal_id_0) + assert graph._has_node(internal_id_2) -def test_get_shortest_path(graph_with_2_routes): - graph = graph_with_2_routes - path = graph.get_shortest_path(1, 3) - assert path == ([1, 2, 3], 2) - - -@pytest.mark.parametrize( - "additional_edges, nodes_in_cycles", - [ - ([], set()), - ([(2, 5)], {1, 2, 5}), - ([(1, 2)], {1, 2}), - ([(1, 2), (1, 2)], {1, 2}), - ([(2, 4)], {1, 2, 4, 5}), - ([(1, 5), (3, 5)], {1, 2, 3, 5}), - ], -) -def test_find_nodes_in_cycle(graph_with_2_routes, additional_edges, nodes_in_cycles): - graph = graph_with_2_routes - for u, v in additional_edges: - graph.add_branch(u, v) - - result = graph.find_fundamental_cycles() - assert len(result) == len(set(additional_edges)) - for node_path in result: - assert node_path[0] == node_path[-1] - assert len(node_path) == len(set(node_path)) + 1 - assert all(node in nodes_in_cycles for node in node_path) - - -def test_find_nodes_in_cycle_multiple_trees(graph): - """The following graph contains 2 unconnected subgraphs of 4 nodes each. - Both subgraphs contain a cycle. - Visual representation: - Subgraph 1: - 1 -- 2 - | | - 4 -- 3 - Subgraph 2: - 5 - 6 -- 7 - | | - '--- 8 - """ - edges = [(1, 2), (2, 3), (3, 4), (1, 4), (5, 6), (6, 7), (6, 8), (7, 8)] - for _id in range(1, 9): - graph.add_node(_id) - for u, v in edges: - graph.add_branch(u, v) - - # The MST is not unique, so the node paths can be in any order. - # For example: [1,2,3,4,1] and [4,3,2,1,4] are both valid return values. - # We do know exactly which nodes are in the cycle and that the first and last node are the same. - result = graph.find_fundamental_cycles() - result_as_sets = {frozenset(nodes) for nodes in result} - assert len(result) == 2 - assert frozenset([1, 2, 3, 4]) in result_as_sets - assert frozenset([6, 7, 8]) in result_as_sets - assert result[0][0] == result[0][-1] - assert result[1][0] == result[1][-1] +class TestPathMethods: + def test_get_shortest_path(self, graph_with_2_routes): + graph = graph_with_2_routes + path = graph.get_shortest_path(1, 3) + assert path == ([1, 2, 3], 2) + + def test_shortest_path_on_circular_network(self, graph_with_5_nodes): + graph_with_5_nodes.add_branch(1, 2) + graph_with_5_nodes.add_branch(2, 3) + + path, length = graph_with_5_nodes.get_shortest_path(1, 3) + + assert path == [1, 2, 3] + assert length == 2 + + path, length = graph_with_5_nodes.get_shortest_path(1, 1) + assert path == [1] + assert length == 0 + + def test_shortest_path_no_path(self, graph_with_5_nodes): + graph_with_5_nodes.add_branch(1, 2) + graph_with_5_nodes.add_branch(3, 4) + graph_with_5_nodes.add_branch(4, 5) + + with pytest.raises(NoPathBetweenNodes): + graph_with_5_nodes.get_shortest_path(1, 5) + + def test_all_paths_on_circular_network(self, graph_with_5_nodes): + graph_with_5_nodes.add_branch(1, 2) + graph_with_5_nodes.add_branch(2, 3) + graph_with_5_nodes.add_branch(3, 4) + graph_with_5_nodes.add_branch(4, 5) + graph_with_5_nodes.add_branch(5, 1) + + paths = graph_with_5_nodes.get_all_paths(1, 3) + + assert len(paths) == 2 + assert [1, 2, 3] in paths + assert [1, 5, 4, 3] in paths + + def test_all_paths_no_path(self, graph_with_5_nodes): + with pytest.raises(NoPathBetweenNodes): + graph_with_5_nodes.get_all_paths(1, 2) + + +class TestFindFundamentalCycles: + @pytest.mark.parametrize( + "additional_edges, nodes_in_cycles", + [ + ([], set()), + ([(2, 5)], {1, 2, 5}), + ([(1, 2)], {1, 2}), + ([(1, 2), (1, 2)], {1, 2}), + ([(2, 4)], {1, 2, 4, 5}), + ([(1, 5), (3, 5)], {1, 2, 3, 5}), + ], + ) + def test_find_fundamental_cycles(self, graph_with_2_routes, additional_edges, nodes_in_cycles): + graph = graph_with_2_routes + for u, v in additional_edges: + graph.add_branch(u, v) + + result = graph.find_fundamental_cycles() + assert len(result) == len(set(additional_edges)) + for node_path in result: + assert node_path[0] == node_path[-1] + assert len(node_path) == len(set(node_path)) + 1 + assert all(node in nodes_in_cycles for node in node_path) + + def test_find_fundamental_cycles_multiple_trees(self, graph): + """The following graph contains 2 unconnected subgraphs of 4 nodes each. + Both subgraphs contain a cycle. + Visual representation: + Subgraph 1: + 1 -- 2 + | | + 4 -- 3 + Subgraph 2: + 5 - 6 -- 7 + | | + '--- 8 + """ + edges = [(1, 2), (2, 3), (3, 4), (1, 4), (5, 6), (6, 7), (6, 8), (7, 8)] + for _id in range(1, 9): + graph.add_node(_id) + for u, v in edges: + graph.add_branch(u, v) + + # The MST is not unique, so the node paths can be in any order. + # For example: [1,2,3,4,1] and [4,3,2,1,4] are both valid return values. + # We do know exactly which nodes are in the cycle and that the first and last node are the same. + result = graph.find_fundamental_cycles() + result_as_sets = {frozenset(nodes) for nodes in result} + assert len(result) == 2 + assert frozenset([1, 2, 3, 4]) in result_as_sets + assert frozenset([6, 7, 8]) in result_as_sets + assert result[0][0] == result[0][-1] + assert result[1][0] == result[1][-1] class TestGetConnected: @@ -320,3 +246,22 @@ def test_get_connected_ignore_multiple_nodes(self, graph_with_2_routes): connected_nodes = graph.get_connected(node_id=1, nodes_to_ignore=[2, 4]) assert {5} == set(connected_nodes) + + +def test_get_components(graph_with_2_routes): + graph = graph_with_2_routes + graph.add_node(99) + graph.add_branch(1, 99) + substation_nodes = np.array([1]) + + components = graph.get_components(substation_nodes=substation_nodes) + + assert len(components) == 3 + assert set(components[0]) == {2, 3} + assert set(components[1]) == {4, 5} + assert set(components[2]) == {99} + + +def test_from_arrays(basic_grid): + new_graph = basic_grid.graphs.complete_graph.__class__.from_arrays(basic_grid) + assert_array_equal(new_graph.external_ids, basic_grid.node.id) From 09595b9d55a13de91cf5234720b7f42153137db8 Mon Sep 17 00:00:00 2001 From: Thijs Baaijen <13253091+Thijss@users.noreply.github.com> Date: Wed, 5 Feb 2025 10:41:53 +0100 Subject: [PATCH 02/13] Feature: add Grid.from_txt method Signed-off-by: Thijs Baaijen <13253091+Thijss@users.noreply.github.com> --- .../_core/model/grids/_text_sources.py | 16 +-- .../_core/model/grids/base.py | 16 ++- tests/unit/model/grids/test_grid_base.py | 116 +++++++++++------- 3 files changed, 89 insertions(+), 59 deletions(-) diff --git a/src/power_grid_model_ds/_core/model/grids/_text_sources.py b/src/power_grid_model_ds/_core/model/grids/_text_sources.py index c075bde..dc670cb 100644 --- a/src/power_grid_model_ds/_core/model/grids/_text_sources.py +++ b/src/power_grid_model_ds/_core/model/grids/_text_sources.py @@ -5,7 +5,6 @@ """Create a grid from text a text file""" import logging -from pathlib import Path from typing import TYPE_CHECKING from power_grid_model_ds._core.model.enums.nodes import NodeType @@ -35,25 +34,20 @@ class TextSource: def __init__(self, grid_class: type["Grid"]): self.grid = grid_class.empty() - def load_grid_from_path(self, path: Path): - """Load assets from text file & sort them by id so that - they are ready to be appended to the grid""" - - txt_nodes, txt_branches = self.read_txt(path) + def load_from_txt(self, txt_lines: list[str]) -> "Grid": + """Load a grid from text""" + txt_nodes, txt_branches = self.read_txt(txt_lines) self.add_nodes(txt_nodes) self.add_branches(txt_branches) self.grid.set_feeder_ids() return self.grid @staticmethod - def read_txt(path: Path) -> tuple[set, dict]: + def read_txt(txt_lines: list[str]) -> tuple[set, dict]: """Extract assets from text""" - with open(path, "r", encoding="utf-8") as f: - txt_rows = f.readlines() - txt_nodes = set() txt_branches = {} - for text_line in txt_rows: + for text_line in txt_lines: if not text_line.strip() or text_line.startswith("#"): continue # skip empty lines and comments try: 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..0721f69 100644 --- a/src/power_grid_model_ds/_core/model/grids/base.py +++ b/src/power_grid_model_ds/_core/model/grids/base.py @@ -408,6 +408,17 @@ def _from_pickle(cls, pickle_path: Path): raise TypeError(f"{pickle_path.name} is not a valid {cls.__name__} cache.") return grid + @classmethod + def from_txt(cls, txt_lines: list[str]): + """Build a grid from a list of strings + + See the documentation for the expected format of the txt_lines + + Example: + >>> Grid.from_txt(["1 2", "2 3", "3 4 transformer", "4 5", "S1 6"]) + """ + return TextSource(grid_class=cls).load_from_txt(txt_lines) + @classmethod # pylint: disable=arguments-differ def from_txt_file(cls, txt_file_path: Path): @@ -416,8 +427,9 @@ def from_txt_file(cls, txt_file_path: Path): Args: txt_file_path (Path): The path to the txt file """ - text_source = TextSource(grid_class=cls) - return text_source.load_grid_from_path(txt_file_path) + with open(txt_file_path, "r", encoding="utf-8") as f: + txt_lines = f.readlines() + return TextSource(grid_class=cls).load_from_txt(txt_lines) def set_feeder_ids(self): """Sets feeder and substation id properties in the grids arrays""" diff --git a/tests/unit/model/grids/test_grid_base.py b/tests/unit/model/grids/test_grid_base.py index 8f9160a..39b68a3 100644 --- a/tests/unit/model/grids/test_grid_base.py +++ b/tests/unit/model/grids/test_grid_base.py @@ -266,52 +266,6 @@ def test_grid_make_inactive_to_side(basic_grid): assert 0 == target_line_after.to_status -def test_from_txt_file(tmp_path): - """Test that Grid can be created from txt file""" - txt_file = tmp_path / "tmp_grid" - txt_file.write_text("S1 2\nS1 3 open\n2 7\n3 5\n3 6 transformer\n5 7\n7 8\n8 9", encoding="utf-8") - grid = Grid.from_txt_file(txt_file) - txt_file.unlink() - - assert 8 == grid.node.size - assert 1 == grid.branches.filter(to_status=0).size - assert 1 == grid.transformer.size - np.testing.assert_array_equal([14, 10, 11, 12, 13, 15, 16, 17], grid.branches.id) - - -def test_from_txt_file_with_branch_ids(tmp_path): - txt_file = tmp_path / "tmp_grid" - txt_file.write_text( - "S1 2 91\nS1 3 92,open\n2 7 93\n3 5 94\n3 6 transformer,95\n5 7 96\n7 8 97\n8 9 98", encoding="utf-8" - ) - grid = Grid.from_txt_file(txt_file) - txt_file.unlink() - - assert 8 == grid.node.size - assert 1 == grid.branches.filter(to_status=0).size - assert 1 == grid.transformer.size - np.testing.assert_array_equal([95, 91, 92, 93, 94, 96, 97, 98], grid.branches.id) - - -def test_from_txt_file_conflicting_ids(tmp_path): - txt_file = tmp_path / "tmp_grid" - txt_file.write_text("S1 2\n1 3", encoding="utf-8") - - with pytest.raises(ValueError): - Grid.from_txt_file(txt_file) - - txt_file.unlink() - - -def test_from_txt_file_with_unordered_node_ids(tmp_path): - txt_file = tmp_path / "tmp_grid" - txt_file.write_text("S1 2\nS1 10\n10 11\n2 5\n5 6\n3 4\n3 7", encoding="utf-8") - grid = Grid.from_txt_file(txt_file) - txt_file.unlink() - - assert 9 == grid.node.size - - def test_grid_as_str(basic_grid): grid = basic_grid @@ -319,3 +273,73 @@ def test_grid_as_str(basic_grid): assert "102 106 301,transformer" in grid_as_string assert "103 104 203,open" in grid_as_string + + +class TestFromTxt: + def test_from_txt(self): + txt_lines = [ + "S1 2", + "S1 3 open", + "2 7", + "3 5", + "3 6 transformer", + "5 7", + "7 8", + "8 9", + ] + grid = Grid.from_txt(txt_lines) + assert 8 == grid.node.size + assert 1 == grid.branches.filter(to_status=0).size + assert 1 == grid.transformer.size + np.testing.assert_array_equal([14, 10, 11, 12, 13, 15, 16, 17], grid.branches.id) + + def test_from_txt_with_branch_ids(self): + txt_lines = [ + "S1 2 91", + "S1 3 92,open", + "2 7 93", + "3 5 94", + "3 6 transformer,95", + "5 7 96", + "7 8 97", + "8 9 98", + ] + + grid = Grid.from_txt(txt_lines) + assert 8 == grid.node.size + assert 1 == grid.branches.filter(to_status=0).size + assert 1 == grid.transformer.size + np.testing.assert_array_equal([95, 91, 92, 93, 94, 96, 97, 98], grid.branches.id) + + def test_from_txt_with_conflicting_ids(self): + txt_lines = [ + "S1 2", + "1 3", + ] + + with pytest.raises(ValueError): + Grid.from_txt(txt_lines) + + def test_from_txt_with_unordered_node_ids(self): + txt_lines = [ + "S1 2", + "S1 10", + "10 11", + "2 5", + "5 6", + "3 4", + "3 7", + ] + grid = Grid.from_txt(txt_lines) + assert 9 == grid.node.size + + def test_from_txt_file(self, tmp_path): + txt_file = tmp_path / "tmp_grid" + txt_file.write_text("S1 2\nS1 3 open\n2 7\n3 5\n3 6 transformer\n5 7\n7 8\n8 9", encoding="utf-8") + grid = Grid.from_txt_file(txt_file) + txt_file.unlink() + + assert 8 == grid.node.size + assert 1 == grid.branches.filter(to_status=0).size + assert 1 == grid.transformer.size + np.testing.assert_array_equal([14, 10, 11, 12, 13, 15, 16, 17], grid.branches.id) From 8441e89a4db4acb74cae289b097a0c851f08d2aa Mon Sep 17 00:00:00 2001 From: Thijs Baaijen <13253091+Thijss@users.noreply.github.com> Date: Wed, 5 Feb 2025 13:06:10 +0100 Subject: [PATCH 03/13] support unordered branches Signed-off-by: Thijs Baaijen <13253091+Thijss@users.noreply.github.com> --- .../_core/model/grids/_text_sources.py | 2 +- tests/unit/model/grids/test_grid_base.py | 16 +++++++++++++++- 2 files changed, 16 insertions(+), 2 deletions(-) diff --git a/src/power_grid_model_ds/_core/model/grids/_text_sources.py b/src/power_grid_model_ds/_core/model/grids/_text_sources.py index dc670cb..ad85602 100644 --- a/src/power_grid_model_ds/_core/model/grids/_text_sources.py +++ b/src/power_grid_model_ds/_core/model/grids/_text_sources.py @@ -110,4 +110,4 @@ def add_branch(self, branch: tuple[str, str], comments: list[str]): new_branch.to_status = 0 else: new_branch.to_status = 1 - self.grid.append(new_branch) + self.grid.append(new_branch, check_max_id=False) diff --git a/tests/unit/model/grids/test_grid_base.py b/tests/unit/model/grids/test_grid_base.py index 39b68a3..f341430 100644 --- a/tests/unit/model/grids/test_grid_base.py +++ b/tests/unit/model/grids/test_grid_base.py @@ -276,7 +276,7 @@ def test_grid_as_str(basic_grid): class TestFromTxt: - def test_from_txt(self): + def test_from_txt_lines(self): txt_lines = [ "S1 2", "S1 3 open", @@ -333,6 +333,20 @@ def test_from_txt_with_unordered_node_ids(self): grid = Grid.from_txt(txt_lines) assert 9 == grid.node.size + def test_from_txt_with_unordered_branch_ids(self): + txt_lines = [ + "3 6 14, transformer", + "S1 2 10", + "S1 3 11, open", + "2 7 12", + "3 5 13", + "5 7 15", + "7 8 16", + "8 9 17", + ] + grid = Grid.from_txt(txt_lines) + assert 9 == grid.node.size + def test_from_txt_file(self, tmp_path): txt_file = tmp_path / "tmp_grid" txt_file.write_text("S1 2\nS1 3 open\n2 7\n3 5\n3 6 transformer\n5 7\n7 8\n8 9", encoding="utf-8") From a1c17b59f20a834872c74731715e70dd05e262d0 Mon Sep 17 00:00:00 2001 From: Thijs Baaijen <13253091+Thijss@users.noreply.github.com> Date: Wed, 5 Feb 2025 13:06:18 +0100 Subject: [PATCH 04/13] update documentation Signed-off-by: Thijs Baaijen <13253091+Thijss@users.noreply.github.com> --- .../utils/grid_from_txt_examples.ipynb | 52 +++++++++++++++---- 1 file changed, 41 insertions(+), 11 deletions(-) diff --git a/docs/examples/utils/grid_from_txt_examples.ipynb b/docs/examples/utils/grid_from_txt_examples.ipynb index 1fb390d..b1588e4 100644 --- a/docs/examples/utils/grid_from_txt_examples.ipynb +++ b/docs/examples/utils/grid_from_txt_examples.ipynb @@ -28,13 +28,22 @@ "- A _transformer_ is defined as ` transformer`\n", " - e.g.: `8 9 transformer`\n", "- A _grid opening_ is defined by adding `open`\n", - " - e.g.: `4 5 open` for _lines_ or `6 7 transformer,open` for _transformers_\n", - "\n", + " - e.g.: `4 5 open` for _lines_ or `6 7 transformer,open` for _transformers_\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ "### Loading a drawn grid into pgm-ds\n", "\n", - "Once you've created a grid, copy the _Graph Data_ of your grid to a text file (e.g. `my_grid.txt`).\n", + "There are two ways of loading a text grid into a pgm-ds:\n", + "- load grid from a .txt file\n", + "- load grid from a list of strings\n", "\n", - "For example, your file could contain the following data:\n", + "#### Load a grid from a .txt file\n", + "Copy the _Graph Data_ of your grid to a text file (e.g. `my_grid.txt`).\n", + "For the example above, the file should contain the following data:\n", "\n", "```text\n", "S1 2\n", @@ -52,12 +61,11 @@ }, { "cell_type": "code", - "execution_count": 1, + "execution_count": 9, "metadata": {}, "outputs": [], "source": [ "from pathlib import Path\n", - "\n", "from power_grid_model_ds import Grid\n", "\n", "txt_file_path = Path(\"../../_static/my_grid.txt\")\n", @@ -68,12 +76,34 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "**You should now have a grid loaded from your drawn graph data!**\n" + "\n", + "#### Load grid from a list of strings\n", + "You can also load a grid directly from a list of strings" + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "metadata": {}, + "outputs": [], + "source": [ + "from power_grid_model_ds import Grid\n", + "\n", + "grid = Grid.from_txt([\n", + " \"S1 2\",\n", + " \"S1 3 open\",\n", + " \"2 7\",\n", + " \"3 5\",\n", + " \"3 6 transformer\",\n", + " \"5 7\",\n", + " \"7 8\",\n", + " \"8 9\",\n", + "])" ] }, { "cell_type": "code", - "execution_count": 2, + "execution_count": 10, "metadata": {}, "outputs": [ { @@ -99,7 +129,7 @@ ], "metadata": { "kernelspec": { - "display_name": ".venv", + "display_name": "Python 3 (ipykernel)", "language": "python", "name": "python3" }, @@ -113,9 +143,9 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.12.3" + "version": "3.13.1" } }, "nbformat": 4, - "nbformat_minor": 2 + "nbformat_minor": 4 } From 0dddcf2ac9cd5c11b8934d403dc8843c8e883448 Mon Sep 17 00:00:00 2001 From: Thijs Baaijen <13253091+Thijss@users.noreply.github.com> Date: Wed, 5 Feb 2025 13:09:29 +0100 Subject: [PATCH 05/13] update gitignore Signed-off-by: Thijs Baaijen <13253091+Thijss@users.noreply.github.com> --- .gitignore | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.gitignore b/.gitignore index 78552f6..0120a87 100644 --- a/.gitignore +++ b/.gitignore @@ -55,3 +55,6 @@ PYPI_VERSION build/ wheelhouse/ _build/ + +# Jupyter Notebook +.ipynb_checkpoints \ No newline at end of file From 960a09d6e8970306e9950583851cd30c13fe0547 Mon Sep 17 00:00:00 2001 From: Thijs Baaijen <13253091+Thijss@users.noreply.github.com> Date: Wed, 5 Feb 2025 13:09:33 +0100 Subject: [PATCH 06/13] update documentation Signed-off-by: Thijs Baaijen <13253091+Thijss@users.noreply.github.com> --- .../utils/grid_from_txt_examples.ipynb | 23 +++++++++++-------- 1 file changed, 13 insertions(+), 10 deletions(-) diff --git a/docs/examples/utils/grid_from_txt_examples.ipynb b/docs/examples/utils/grid_from_txt_examples.ipynb index b1588e4..f2a2890 100644 --- a/docs/examples/utils/grid_from_txt_examples.ipynb +++ b/docs/examples/utils/grid_from_txt_examples.ipynb @@ -66,6 +66,7 @@ "outputs": [], "source": [ "from pathlib import Path\n", + "\n", "from power_grid_model_ds import Grid\n", "\n", "txt_file_path = Path(\"../../_static/my_grid.txt\")\n", @@ -89,16 +90,18 @@ "source": [ "from power_grid_model_ds import Grid\n", "\n", - "grid = Grid.from_txt([\n", - " \"S1 2\",\n", - " \"S1 3 open\",\n", - " \"2 7\",\n", - " \"3 5\",\n", - " \"3 6 transformer\",\n", - " \"5 7\",\n", - " \"7 8\",\n", - " \"8 9\",\n", - "])" + "grid = Grid.from_txt(\n", + " [\n", + " \"S1 2\",\n", + " \"S1 3 open\",\n", + " \"2 7\",\n", + " \"3 5\",\n", + " \"3 6 transformer\",\n", + " \"5 7\",\n", + " \"7 8\",\n", + " \"8 9\",\n", + " ]\n", + ")" ] }, { From ce2e0ddb61413cef79f9df71082c1196920000a0 Mon Sep 17 00:00:00 2001 From: Thijs Baaijen <13253091+Thijss@users.noreply.github.com> Date: Wed, 5 Feb 2025 13:09:37 +0100 Subject: [PATCH 07/13] fix test Signed-off-by: Thijs Baaijen <13253091+Thijss@users.noreply.github.com> --- tests/unit/model/grids/test_grid_base.py | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/tests/unit/model/grids/test_grid_base.py b/tests/unit/model/grids/test_grid_base.py index f341430..7fea164 100644 --- a/tests/unit/model/grids/test_grid_base.py +++ b/tests/unit/model/grids/test_grid_base.py @@ -335,14 +335,13 @@ def test_from_txt_with_unordered_node_ids(self): def test_from_txt_with_unordered_branch_ids(self): txt_lines = [ - "3 6 14, transformer", - "S1 2 10", - "S1 3 11, open", - "2 7 12", - "3 5 13", - "5 7 15", - "7 8 16", - "8 9 17", + "5 6 16", + "3 4 17", + "3 7 18", + "S1 2 12", + "S1 10 13", + "10 11 14", + "2 5 15", ] grid = Grid.from_txt(txt_lines) assert 9 == grid.node.size From a7bdd9d41984a7bbe247e523a83ec14772afd035 Mon Sep 17 00:00:00 2001 From: Thijs Baaijen <13253091+Thijss@users.noreply.github.com> Date: Wed, 5 Feb 2025 14:24:35 +0100 Subject: [PATCH 08/13] switch to regex for better text support Signed-off-by: Thijs Baaijen <13253091+Thijss@users.noreply.github.com> --- .../_core/model/grids/_text_sources.py | 20 +++++++++++++------ 1 file changed, 14 insertions(+), 6 deletions(-) diff --git a/src/power_grid_model_ds/_core/model/grids/_text_sources.py b/src/power_grid_model_ds/_core/model/grids/_text_sources.py index ad85602..0612957 100644 --- a/src/power_grid_model_ds/_core/model/grids/_text_sources.py +++ b/src/power_grid_model_ds/_core/model/grids/_text_sources.py @@ -5,6 +5,7 @@ """Create a grid from text a text file""" import logging +import re from typing import TYPE_CHECKING from power_grid_model_ds._core.model.enums.nodes import NodeType @@ -34,8 +35,11 @@ class TextSource: def __init__(self, grid_class: type["Grid"]): self.grid = grid_class.empty() - def load_from_txt(self, txt_lines: list[str]) -> "Grid": + def load_from_txt(self, txt_lines: list[str] | str) -> "Grid": """Load a grid from text""" + if isinstance(txt_lines, str): + txt_lines = txt_lines.split("\n") + txt_nodes, txt_branches = self.read_txt(txt_lines) self.add_nodes(txt_nodes) self.add_branches(txt_branches) @@ -50,16 +54,20 @@ def read_txt(txt_lines: list[str]) -> tuple[set, dict]: for text_line in txt_lines: if not text_line.strip() or text_line.startswith("#"): continue # skip empty lines and comments - try: - from_node_str, to_node_str, *comments = text_line.strip().split(" ") - except ValueError as err: - raise ValueError(f"Text line '{text_line}' is invalid. Skipping...") from err - comments = comments[0].split(",") if comments else [] + + pattern = re.compile(r'^\s*(\S+)\s+(\S+)(?:\s+(\S+))?\s*$') + match = pattern.match(text_line) + if not match: + raise ValueError(f"Text line '{text_line}' is invalid. Skipping...") + + from_node_str, to_node_str, comment = match.groups() + comments = comment.split(",") if comment else [] txt_nodes |= {from_node_str, to_node_str} txt_branches[(from_node_str, to_node_str)] = comments return txt_nodes, txt_branches + def add_nodes(self, nodes: set[str]): """Add nodes to the grid""" source_nodes = {int(node[1:]) for node in nodes if node.startswith("S")} From 4866e6be2cb757dc1e6a2f356ffeef87790be4a5 Mon Sep 17 00:00:00 2001 From: Thijs Baaijen <13253091+Thijss@users.noreply.github.com> Date: Wed, 5 Feb 2025 14:24:54 +0100 Subject: [PATCH 09/13] add support for both list[str] and str Signed-off-by: Thijs Baaijen <13253091+Thijss@users.noreply.github.com> --- .../_core/model/grids/_text_sources.py | 3 +- .../_core/model/grids/base.py | 8 ++++-- tests/unit/model/grids/test_grid_base.py | 28 +++++++++---------- 3 files changed, 20 insertions(+), 19 deletions(-) diff --git a/src/power_grid_model_ds/_core/model/grids/_text_sources.py b/src/power_grid_model_ds/_core/model/grids/_text_sources.py index 0612957..c042201 100644 --- a/src/power_grid_model_ds/_core/model/grids/_text_sources.py +++ b/src/power_grid_model_ds/_core/model/grids/_text_sources.py @@ -55,7 +55,7 @@ def read_txt(txt_lines: list[str]) -> tuple[set, dict]: if not text_line.strip() or text_line.startswith("#"): continue # skip empty lines and comments - pattern = re.compile(r'^\s*(\S+)\s+(\S+)(?:\s+(\S+))?\s*$') + pattern = re.compile(r"^\s*(\S+)\s+(\S+)(?:\s+(\S+))?\s*$") match = pattern.match(text_line) if not match: raise ValueError(f"Text line '{text_line}' is invalid. Skipping...") @@ -67,7 +67,6 @@ def read_txt(txt_lines: list[str]) -> tuple[set, dict]: txt_branches[(from_node_str, to_node_str)] = comments return txt_nodes, txt_branches - def add_nodes(self, nodes: set[str]): """Add nodes to the grid""" source_nodes = {int(node[1:]) for node in nodes if node.startswith("S")} 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 0721f69..717d250 100644 --- a/src/power_grid_model_ds/_core/model/grids/base.py +++ b/src/power_grid_model_ds/_core/model/grids/base.py @@ -409,13 +409,17 @@ def _from_pickle(cls, pickle_path: Path): return grid @classmethod - def from_txt(cls, txt_lines: list[str]): + def from_txt(cls, txt_lines: list[str] | str): """Build a grid from a list of strings See the documentation for the expected format of the txt_lines - Example: + Args: + txt_lines (list[str] | str): The lines of the txt + + Examples: >>> Grid.from_txt(["1 2", "2 3", "3 4 transformer", "4 5", "S1 6"]) + alternative: Grid.from_txt("1 2\n2 3\n3 4 transformer\n4 5\nS1 6") """ return TextSource(grid_class=cls).load_from_txt(txt_lines) diff --git a/tests/unit/model/grids/test_grid_base.py b/tests/unit/model/grids/test_grid_base.py index 7fea164..f1e7c39 100644 --- a/tests/unit/model/grids/test_grid_base.py +++ b/tests/unit/model/grids/test_grid_base.py @@ -293,6 +293,14 @@ def test_from_txt_lines(self): assert 1 == grid.transformer.size np.testing.assert_array_equal([14, 10, 11, 12, 13, 15, 16, 17], grid.branches.id) + def test_from_txt_string(self): + txt_string = "S1 2\nS1 3 open\n2 7\n3 5\n3 6 transformer\n5 7\n7 8\n8 9" + assert Grid.from_txt(txt_string) + + def test_from_txt_string_with_spaces(self): + txt_string = "S1 2 \nS1 3 open\n2 7\n3 5\n 3 6 transformer\n5 7\n7 8\n8 9" + assert Grid.from_txt(txt_string) + def test_from_txt_with_branch_ids(self): txt_lines = [ "S1 2 91", @@ -312,25 +320,15 @@ def test_from_txt_with_branch_ids(self): np.testing.assert_array_equal([95, 91, 92, 93, 94, 96, 97, 98], grid.branches.id) def test_from_txt_with_conflicting_ids(self): - txt_lines = [ - "S1 2", - "1 3", - ] + with pytest.raises(ValueError): + Grid.from_txt(["S1 2", "1 3"]) + def test_from_txt_with_invalid_line(self): with pytest.raises(ValueError): - Grid.from_txt(txt_lines) + Grid.from_txt(["S1 2 arg3 arg4"]) def test_from_txt_with_unordered_node_ids(self): - txt_lines = [ - "S1 2", - "S1 10", - "10 11", - "2 5", - "5 6", - "3 4", - "3 7", - ] - grid = Grid.from_txt(txt_lines) + grid = Grid.from_txt(["S1 2", "S1 10", "10 11", "2 5", "5 6", "3 4", "3 7"]) assert 9 == grid.node.size def test_from_txt_with_unordered_branch_ids(self): From 2860f04ffa1b01e596f2e47f3f6d2d65f6e098f3 Mon Sep 17 00:00:00 2001 From: Thijs Baaijen <13253091+Thijss@users.noreply.github.com> Date: Wed, 5 Feb 2025 14:27:19 +0100 Subject: [PATCH 10/13] add test for docstring Signed-off-by: Thijs Baaijen <13253091+Thijss@users.noreply.github.com> --- tests/unit/model/grids/test_grid_base.py | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/tests/unit/model/grids/test_grid_base.py b/tests/unit/model/grids/test_grid_base.py index f1e7c39..a40b500 100644 --- a/tests/unit/model/grids/test_grid_base.py +++ b/tests/unit/model/grids/test_grid_base.py @@ -301,6 +301,19 @@ def test_from_txt_string_with_spaces(self): txt_string = "S1 2 \nS1 3 open\n2 7\n3 5\n 3 6 transformer\n5 7\n7 8\n8 9" assert Grid.from_txt(txt_string) + def test_from_docstring(self): + txt_string = """ + S1 2 + S1 3 open + 2 7 + 3 5 + 3 6 transformer + 5 7 + 7 8 + 8 9 + """ + assert Grid.from_txt(txt_string) + def test_from_txt_with_branch_ids(self): txt_lines = [ "S1 2 91", From 8124ef20e330fcff9f125364c04108c457a49b43 Mon Sep 17 00:00:00 2001 From: Thijs Baaijen <13253091+Thijss@users.noreply.github.com> Date: Wed, 5 Feb 2025 14:51:31 +0100 Subject: [PATCH 11/13] switch to args input Signed-off-by: Thijs Baaijen <13253091+Thijss@users.noreply.github.com> --- .../_core/model/grids/_text_sources.py | 8 ++-- .../_core/model/grids/base.py | 10 ++--- tests/unit/model/grids/test_grid_base.py | 42 +++++-------------- 3 files changed, 20 insertions(+), 40 deletions(-) diff --git a/src/power_grid_model_ds/_core/model/grids/_text_sources.py b/src/power_grid_model_ds/_core/model/grids/_text_sources.py index c042201..89b7048 100644 --- a/src/power_grid_model_ds/_core/model/grids/_text_sources.py +++ b/src/power_grid_model_ds/_core/model/grids/_text_sources.py @@ -35,12 +35,12 @@ class TextSource: def __init__(self, grid_class: type["Grid"]): self.grid = grid_class.empty() - def load_from_txt(self, txt_lines: list[str] | str) -> "Grid": + def load_from_txt(self, *args: str) -> "Grid": """Load a grid from text""" - if isinstance(txt_lines, str): - txt_lines = txt_lines.split("\n") - txt_nodes, txt_branches = self.read_txt(txt_lines) + text_lines = [line for arg in args for line in arg.strip().split("\n")] + + txt_nodes, txt_branches = self.read_txt(text_lines) self.add_nodes(txt_nodes) self.add_branches(txt_branches) self.grid.set_feeder_ids() 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 717d250..7af482c 100644 --- a/src/power_grid_model_ds/_core/model/grids/base.py +++ b/src/power_grid_model_ds/_core/model/grids/base.py @@ -409,19 +409,19 @@ def _from_pickle(cls, pickle_path: Path): return grid @classmethod - def from_txt(cls, txt_lines: list[str] | str): + def from_txt(cls, *args: str): """Build a grid from a list of strings See the documentation for the expected format of the txt_lines Args: - txt_lines (list[str] | str): The lines of the txt + *args (str): The lines of the grid Examples: - >>> Grid.from_txt(["1 2", "2 3", "3 4 transformer", "4 5", "S1 6"]) + >>> Grid.from_txt("1 2", "2 3", "3 4 transformer", "4 5", "S1 6") alternative: Grid.from_txt("1 2\n2 3\n3 4 transformer\n4 5\nS1 6") """ - return TextSource(grid_class=cls).load_from_txt(txt_lines) + return TextSource(grid_class=cls).load_from_txt(*args) @classmethod # pylint: disable=arguments-differ @@ -433,7 +433,7 @@ def from_txt_file(cls, txt_file_path: Path): """ with open(txt_file_path, "r", encoding="utf-8") as f: txt_lines = f.readlines() - return TextSource(grid_class=cls).load_from_txt(txt_lines) + return TextSource(grid_class=cls).load_from_txt(*txt_lines) def set_feeder_ids(self): """Sets feeder and substation id properties in the grids arrays""" diff --git a/tests/unit/model/grids/test_grid_base.py b/tests/unit/model/grids/test_grid_base.py index a40b500..db10df7 100644 --- a/tests/unit/model/grids/test_grid_base.py +++ b/tests/unit/model/grids/test_grid_base.py @@ -277,7 +277,7 @@ def test_grid_as_str(basic_grid): class TestFromTxt: def test_from_txt_lines(self): - txt_lines = [ + grid = Grid.from_txt( "S1 2", "S1 3 open", "2 7", @@ -286,8 +286,7 @@ def test_from_txt_lines(self): "5 7", "7 8", "8 9", - ] - grid = Grid.from_txt(txt_lines) + ) assert 8 == grid.node.size assert 1 == grid.branches.filter(to_status=0).size assert 1 == grid.transformer.size @@ -302,7 +301,7 @@ def test_from_txt_string_with_spaces(self): assert Grid.from_txt(txt_string) def test_from_docstring(self): - txt_string = """ + assert Grid.from_txt(""" S1 2 S1 3 open 2 7 @@ -311,22 +310,12 @@ def test_from_docstring(self): 5 7 7 8 8 9 - """ - assert Grid.from_txt(txt_string) + """) def test_from_txt_with_branch_ids(self): - txt_lines = [ - "S1 2 91", - "S1 3 92,open", - "2 7 93", - "3 5 94", - "3 6 transformer,95", - "5 7 96", - "7 8 97", - "8 9 98", - ] - - grid = Grid.from_txt(txt_lines) + grid = Grid.from_txt( + "S1 2 91", "S1 3 92,open", "2 7 93", "3 5 94", "3 6 transformer,95", "5 7 96", "7 8 97", "8 9 98" + ) assert 8 == grid.node.size assert 1 == grid.branches.filter(to_status=0).size assert 1 == grid.transformer.size @@ -334,27 +323,18 @@ def test_from_txt_with_branch_ids(self): def test_from_txt_with_conflicting_ids(self): with pytest.raises(ValueError): - Grid.from_txt(["S1 2", "1 3"]) + Grid.from_txt("S1 2", "1 3") def test_from_txt_with_invalid_line(self): with pytest.raises(ValueError): - Grid.from_txt(["S1 2 arg3 arg4"]) + Grid.from_txt("S1 2 arg3 arg4") def test_from_txt_with_unordered_node_ids(self): - grid = Grid.from_txt(["S1 2", "S1 10", "10 11", "2 5", "5 6", "3 4", "3 7"]) + grid = Grid.from_txt("S1 2", "S1 10", "10 11", "2 5", "5 6", "3 4", "3 7") assert 9 == grid.node.size def test_from_txt_with_unordered_branch_ids(self): - txt_lines = [ - "5 6 16", - "3 4 17", - "3 7 18", - "S1 2 12", - "S1 10 13", - "10 11 14", - "2 5 15", - ] - grid = Grid.from_txt(txt_lines) + grid = Grid.from_txt("5 6 16", "3 4 17", "3 7 18", "S1 2 12", "S1 10 13", "10 11 14", "2 5 15") assert 9 == grid.node.size def test_from_txt_file(self, tmp_path): From 84863dcb74ab63cd0be02a9d133caf4ab182b255 Mon Sep 17 00:00:00 2001 From: Thijs Baaijen <13253091+Thijss@users.noreply.github.com> Date: Wed, 5 Feb 2025 15:19:29 +0100 Subject: [PATCH 12/13] remove re module implementation Signed-off-by: Thijs Baaijen <13253091+Thijss@users.noreply.github.com> --- .../_core/model/grids/_text_sources.py | 15 ++++++--------- tests/unit/model/grids/test_grid_base.py | 2 +- 2 files changed, 7 insertions(+), 10 deletions(-) diff --git a/src/power_grid_model_ds/_core/model/grids/_text_sources.py b/src/power_grid_model_ds/_core/model/grids/_text_sources.py index 89b7048..ab9c1a9 100644 --- a/src/power_grid_model_ds/_core/model/grids/_text_sources.py +++ b/src/power_grid_model_ds/_core/model/grids/_text_sources.py @@ -5,7 +5,6 @@ """Create a grid from text a text file""" import logging -import re from typing import TYPE_CHECKING from power_grid_model_ds._core.model.enums.nodes import NodeType @@ -49,19 +48,17 @@ def load_from_txt(self, *args: str) -> "Grid": @staticmethod def read_txt(txt_lines: list[str]) -> tuple[set, dict]: """Extract assets from text""" + txt_nodes = set() txt_branches = {} for text_line in txt_lines: if not text_line.strip() or text_line.startswith("#"): continue # skip empty lines and comments - - pattern = re.compile(r"^\s*(\S+)\s+(\S+)(?:\s+(\S+))?\s*$") - match = pattern.match(text_line) - if not match: - raise ValueError(f"Text line '{text_line}' is invalid. Skipping...") - - from_node_str, to_node_str, comment = match.groups() - comments = comment.split(",") if comment else [] + try: + from_node_str, to_node_str, *comments = text_line.strip().split() + except ValueError as err: + raise ValueError(f"Text line '{text_line}' is invalid. Skipping...") from err + comments = comments[0].split(",") if comments else [] txt_nodes |= {from_node_str, to_node_str} txt_branches[(from_node_str, to_node_str)] = comments diff --git a/tests/unit/model/grids/test_grid_base.py b/tests/unit/model/grids/test_grid_base.py index db10df7..fd1d1b6 100644 --- a/tests/unit/model/grids/test_grid_base.py +++ b/tests/unit/model/grids/test_grid_base.py @@ -327,7 +327,7 @@ def test_from_txt_with_conflicting_ids(self): def test_from_txt_with_invalid_line(self): with pytest.raises(ValueError): - Grid.from_txt("S1 2 arg3 arg4") + Grid.from_txt("S1") def test_from_txt_with_unordered_node_ids(self): grid = Grid.from_txt("S1 2", "S1 10", "10 11", "2 5", "5 6", "3 4", "3 7") From 0018e2feb3495870654da832367a9a1161f9529d Mon Sep 17 00:00:00 2001 From: Thijs Baaijen <13253091+Thijss@users.noreply.github.com> Date: Wed, 5 Feb 2025 15:19:46 +0100 Subject: [PATCH 13/13] bump minor Signed-off-by: Thijs Baaijen <13253091+Thijss@users.noreply.github.com> --- VERSION | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/VERSION b/VERSION index 9f8e9b6..b123147 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -1.0 \ No newline at end of file +1.1 \ No newline at end of file