Skip to content

Commit

Permalink
Merge pull request e2nIEE#581 from dlohmeier/dev_plotting
Browse files Browse the repository at this point in the history
more generic formulations in geodata creation and manipulation for plotting
  • Loading branch information
dlohmeier authored Dec 11, 2019
2 parents 02973c0 + 48bd1ed commit 58e5c24
Show file tree
Hide file tree
Showing 2 changed files with 206 additions and 93 deletions.
169 changes: 106 additions & 63 deletions pandapower/plotting/generic_geodata.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,41 +8,55 @@

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):
"""
This function uses the igraph library to create an igraph graph for a given pandapower network.
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
except (DeprecationWarning, ImportError):
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():
Expand All @@ -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)

Expand All @@ -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
130 changes: 100 additions & 30 deletions pandapower/plotting/geo.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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

0 comments on commit 58e5c24

Please sign in to comment.