From 7379f0194c87d61364b3c953a76fc8808d57c99b Mon Sep 17 00:00:00 2001 From: Roman Bolgaryn Date: Thu, 23 Nov 2023 14:21:44 +0100 Subject: [PATCH] add tollbox function get_substations that finds clusters of connected buses (ignoring branches except for tranaformers) and say they are substations --- CHANGELOG.rst | 1 + doc/toolbox.rst | 2 + pandapower/grid_equivalents/get_equivalent.py | 2 +- pandapower/grid_equivalents/toolbox.py | 7 +-- pandapower/plotting/generic_geodata.py | 15 +++-- pandapower/shortcircuit/toolbox.py | 3 +- .../test/toolbox/test_element_selection.py | 62 +++++++++++++++++++ pandapower/toolbox/element_selection.py | 62 +++++++++++++++++++ pandapower/topology/create_graph.py | 27 ++++---- pandapower/topology/graph_searches.py | 3 +- 10 files changed, 157 insertions(+), 27 deletions(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 79ee2f922..028078d1b 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -36,6 +36,7 @@ Change Log - [FIXED] :code:`convert_format.py`: update the attributes of the characteristic objects to match the new characteristic - [FIXED] additional arguments from mpc saved to net._options: create "_options" if it does not exist - [CHANGED] cim2pp: extracted getting default classes, added generic setting datatypes from CGMES XMI schema +- [ADDED] toolbox function :code:`get_substations` that finds all substations based on connectivity analysis of a networkx graph [2.13.1] - 2023-05-12 diff --git a/doc/toolbox.rst b/doc/toolbox.rst index 0f64d45df..da9076387 100644 --- a/doc/toolbox.rst +++ b/doc/toolbox.rst @@ -69,6 +69,8 @@ Item/Element Selection .. autofunction:: pandapower.toolbox.get_connected_switches +.. autofunction:: pandapower.toolbox.get_substations + .. autofunction:: pandapower.toolbox.get_connecting_branches .. autofunction:: pandapower.toolbox.false_elm_links diff --git a/pandapower/grid_equivalents/get_equivalent.py b/pandapower/grid_equivalents/get_equivalent.py index 67faac8c6..34a6719f5 100644 --- a/pandapower/grid_equivalents/get_equivalent.py +++ b/pandapower/grid_equivalents/get_equivalent.py @@ -479,7 +479,7 @@ def _determine_bus_groups(net, boundary_buses, internal_buses, # --- determine buses connected to boundary buses via bus-bus-switch boundary_buses_inclusive_bswitch = set() mg_sw = top.create_nxgraph(net, respect_switches=True, include_lines=False, include_impedances=False, - include_tcsc=False, include_trafos=False, include_trafo3ws=False) + include_trafos=False, include_trafo3ws=False, include_tcsc=False) for bbus in boundary_buses: boundary_buses_inclusive_bswitch |= set(top.connected_component(mg_sw, bbus)) if len(boundary_buses_inclusive_bswitch) > len(boundary_buses): diff --git a/pandapower/grid_equivalents/toolbox.py b/pandapower/grid_equivalents/toolbox.py index 419b1ca49..77ac21b10 100644 --- a/pandapower/grid_equivalents/toolbox.py +++ b/pandapower/grid_equivalents/toolbox.py @@ -257,11 +257,8 @@ def append_boundary_buses_externals_per_zone(boundary_buses, boundaries, zone, o def get_connected_switch_buses_groups(net, buses): all_buses = set() bus_dict = [] - mg_sw = top.create_nxgraph(net, include_trafos=False, - include_trafo3ws=False, - respect_switches=True, - include_lines=False, - include_impedances=False) + mg_sw = top.create_nxgraph(net, respect_switches=True, include_lines=False, include_impedances=False, + include_trafos=False, include_trafo3ws=False) for bbus in buses: if bbus in all_buses: continue diff --git a/pandapower/plotting/generic_geodata.py b/pandapower/plotting/generic_geodata.py index 42474a83e..dd13220e3 100644 --- a/pandapower/plotting/generic_geodata.py +++ b/pandapower/plotting/generic_geodata.py @@ -164,7 +164,9 @@ def create_generic_coordinates(net, mg=None, library="igraph", respect_switches=False, geodata_table="bus_geodata", buses=None, - overwrite=False): + overwrite=False, + include_out_of_service_buses=False, + include_out_of_service_branches=False): """ This function will add arbitrary geo-coordinates for all buses based on an analysis of branches and rings. It will remove out of service buses/lines from the net. The coordinates will be @@ -182,6 +184,10 @@ def create_generic_coordinates(net, mg=None, library="igraph", :type buses: list :param overwrite: overwrite existing geodata :type overwrite: bool + :param include_out_of_service_buses: also include the buses that are out-of-service in the graph + :type include_out_of_service_buses: bool + :param include_out_of_service_branches: also include the branches that are out-of-service in the graph + :type include_out_of_service_branches: bool :return: net - pandapower network with added geo coordinates for the buses :Example: @@ -197,7 +203,8 @@ def create_generic_coordinates(net, mg=None, library="igraph", elif library == "networkx": if mg is None: nxg = top.create_nxgraph(net, respect_switches=respect_switches, - include_out_of_service=True) + include_out_of_service=include_out_of_service_buses, + include_out_of_service_branches=include_out_of_service_branches) else: nxg = copy.deepcopy(mg) coords = coords_from_nxgraph(nxg) @@ -220,8 +227,8 @@ def _prepare_geodata_table(net, geodata_table, overwrite): net[geodata_table] = pd.DataFrame(columns=["x", "y"]) def fuse_geodata(net): - mg = top.create_nxgraph(net, include_lines=False, include_impedances=False, - respect_switches=False) + mg = top.create_nxgraph(net, respect_switches=False, include_lines=False, include_impedances=False, + include_out_of_service=True) geocoords = set(net.bus_geodata.index) for area in top.connected_components(mg): if len(area & geocoords) > 1: diff --git a/pandapower/shortcircuit/toolbox.py b/pandapower/shortcircuit/toolbox.py index 8c93fb6ee..65c67271d 100644 --- a/pandapower/shortcircuit/toolbox.py +++ b/pandapower/shortcircuit/toolbox.py @@ -63,8 +63,7 @@ def detect_power_station_unit(net, mode="auto", trafo_lv_bus = net.trafo.loc[required_trafo.index, "lv_bus"].values trafo_hv_bus = net.trafo.loc[required_trafo.index, "hv_bus"].values - g = create_nxgraph(net, respect_switches=True, - nogobuses=None, notravbuses=trafo_hv_bus) + g = create_nxgraph(net, respect_switches=True, nogobuses=None, notravbuses=trafo_hv_bus) for t_ix in required_trafo.index: t_lv_bus = required_trafo.at[t_ix, "lv_bus"] diff --git a/pandapower/test/toolbox/test_element_selection.py b/pandapower/test/toolbox/test_element_selection.py index a355c056d..ae5afe2c7 100644 --- a/pandapower/test/toolbox/test_element_selection.py +++ b/pandapower/test/toolbox/test_element_selection.py @@ -3,6 +3,7 @@ # Copyright (c) 2016-2023 by University of Kassel and Fraunhofer Institute for Energy Economics # and Energy System Technology (IEE), Kassel. All rights reserved. import pandas as pd +import numpy as np import pytest import pandapower as pp @@ -219,5 +220,66 @@ def test_count_elements(): assert set(received.index) == pandapower.toolbox.pp_elements() +def test_get_substations(): + net = pp.create_empty_network() + pp.create_buses(net, 5, 110) + pp.create_buses(net, 5, 20) + pp.create_buses(net, 2, 10) + + pp.create_transformer(net, 3, 5, "63 MVA 110/20 kV") + pp.create_transformer3w(net, 4, 8, 10, "63/25/38 MVA 110/20/10 kV") + + pp.create_switches(net, buses=[0, 0, 2, 5, 6, 1, 8, 10], elements=[1, 2, 3, 6, 7, 4, 9, 11], et="b") + pp.create_switches(net, buses=[3, 5], elements=[0, 0], et=["t", "t"]) + pp.create_switches(net, buses=[4, 8, 10], elements=[0, 0, 0], et=["t3", "t3", "t3"]) + + s = pp.toolbox.get_substations(net, write_to_net=False) + assert len(s) == 1 + assert "substation" not in net.bus.columns + pp.toolbox.get_substations(net) + assert np.alltrue(net.bus.substation == 0) + assert np.array_equal(net.bus.index.values, s[0]) + + s1 = pp.toolbox.get_substations(net, include_trafos=False) + # 110 kV buses HV side, 20 kV buses for trafo and trafo3w, 10 kV buses for trafo3w + assert len(s1) == 4 + assert len(net.bus.substation.unique()) == 4 + for c in s1.values(): + assert len(net.bus.loc[c, "vn_kv"].unique()) == 1 + + net.trafo.in_service = False + net.trafo3w.in_service = False + s11 = pp.toolbox.get_substations(net, include_out_of_service_branches=False) + assert len(s11) == 4 + assert len(net.bus.substation.unique()) == 4 + for k, v in s11.items(): + assert np.array_equal(v, s1[k]) + + net.switch.closed = False + s2 = pp.toolbox.get_substations(net) + assert len(s2) == 1 + assert len(net.bus.substation.unique()) == 1 + assert np.array_equal(s[0], s2[0]) + + s3 = pp.toolbox.get_substations(net, respect_switches=True) + assert len(s3) == 0 + assert np.alltrue(pd.isna(net.bus.substation)) + + s4 = pp.toolbox.get_substations(net, respect_switches=True, return_all_buses=True) + assert len(s4) == 12 + assert len(net.bus.substation.unique()) == 12 + + # even when al switches open and trafos out of service, find 1 substation: + s5 = pp.toolbox.get_substations(net) + assert np.alltrue(net.bus.substation == 0) + assert np.array_equal(net.bus.index.values, s5[0]) + + # even when all buses out of service: + net.bus.in_service = False + s6 = pp.toolbox.get_substations(net) + assert np.alltrue(net.bus.substation == 0) + assert np.array_equal(net.bus.index.values, s6[0]) + + if __name__ == '__main__': pytest.main([__file__, "-x"]) \ No newline at end of file diff --git a/pandapower/toolbox/element_selection.py b/pandapower/toolbox/element_selection.py index 4a51f9f7d..a37a34492 100644 --- a/pandapower/toolbox/element_selection.py +++ b/pandapower/toolbox/element_selection.py @@ -526,6 +526,68 @@ def get_connecting_branches(net, buses1, buses2, branch_elements=None): return {key: val for key, val in found.items() if len(val)} +def get_substations(net, include_trafos=True, include_out_of_service_branches=True, respect_switches=False, + return_all_buses=False, write_to_net=True): + """ + Finds all substations in net. A substation is a cluster of connected buses. Can be parametrized to consider + trafo and trafo3w as relevant connections that define the connected bus clusters (default) or to only consider + bus-bus switches as relevant connections to find such clusters (seeing the HV and LV sides of the substation + as separate substations). + By default, out-of-service transformers still define a relevant connection for the bus clusters. This can be + changed by setting the parameter "include_out_of_service" to False. Out-of-service buses are always included. + Open switches are considered as relevant connections by default. Setting "respect_switches" to True will ignore + open switches and only consider closed switches. + Can return only substations with more than 1 bus (default) or also consider a single bus as its own substation. + By default, the found substations are also written into the bus table, into the new column "substation" (that is + overwritten if it exists) + + Parameters + ---------- + net : pandapowerNet + include_trafos : bool, default True + Whether to consider transformers (trafo and trafo3w) as relevant connections that define a substation + include_out_of_service_branches : bool, default True + Whether to consider out-of-service transformers as relevant or only the in_service transformers. + Note: out-of-service buses are always included + respect_switches : bool, default False + Whether to consider all switches or only closed switches as relevant connections + return_all_buses : bool, default False + Whether to only return substations that have more than one bus or to also include single buses as their own + substations + write_to_net : bool, default True + Write the found substation into the net.bus.substation column (overwriting data if the column exists) + + Returns + ------- + substations : dict + Dictionary {index: buses} of all found substations + + """ + mg = pp.topology.create_nxgraph(net, respect_switches=respect_switches, include_lines=False, + include_impedances=False, include_dclines=False, include_trafos=include_trafos, + include_trafo3ws=include_trafos, include_tcsc=False, + include_out_of_service=True, include_out_of_service_branches=include_out_of_service_branches) + cc = pp.topology.connected_components(mg) + if return_all_buses: + substations = {i: list(c) for i, c in enumerate(cc)} + else: + substations = {i: list(c) for i, c in enumerate(cc) if len(c) > 1} + logger.info(f"Found {len(substations)} substations") + + if write_to_net: + if "substation" in net.bus.columns: + logger.info("Overwriting the data in the existing column net.bus.substation") + else: + logger.info("Writing the substation indices to a new column net.bus.substation") + + net.bus["substation"] = pd.Series(index=net.bus.index, dtype="Int64") + + for i, c in substations.items(): + net.bus.loc[c, "substation"] = i + + return substations + + def get_gc_objects_dict(): """ This function is based on the code in mem_top module diff --git a/pandapower/topology/create_graph.py b/pandapower/topology/create_graph.py index f4d2a4abc..aff1da98b 100644 --- a/pandapower/topology/create_graph.py +++ b/pandapower/topology/create_graph.py @@ -44,7 +44,7 @@ def create_nxgraph(net, respect_switches=True, include_lines=True, include_imped include_dclines=True, include_trafos=True, include_trafo3ws=True, include_tcsc=True, nogobuses=None, notravbuses=None, multi=True, calc_branch_impedances=False, branch_impedance_unit="ohm", - library="networkx", include_out_of_service=False): + library="networkx", include_out_of_service=False, include_out_of_service_branches=False): """ Converts a pandapower network into a NetworkX graph, which is a is a simplified representation of a network's topology, reduced to nodes and edges. Busses are being represented by nodes @@ -78,6 +78,9 @@ def create_nxgraph(net, respect_switches=True, include_lines=True, include_imped **include_trafo3ws** (boolean or index, True) - determines, whether or which trafo3ws get converted to edges + **include_tcsc** (boolean or index, True) - determines, whether or which tcsc get + converted to edges + **nogobuses** (integer/list, None) - nogobuses are not being considered in the graph **notravbuses** (integer/list, None) - lines connected to these buses are not being @@ -99,6 +102,8 @@ def create_nxgraph(net, respect_switches=True, include_lines=True, include_imped **include_out_of_service** (bool, False) - defines if out of service buses are included in the nx graph + **include_out_of_service_branches** (bool, False) - defines if out of service branches are included in the nx graph + OUTPUT: **mg** - Returns the required NetworkX graph @@ -108,10 +113,6 @@ def create_nxgraph(net, respect_switches=True, include_lines=True, include_imped mg = top.create_nx_graph(net, respect_switches = False) # converts the pandapower network "net" to a MultiGraph. Open switches will be ignored. - Parameters - ---------- - include_tcsc - """ if multi: @@ -132,7 +133,7 @@ def create_nxgraph(net, respect_switches=True, include_lines=True, include_imped line = get_edge_table(net, "line", include_lines) if line is not None: - indices, parameter, in_service = init_par(line, calc_branch_impedances) + indices, parameter, in_service = init_par(line, calc_branch_impedances, include_out_of_service_branches) indices[:, F_BUS] = line.from_bus.values indices[:, T_BUS] = line.to_bus.values @@ -158,7 +159,7 @@ def create_nxgraph(net, respect_switches=True, include_lines=True, include_imped impedance = get_edge_table(net, "impedance", include_impedances) if impedance is not None: - indices, parameter, in_service = init_par(impedance, calc_branch_impedances) + indices, parameter, in_service = init_par(impedance, calc_branch_impedances, include_out_of_service_branches) indices[:, F_BUS] = impedance.from_bus.values indices[:, T_BUS] = impedance.to_bus.values @@ -174,7 +175,7 @@ def create_nxgraph(net, respect_switches=True, include_lines=True, include_imped tcsc = get_edge_table(net, "tcsc", include_tcsc) if tcsc is not None: - indices, parameter, in_service = init_par(tcsc, calc_branch_impedances) + indices, parameter, in_service = init_par(tcsc, calc_branch_impedances, include_out_of_service_branches) indices[:, F_BUS] = tcsc.from_bus.values indices[:, T_BUS] = tcsc.to_bus.values @@ -189,7 +190,7 @@ def create_nxgraph(net, respect_switches=True, include_lines=True, include_imped dclines = get_edge_table(net, "dcline", include_dclines) if dclines is not None: - indices, parameter, in_service = init_par(dclines, calc_branch_impedances) + indices, parameter, in_service = init_par(dclines, calc_branch_impedances, include_out_of_service_branches) indices[:, F_BUS] = dclines.from_bus.values indices[:, T_BUS] = dclines.to_bus.values @@ -202,7 +203,7 @@ def create_nxgraph(net, respect_switches=True, include_lines=True, include_imped trafo = get_edge_table(net, "trafo", include_trafos) if trafo is not None: - indices, parameter, in_service = init_par(trafo, calc_branch_impedances) + indices, parameter, in_service = init_par(trafo, calc_branch_impedances, include_out_of_service_branches) indices[:, F_BUS] = trafo.hv_bus.values indices[:, T_BUS] = trafo.lv_bus.values @@ -244,7 +245,7 @@ def create_nxgraph(net, respect_switches=True, include_lines=True, include_imped open_trafo3w_buses = net.switch.bus.values[mask] open_trafo3w = (open_trafo3w_index + open_trafo3w_buses * 1j).flatten() for f, t in combinations(sides, 2): - indices, parameter, in_service = init_par(trafo3w, calc_branch_impedances) + indices, parameter, in_service = init_par(trafo3w, calc_branch_impedances, include_out_of_service_branches) indices[:, F_BUS] = trafo3w["%s_bus" % f].values indices[:, T_BUS] = trafo3w["%s_bus" % t].values if respect_switches and len(open_trafo3w): @@ -354,7 +355,7 @@ def get_baseR(net, ppc, buses): return np.square(base_kv) / net.sn_mva -def init_par(tab, calc_branch_impedances=False): +def init_par(tab, calc_branch_impedances=False, include_out_of_service_branches=False): n = tab.shape[0] indices = np.zeros((n, 3), dtype=np.int64) indices[:, INDEX] = tab.index @@ -364,7 +365,7 @@ def init_par(tab, calc_branch_impedances=False): parameters = np.zeros((n, 1), dtype=float) if "in_service" in tab: - return indices, parameters, tab.in_service.values.copy() + return indices, parameters, tab.in_service.values.copy() | include_out_of_service_branches else: return indices, parameters diff --git a/pandapower/topology/graph_searches.py b/pandapower/topology/graph_searches.py index 832641f70..c01598580 100644 --- a/pandapower/topology/graph_searches.py +++ b/pandapower/topology/graph_searches.py @@ -120,8 +120,7 @@ def calc_distance_to_bus(net, bus, respect_switches=True, nogobuses=None, dist = top.calc_distance_to_bus(net, 5) """ - g = create_nxgraph(net, respect_switches=respect_switches, nogobuses=nogobuses, - notravbuses=notravbuses) + g = create_nxgraph(net, respect_switches=respect_switches, nogobuses=nogobuses, notravbuses=notravbuses) return pd.Series(nx.single_source_dijkstra_path_length(g, bus, weight=weight))