Skip to content

Commit fe52596

Browse files
authored
Issue/generic geodata (#2995)
* fixed issue where generic geodata could not be created if some geodata already existed or the column was not present * updated the notebook to ignore warnings instead of capturing all output and only displaying specifics. * added typechecking import for igraph * fixed issue with None as buses to _prepare_geodata_table * fixed issue with non-standard tables
1 parent fbdbab5 commit fe52596

5 files changed

Lines changed: 2078 additions & 715 deletions

File tree

pandapower/create/_utils.py

Lines changed: 14 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,14 @@ def add_column_to_df(net: ADict, table_name: str, column_name: str) -> None:
3333
"""
3434
Adds column to table if not present, if table not present adds table
3535
Only works for columns that are defined in the network structure dict
36+
37+
Parameters:
38+
net: ADict object (pandapowerNet)
39+
table_name: the DataFrame to which to add the column
40+
column_name: the column to add to the DataFrame
41+
42+
Raises:
43+
ValueError: if column is not defined in table schema
3644
"""
3745
if table_name in net and column_name in net[table_name]:
3846
return
@@ -360,19 +368,18 @@ def _set_entries(net, table, index, preserve_dtypes=True, entries: dict | None =
360368
# only get dtypes of columns that are set and that are already present in the table
361369
dtypes = net[table][intersect1d(net[table].columns, list(entries))].dtypes
362370

371+
dtype_dict = get_structure_dict(required_only=False)[table]
363372
for col, val in entries.items():
364373
val_not_na: bool = pd.notna(val) if pd.api.types.is_scalar(val) else pd.notna(val).any()
365374
if val_not_na:
366375
net[table].at[index, col] = val
367-
try:
368-
dtype = get_structure_dict(required_only=False)[table][col]
369-
if (
370-
dtype == bool and net[table][col].isna().any()
371-
): # default value for bool entries # TODO: check if wanted behaviour
376+
# set col dtype:
377+
if col in dtype_dict:
378+
dtype = dtype_dict[col]
379+
# default value for bool entries
380+
if dtype == bool and net[table][col].isna().any(): # TODO: check if wanted behaviour
372381
net[table][col] = net[table][col].astype(pd.BooleanDtype()).fillna(False)
373382
net[table][col] = net[table][col].astype(dtype)
374-
except KeyError as e:
375-
logger.error(f"column {col} has no dtype in network structure")
376383

377384
# and preserve dtypes
378385
if preserve_dtypes:

pandapower/plotting/generic_geodata.py

Lines changed: 98 additions & 66 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,18 @@
1-
# -*- coding: utf-8 -*-
2-
31
# Copyright (c) 2016-2026 by University of Kassel and Fraunhofer Institute for Energy Economics
42
# and Energy System Technology (IEE), Kassel. All rights reserved.
53

64
import sys
75
import copy
6+
from typing import TYPE_CHECKING, Iterable
87

98
import geojson
9+
import networkx
1010
import networkx as nx
1111
import pandas as pd
1212
import numpy as np
1313

14-
from pandapower.auxiliary import soft_dependency_error
14+
from pandapower.auxiliary import pandapowerNet, soft_dependency_error
15+
from pandapower.create._utils import add_column_to_df
1516
from pandapower.topology.create_graph import create_nxgraph
1617
from pandapower.topology.graph_searches import connected_components
1718

@@ -23,24 +24,31 @@
2324

2425
import logging
2526

27+
if TYPE_CHECKING:
28+
import igraph
29+
2630
logger = logging.getLogger(__name__)
2731

2832

29-
def build_igraph_from_pp(net, respect_switches=False, buses=None, trafo_length_km=0.01, switch_length_km=0.001,
30-
dcline_length_km=1.0):
33+
def build_igraph_from_pp(
34+
net: pandapowerNet,
35+
respect_switches: bool = False,
36+
buses=None,
37+
trafo_length_km=0.01,
38+
switch_length_km=0.001,
39+
dcline_length_km=1.0
40+
):
3141
"""
3242
This function uses the igraph library to create an igraph graph for a given pandapower network.
3343
Lines, transformers and switches are respected.
3444
Performance vs. networkx: https://graph-tool.skewed.de/performance
3545
36-
:param net: pandapower network
37-
:type net: pandapowerNet
38-
:param respect_switches: if True, exclude edges for open switches (also lines that are \
39-
connected via line switches)
40-
:type respect_switches: bool, default False
46+
Parameters:
47+
net: the pandapower network
48+
respect_switches: if True, exclude edges for open switches (also lines that are connected via line switches)
4149
42-
:Example:
43-
graph, meshed, roots = build_igraph_from_pp(net)
50+
Example:
51+
>>> graph, meshed, roots = build_igraph_from_pp(net)
4452
"""
4553
if not IGRAPH_INSTALLED:
4654
soft_dependency_error(str(sys._getframe().f_code.co_name)+"()", "igraph")
@@ -123,19 +131,23 @@ def _get_switch_mask(net, element, switch_element, open_switches):
123131
open_element_mask = np.isin(net[element].index, open_elements, invert=True)
124132
return open_element_mask
125133

126-
def coords_from_igraph(graph, roots, meshed=False, calculate_meshed=False):
134+
def coords_from_igraph(
135+
graph: "igraph.Graph",
136+
roots: Iterable,
137+
meshed: bool = False,
138+
calculate_meshed: bool = False
139+
) -> list[list[float]]:
127140
"""
128141
Create a list of generic coordinates from an igraph graph layout.
129142
130-
:param graph: The igraph graph on which the coordinates shall be based
131-
:type graph: igraph.Graph
132-
:param roots: The root buses of the graph
133-
:type roots: iterable
134-
:param meshed: determines if the graph has any meshes
135-
:type meshed: bool, default False
136-
:param calculate_meshed: determines whether to calculate the meshed status
137-
:type calculate_meshed: bool, default False
138-
:return: coords - list of coordinates from the graph layout
143+
Parameters:
144+
graph: The igraph graph on which the coordinates shall be based
145+
roots: The root buses of the graph
146+
meshed: determines if the graph has any meshes
147+
calculate_meshed: determines whether to calculate the meshed status
148+
149+
Return:
150+
list of coordinates from the graph layout
139151
"""
140152
if calculate_meshed:
141153
meshed = False
@@ -151,15 +163,19 @@ def coords_from_igraph(graph, roots, meshed=False, calculate_meshed=False):
151163
return list(zip(*layout.coords))
152164

153165

154-
def coords_from_nxgraph(mg=None, layout_engine='neato'):
166+
def coords_from_nxgraph(
167+
mg: networkx.Graph = None,
168+
layout_engine: str = 'neato'
169+
) -> list[list[float]]:
155170
"""
156171
Create a list of generic coordinates from a networkx graph layout.
157172
158-
:param mg: The networkx graph on which the coordinates shall be based
159-
:type mg: networkx.Graph
160-
:param layout_engine: GraphViz Layout Engine for layouting a network. See https://graphviz.org/docs/layouts/
161-
:type layout_engine: str
162-
:return: coords - list of coordinates from the graph layout
173+
Parameters:
174+
mg: The networkx graph on which the coordinates shall be based
175+
layout_engine: GraphViz Layout Engine for layouting a network. See https://graphviz.org/docs/layouts/
176+
177+
Return:
178+
list of coordinates from the graph layout
163179
"""
164180
# workaround for bug in agraph
165181
for u, v in mg.edges(data=False):
@@ -171,42 +187,43 @@ def coords_from_nxgraph(mg=None, layout_engine='neato'):
171187
return list(zip(*(list(nx.drawing.nx_agraph.graphviz_layout(mg, prog=layout_engine).values()))))
172188

173189

174-
def create_generic_coordinates(net, mg=None, library="igraph",
175-
respect_switches=False,
176-
geodata_table="bus",
177-
buses=None,
178-
overwrite=False,
179-
layout_engine='neato',
180-
trafo_length_km=0.01,
181-
switch_length_km=0.001):
190+
def create_generic_coordinates(
191+
net: pandapowerNet,
192+
mg: networkx.Graph = None,
193+
library: str = "igraph",
194+
respect_switches: bool = False,
195+
geodata_table: str = "bus",
196+
buses: Iterable[int] = None,
197+
overwrite: bool = False,
198+
layout_engine: str = 'neato',
199+
trafo_length_km: float = 0.01,
200+
switch_length_km: float = 0.001
201+
) -> pandapowerNet:
182202
"""
183203
This function will add arbitrary geo-coordinates for all buses based on an analysis of branches
184204
and rings. It will remove out of service buses/lines from the net. The coordinates will be
185205
created either by igraph or by using networkx library.
186206
187-
:param net: pandapower network
188-
:type net: pandapowerNet
189-
:param mg: Existing networkx multigraph, if available. Convenience to save computation time.
190-
:type mg: networkx.Graph
191-
:param respect_switches: respect switches in a network for generic coordinates
192-
:type respect_switches: bool
193-
:param library: "igraph" to use igraph package or "networkx" to use networkx package
194-
:type library: str
195-
:param geodata_table: table to write the generic geodatas to
196-
:type geodata_table: str
197-
:param buses: buses for which generic geodata are created, all buses will be used by default
198-
:type buses: list
199-
:param overwrite: overwrite existing geodata
200-
:type overwrite: bool
201-
:param layout_engine: GraphViz Layout Engine for layouting a network. See https://graphviz.org/docs/layouts/
202-
:type layout_engine: str
203-
:return: net - pandapower network with added geo coordinates for the buses
204-
205-
:Example:
207+
Parameters:
208+
net: pandapower network
209+
mg: Existing networkx multigraph, if available. Convenience to save computation time.
210+
respect_switches: respect switches in a network for generic coordinates
211+
library: "igraph" to use igraph package or "networkx" to use networkx package
212+
geodata_table: table to write the generic geodatas to
213+
buses: buses for which generic geodata are created, all buses will be used by default
214+
overwrite: overwrite existing geodata
215+
layout_engine: GraphViz Layout Engine for layouting a network. See https://graphviz.org/docs/layouts/
216+
217+
Return:
218+
the pandapower network with added geo coordinates for the buses.
219+
Does not copy the network, so the original network will be modified!
220+
221+
Example:
206222
>>> net = create_generic_coordinates(net)
207223
"""
208-
209-
_prepare_geodata_table(net, geodata_table, overwrite)
224+
if buses is None:
225+
buses = net[geodata_table].index.tolist()
226+
_prepare_geodata_table(net, geodata_table, overwrite, buses)
210227
if library == "igraph":
211228
if not IGRAPH_INSTALLED:
212229
soft_dependency_error("build_igraph_from_pp()", "igraph")
@@ -224,23 +241,38 @@ def create_generic_coordinates(net, mg=None, library="igraph",
224241
else:
225242
raise ValueError("Unknown library %s - chose 'igraph' or 'networkx'" % library)
226243
if len(coords):
227-
net[geodata_table]["geo"] = pd.Series(
228-
map(lambda x: geojson.dumps(geojson.Point((x[1], x[0])), sort_keys=True), zip(*coords)),
229-
index=net[geodata_table].index if buses is None else buses,
244+
net[geodata_table]["geo"][buses] = pd.Series(
245+
data=map(lambda x: geojson.dumps(geojson.Point((x[1], x[0])), sort_keys=True), zip(*coords)),
246+
index=buses,
230247
)
231248
return net
232249

233250

234-
def _prepare_geodata_table(net, geodata_table, overwrite):
235-
if geodata_table in net and "geo" in net[geodata_table] and net[geodata_table]["geo"].dropna().shape[0]:
251+
def _prepare_geodata_table(
252+
net: pandapowerNet, geodata_table: str, overwrite: bool, elements: Iterable[int] | None
253+
) -> None:
254+
if geodata_table not in net or "geo" not in net[geodata_table]:
255+
try:
256+
add_column_to_df(net, geodata_table, "geo")
257+
except KeyError as e:
258+
logger.warning("Creating geodata for a unknown table")
259+
if geodata_table not in net:
260+
net[geodata_table] = pd.DataFrame(columns=["geo"], index=elements, dtype=pd.StringDtype())
261+
else:
262+
net[geodata_table]["geo"] = pd.NA
263+
if elements is None:
264+
elements = net[geodata_table].index.tolist()
265+
try:
266+
net[geodata_table].loc[elements]
267+
except KeyError as e:
268+
logger.error(f"While preparing geodata table for {geodata_table} a nonexistent bus was passed!")
269+
raise e
270+
if net[geodata_table].loc[elements, "geo"].dropna().shape[0]:
236271
if overwrite:
237-
net[geodata_table] = net[geodata_table].drop("geo", axis=1)
238-
net[geodata_table] = net[geodata_table].dropna(how='all')
272+
net[geodata_table].loc[elements, "geo"] = pd.NA
239273
else:
240274
raise UserWarning(f"Table {geodata_table} is not empty - use overwrite=True to overwrite existing geodata")
241275

242-
if geodata_table not in net:
243-
net[geodata_table] = pd.DataFrame(columns=["geo"])
244276

245277
def fuse_geodata(net):
246278
mg = create_nxgraph(net, include_lines=False, include_impedances=False, respect_switches=False)

pandapower/plotting/simple_plot.py

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -725,13 +725,15 @@ def simple_plot(
725725
respect_switches = False
726726

727727
# create generic coordinates if no geodata is available
728-
if (len(net.line.geo) == 0 and len(net.bus.geo) == 0) or (
729-
net.line.geo.isna().any() and net.bus.geo.isna().any()):
728+
if ('geo' not in net.line.columns or 'geo' not in net.bus.columns or
729+
(len(net.line.geo) == 0 and len(net.bus.geo) == 0) or (
730+
net.line.geo.isna().any() and net.bus.geo.isna().any())
731+
):
730732
logger.warning(
731-
"No or insufficient geodata available --> Creating artificial coordinates."
732-
" This may take some time"
733+
"No or insufficient geodata available --> Creating artificial coordinates. This may take some time"
733734
)
734-
create_generic_coordinates(net, respect_switches=respect_switches, library=library)
735+
buses = net.bus.index.tolist() if "geo" not in net.bus else net.bus.index[net.bus.geo.isna()].tolist()
736+
create_generic_coordinates(net, respect_switches=respect_switches, library=library, buses=buses)
735737

736738
if scale_size:
737739
# scale all symbol sizes relative to the mean distance between buses

pandapower/test/plotting/test_generic_coordinates.py

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
# and Energy System Technology (IEE), Kassel. All rights reserved.
55

66
import numpy as np
7+
import pandas as pd
78
import pytest
89

910
from pandapower.networks.simple_pandapower_test_networks import simple_four_bus_system
@@ -38,9 +39,18 @@ def test_create_generic_coordinates_nx():
3839
@pytest.mark.skipif(IGRAPH_INSTALLED is False, reason="Requires igraph.")
3940
def test_create_generic_coordinates_igraph_custom_table_index():
4041
net = simple_four_bus_system()
41-
for buses in [[0, 1], [0, 2], [0, 1, 2]]:
42-
create_generic_coordinates(net, buses=buses, geodata_table="test", overwrite=True)
43-
assert np.all(net.test.index == buses)
42+
create_generic_coordinates(net, geodata_table="bus", overwrite=True)
43+
assert pd.notna(net.bus.geo).all()
44+
net.bus.geo[[0, 2]] = pd.NA
45+
create_generic_coordinates(net, geodata_table="bus", buses=[0, 2])
46+
assert pd.notna(net.bus.geo).all()
47+
net.bus.geo.at[0] = "Hallo"
48+
create_generic_coordinates(net, geodata_table="bus", buses=[0], overwrite=True)
49+
assert net.bus.geo.at[0] != "Hallo"
50+
51+
net["test"] = pd.DataFrame(data=["T1", "T2", "T3"], columns=["name"])
52+
create_generic_coordinates(net, geodata_table="test")
53+
assert pd.notna(net.test.geo).all()
4454

4555

4656
if __name__ == "__main__":

0 commit comments

Comments
 (0)