diff --git a/pandapower/plotting/generic_geodata.py b/pandapower/plotting/generic_geodata.py index 4c754a182..5450e97f0 100644 --- a/pandapower/plotting/generic_geodata.py +++ b/pandapower/plotting/generic_geodata.py @@ -8,9 +8,23 @@ import networkx as nx import pandas as pd +import numpy as np import pandapower.topology as top +try: + import igraph + IGRAPH_INSTALLED = True +except ImportError: + IGRAPH_INSTALLED = False + +try: + import pplog as logging +except ImportError: + import logging + +logger = logging.getLogger(__name__) + def build_igraph_from_pp(net, respect_switches=False): """ @@ -18,14 +32,14 @@ def build_igraph_from_pp(net, respect_switches=False): Lines, transformers and switches are respected. Performance vs. networkx: https://graph-tool.skewed.de/performance - Input: - - **net** - pandapower network - - Example: - - graph = build_igraph_from_pp(net + :param net: pandapower network + :type net: pandapowerNet + :param respect_switches: if True, exclude edges for open switches (also lines that are \ + connected via line switches) + :type respect_switches: bool, default False + :Example: + graph, meshed, roots = build_igraph_from_pp(net) """ try: import igraph as ig @@ -33,16 +47,16 @@ def build_igraph_from_pp(net, respect_switches=False): raise ImportError("Please install python-igraph") g = ig.Graph(directed=True) g.add_vertices(net.bus.shape[0]) - g.vs["label"] = net.bus.index.tolist() # [s.encode('unicode-escape') for s in net.bus.name.tolist()] + # g.vs["label"] = [s.encode('unicode-escape') for s in net.bus.name.tolist()] + g.vs["label"] = net.bus.index.tolist() pp_bus_mapping = dict(list(zip(net.bus.index, list(range(net.bus.index.shape[0]))))) # add lines nogolines = set(net.switch.element[(net.switch.et == "l") & (net.switch.closed == 0)]) \ - if respect_switches else set() + if respect_switches else set() for lix in (ix for ix in net.line.index if ix not in nogolines): fb, tb = net.line.at[lix, "from_bus"], net.line.at[lix, "to_bus"] - g.add_edge(pp_bus_mapping[fb], pp_bus_mapping[tb]) - g.es["weight"] = net.line.length_km.values + g.add_edge(pp_bus_mapping[fb], pp_bus_mapping[tb], weight=net.line.at[lix, "length_km"]) # add trafos for _, trafo in net.trafo.iterrows(): @@ -54,7 +68,7 @@ def build_igraph_from_pp(net, respect_switches=False): # add switches bs = net.switch[(net.switch.et == "b") & (net.switch.closed == 1)] if respect_switches else \ - net.switch[(net.switch.et == "b")] + net.switch[(net.switch.et == "b")] for fb, tb in zip(bs.bus, bs.element): g.add_edge(pp_bus_mapping[fb], pp_bus_mapping[tb], weight=0.001) @@ -68,78 +82,107 @@ def build_igraph_from_pp(net, respect_switches=False): return g, meshed, roots # g, (not g.is_dag()) -def create_generic_coordinates(net, mg=None, library="igraph", respect_separation_points=False, - name_node='bus'): +def coords_from_igraph(graph, roots, meshed=False, calculate_meshed=False): + """ + Create a list of generic coordinates from an igraph graph layout. + + :param graph: The igraph graph on which the coordinates shall be based + :type graph: igraph.Graph + :param roots: The root buses of the graph + :type roots: iterable + :param meshed: determines if the graph has any meshes + :type meshed: bool, default False + :param calculate_meshed: determines whether to calculate the meshed status + :type calculate_meshed: bool, default False + :return: coords - list of coordinates from the graph layout """ - 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 created either by igraph or by - using networkx library. + if calculate_meshed: + meshed = False + for i in range(1, net.bus.shape[0]): + if len(g.get_all_shortest_paths(0, i, mode="ALL")) > 1: + meshed = True + break + if meshed is True: + layout = graph.layout("kk") + else: + graph.to_undirected(mode="each", combine_edges="first") + layout = graph.layout("rt", root=roots) + return list(zip(*layout.coords)) - INPUT: - **net** - pandapower network - OPTIONAL: - **mg** - Existing networkx multigraph, if available. Convenience to save computation time. +def coords_from_nxgraph(mg=None): + """ + Create a list of generic coordinates from a networkx graph layout. - **library** - "igraph" to use igraph package or "networkx" to use networkx package + :param mg: The networkx graph on which the coordinates shall be based + :type mg: networkx.Graph + :return: coords - list of coordinates from the graph layout + """ + # workaround for bug in agraph + for u, v in mg.edges(data=False, keys=False): + if 'key' in mg[int(u)][int(v)]: + del mg[int(u)][int(v)]['key'] + if 'key' in mg[int(u)][int(v)][0]: + del mg[int(u)][int(v)][0]['key'] + # ToDo: Insert fallback layout for nxgraph + return list(zip(*(list(nx.drawing.nx_agraph.graphviz_layout(mg, prog='neato').values())))) - OUTPUT: - **net** - pandapower network with added geo coordinates for the buses - EXAMPLE: +def create_generic_coordinates(net, mg=None, library="igraph", respect_switches=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 + created either by igraph or by using networkx library. + + :param net: pandapower network + :type net: pandapowerNet + :param mg: Existing networkx multigraph, if available. Convenience to save computation time. + :type mg: networkx.Graph + :param library: "igraph" to use igraph package or "networkx" to use networkx package + :type library: str + :return: net - pandapower network with added geo coordinates for the buses + + :Example: net = create_generic_coordinates(net) - """ - if "name_node +'_geodata'" in net and net[name_node +'_geodata'].shape[0]: - print("Please delete all geodata. This function cannot be used with pre-existing geodata.") + + if "bus_geodata" in net and net.bus_geodata.shape[0]: + logger.warning("Please delete all geodata. This function cannot be used with pre-existing" + " geodata.") return - if not "name_node +'_geodata'" in net or net[name_node +'_geodata'] is None: - net[name_node +'_geodata'] = pd.DataFrame(columns=["x", "y"]) + if "bus_geodata" not in net or net.bus_geodata is None: + net.bus_geodata = pd.DataFrame(columns=["x", "y"]) gnet = copy.deepcopy(net) - gnet[name_node] = gnet[name_node][gnet[name_node].in_service == True] + gnet.bus = gnet.bus[gnet.bus.in_service == True] + if library == "igraph": - try: - import igraph - except ImportError: - raise UserWarning("The library igraph is selected for plotting, " - "but not installed correctly.") - graph, meshed, roots = build_igraph_from_pp(gnet, respect_separation_points) - if meshed: - layout = graph.layout("kk") - else: - graph.to_undirected(mode="each", combine_edges="first") - layout = graph.layout("rt", root=roots) - coords = list(zip(*layout.coords)) + if not IGRAPH_INSTALLED: + raise UserWarning("The library igraph is selected for plotting, but not installed " + "correctly.") + graph, meshed, roots = create_igraph(net, respect_switches) + coords = coords_from_igraph(graph, meshed, roots) elif library == "networkx": if mg is None: - nxg = top.create_nxgraph(gnet, respect_separation_points) + nxg = create_mg(gnet, respect_switches) else: nxg = copy.deepcopy(mg) - # workaround for bug in agraph - for u, v in nxg.edges(data=False, keys=False): - if 'key' in nxg[int(u)][int(v)]: - del nxg[int(u)][int(v)]['key'] - if 'key' in nxg[int(u)][int(v)][0]: - del nxg[int(u)][int(v)][0]['key'] - # ToDo: Insert fallback layout for nxgraph - coords = list(zip(*(list(nx.drawing.nx_agraph.graphviz_layout(nxg, prog='neato').values())))) + coords = coords_from_nxgraph(nxg) else: - raise ValueError("Unknown library %s - chose 'igraph' or 'networkx'"%library) - net[name_node +'_geodata'].x = coords[1] - net[name_node +'_geodata'].y = coords[0] - net[name_node +'_geodata'].index = gnet[name_node].index + raise ValueError("Unknown library %s - chose 'igraph' or 'networkx'" % library) + + net.bus_geodata.x = coords[1] + net.bus_geodata.y = coords[0] + net.bus_geodata.index = gnet.bus.index return net -def fuse_geodata(net, name_node_geodata='bus'): - name_node = name_node_geodata +def fuse_geodata(net): mg = top.create_nxgraph(net, include_lines=False, include_impedances=False, respect_switches=False) - geocoords = set(net[name_node +'_geodata'].index) + geocoords = set(net.bus_geodata.index) for area in top.connected_components(mg): if len(area & geocoords) > 1: - geo = net[name_node +'_geodata'].loc[area & geocoords].values[0] - for name_node in area: - net[name_node +'_geodata'].loc[name_node] = geo - + geo = net.bus_geodata.loc[area & geocoords].values[0] + for bus in area: + net.bus_geodata.loc[bus] = geo diff --git a/pandapower/plotting/geo.py b/pandapower/plotting/geo.py index 397e24428..0cd328fb2 100644 --- a/pandapower/plotting/geo.py +++ b/pandapower/plotting/geo.py @@ -7,37 +7,112 @@ pass -def convert_geodata_to_gis(net, epsg=31467, node_geodata=True, branch_geodata=True, - name_node_geodata='bus', name_branch_geodata='line'): - name_node = name_node_geodata - name_branch = name_branch_geodata +def _node_geometries_from_geodata(node_geo, epsg=31467): + """ + Creates a geopandas geodataframe from a given dataframe of with node coordinates as x and y + values. + + :param node_geo: The dataframe containing the node coordinates (x and y values) + :type node_geo: pandas.dataframe + :param epsg: The epsg projection of the node coordinates + :type epsg: int, default 31467 (= Gauss-Krüger Zone 3) + :return: node_geodata - a geodataframe containing the node_geo and Points in the geometry column + """ + geoms = [Point(x, y) for x, y in node_geo[["x", "y"]].values] + return GeoDataFrame(node_geo, crs=from_epsg(epsg), geometry=geoms, index=node_geo.index) + + +def _branch_geometries_from_geodata(branch_geo, epsg=31467): + geoms = GeoSeries([LineString(x) for x in branch_geo.coords.values], index=branch_geo.index, + crs=from_epsg(epsg)) + return GeoDataFrame(branch_geo, crs=from_epsg(epsg), geometry=geoms, index=branch_geo.index) + + +def _transform_node_geometry_to_geodata(node_geo): + """ + Create x and y values from geodataframe + + :param node_geo: The dataframe containing the node geometries (as shapely points) + :type node_geo: geopandas.GeoDataFrame + :return: bus_geo - The given geodataframe with x and y values + """ + node_geo["x"] = [p.x for p in node_geo.geometry] + node_geo["y"] = [p.y for p in node_geo.geometry] + return node_geo + + +def _transform_branch_geometry_to_coords(branch_geo): + """ + Create coords entries from geodataframe geometries + + :param branch_geo: The dataframe containing the branch geometries (as shapely LineStrings) + :type branch_geo: geopandas.GeoDataFrame + :return: branch_geo - The given geodataframe with coords + """ + branch_geo["coords"] = branch_geo["coords"].geometry.apply(lambda x: list(x.coords)) + return branch_geo + + +def _convert_xy_epsg(x, y, epsg_in=4326, epsg_out=31467): + """ + Converts the given x and y coordinates according to the defined epsg projections. + + :param x: x-values of coordinates + :type x: iterable + :param y: y-values of coordinates + :type y: iterable + :param epsg_in: current epsg projection + :type epsg_in: int, default 4326 (= WGS84) + :param epsg_out: epsg projection to be transformed to + :type epsg_out: int, default 31467 (= Gauss-Krüger Zone 3) + :return: transformed_coords - x and y values in new coordinate system + """ + in_proj = Proj(init='epsg:%i' % epsg_in) + out_proj = Proj(init='epsg:%i' % epsg_out) + return transform(in_proj, out_proj, x, y) + + +def convert_gis_to_geodata(net, node_geodata=True, branch_geodata=True): + """ + Extracts information on bus and line geodata from the geometries of a geopandas geodataframe. + + :param net: The net for which to convert the geodata + :type net: pandapowerNet + :param node_geodata: flag if to extract x and y values for bus geodata + :type node_geodata: bool, default True + :param branch_geodata: flag if to extract coordinates values for line geodata + :type branch_geodata: bool, default True + :return: No output. + """ if node_geodata: - node_geo = net[name_node + '_geodata'] - geo = [Point(x, y) for x, y in node_geo[["x", "y"]].values] - net[name_node + '_geodata'] = GeoDataFrame(node_geo, crs=from_epsg(epsg), geometry=geo, - index=node_geo.index) + _transform_node_geometry_to_geodata(net.bus_geodata) if branch_geodata: - branch_geo = net[name_branch + '_geodata'] - geo = GeoSeries([LineString(x) for x in net[name_branch + '_geodata'].coords.values], - index=net[name_branch + '_geodata'].index, crs=from_epsg(epsg)) - net[name_branch + '_geodata'] = GeoDataFrame(branch_geo, crs=from_epsg(epsg), geometry=geo, - index=branch_geo.index) - net["gis_epsg_code"] = epsg + _transform_branch_geometry_to_coords(net.line_geodata) -def convert_gis_to_geodata(net, node_geodata=True, branch_geodata=True, name_node_geodata='bus', - name_branch_geodata='line'): - name_node = name_node_geodata - name_branch = name_branch_geodata +def convert_geodata_to_gis(net, epsg=31467, node_geodata=True, branch_geodata=True): + """ + Transforms the bus and line geodata of a net into a geopandaas geodataframe with the respective + geometries. + + :param net: The net for which to convert the geodata + :type net: pandapowerNet + :param epsg: current epsg projection + :type epsg: int, default 4326 (= WGS84) + :param node_geodata: flag if to transform the bus geodata table + :type node_geodata: bool, default True + :param branch_geodata: flag if to transform the line geodata table + :type branch_geodata: bool, default True + :return: No output. + """ if node_geodata: - net[name_node + '_geodata']["x"] = [x.x for x in net[name_node + '_geodata'].geometry] - net[name_node + '_geodata']["y"] = [x.y for x in net[name_node + '_geodata'].geometry] + net["bus_geodata"] = _node_geometries_from_geodata(net["bus_geodata"], epsg) if branch_geodata: - net[name_branch +'_geodata']["coords"] = \ - net[name_branch +'_geodata'].geometry.apply(lambda x: list(x.coords)) + net["line_geodata"] = _branch_geometries_from_geodata(net["line_geodata"], epsg) + net["gis_epsg_code"] = epsg -def convert_epgs_bus_geodata(net, epsg_in=4326, epsg_out=31467, name_node_geodata='bus'): +def convert_epsg_bus_geodata(net, epsg_in=4326, epsg_out=31467): """ Converts bus geodata in net from epsg_in to epsg_out @@ -49,11 +124,6 @@ def convert_epgs_bus_geodata(net, epsg_in=4326, epsg_out=31467, name_node_geodat :type epsg_out: int, default 31467 (= Gauss-Krüger Zone 3) :return: net - the given pandapower network (no copy!) """ - name_node = name_node_geodata - in_proj = Proj(init='epsg:%i' % epsg_in) - out_proj = Proj(init='epsg:%i' % epsg_out) - x1, y1 = net[name_node + '_geodata'].loc[:, "x"].values, \ - net[name_node + '_geodata'].loc[:, "y"].values - net[name_node + '_geodata'].loc[:, "x"], net[name_node + '_geodata'].loc[:, "y"] = \ - transform(in_proj, out_proj, x1, y1) + net['bus_geodata'].loc[:, "x"], net['bus_geodata'].loc[:, "y"] = _convert_xy_epsg( + net['bus_geodata'].loc[:, "x"], net['bus_geodata'].loc[:, "y"], epsg_in, epsg_out) return net