Skip to content

Commit

Permalink
add tollbox function get_substations that finds clusters of connected…
Browse files Browse the repository at this point in the history
… buses (ignoring branches except for tranaformers) and say they are substations
  • Loading branch information
rbolgaryn committed Nov 23, 2023
1 parent b869213 commit 7379f01
Show file tree
Hide file tree
Showing 10 changed files with 157 additions and 27 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 2 additions & 0 deletions doc/toolbox.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion pandapower/grid_equivalents/get_equivalent.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down
7 changes: 2 additions & 5 deletions pandapower/grid_equivalents/toolbox.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
15 changes: 11 additions & 4 deletions pandapower/plotting/generic_geodata.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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:
Expand All @@ -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)
Expand All @@ -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:
Expand Down
3 changes: 1 addition & 2 deletions pandapower/shortcircuit/toolbox.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"]
Expand Down
62 changes: 62 additions & 0 deletions pandapower/test/toolbox/test_element_selection.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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"])
62 changes: 62 additions & 0 deletions pandapower/toolbox/element_selection.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
27 changes: 14 additions & 13 deletions pandapower/topology/create_graph.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand All @@ -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
Expand All @@ -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:
Expand All @@ -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

Expand All @@ -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

Expand All @@ -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

Expand All @@ -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

Expand All @@ -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

Expand Down Expand Up @@ -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):
Expand Down Expand Up @@ -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
Expand All @@ -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

Expand Down
3 changes: 1 addition & 2 deletions pandapower/topology/graph_searches.py
Original file line number Diff line number Diff line change
Expand Up @@ -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))


Expand Down

0 comments on commit 7379f01

Please sign in to comment.