-
Notifications
You must be signed in to change notification settings - Fork 465
/
create_graph.py
350 lines (285 loc) · 14.6 KB
/
create_graph.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
# -*- coding: utf-8 -*-
# Copyright (c) 2016-2020 by University of Kassel and Fraunhofer Institute for Energy Economics
# and Energy System Technology (IEE), Kassel. All rights reserved.
from itertools import combinations
import networkx as nx
import numpy as np
from pandapower.auxiliary import _init_nx_options
from pandapower.build_branch import _calc_impedance_parameters_from_dataframe, \
_calc_branch_values_from_trafo_df, \
_trafo_df_from_trafo3w
from pandapower.build_bus import _build_bus_ppc
from pandapower.pd2ppc import _init_ppc
from pandapower.pypower.idx_bus import BASE_KV
try:
import pplog as logging
except ImportError:
import logging
try:
from pandapower.topology.graph_tool_interface import GraphToolInterface
graph_tool_available = True
except:
graph_tool_available = False
INDEX = 0
F_BUS = 1
T_BUS = 2
WEIGHT = 0
BR_R = 1
BR_X = 2
BR_Z = 3
logger = logging.getLogger(__name__)
def create_nxgraph(net, respect_switches=True, include_lines=True, include_impedances=True,
include_dclines=True, include_trafos=True, include_trafo3ws=True,
nogobuses=None, notravbuses=None, multi=True,
calc_branch_impedances=False, branch_impedance_unit="ohm",
library="networkx", include_out_of_service=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
(Note: only buses with in_service = 1 appear in the graph), edges represent physical
connections between buses (typically lines or trafos).
INPUT:
**net** (pandapowerNet) - variable that contains a pandapower network
OPTIONAL:
**respect_switches** (boolean, True) - True: open switches (line, trafo, bus) are being \
considered (no edge between nodes)
False: open switches are being ignored
**include_lines** (boolean or index, True) - determines, whether or which lines get
converted to edges
**include_impedances** (boolean or , True) - determines, whether or which per unit
impedances (net.impedance) are converted to edges
**include_dclines** (boolean or index, True) - determines, whether or which dclines get
converted to edges
**include_trafos** (boolean or index, True) - determines, whether or which trafos get
converted to edges
**include_trafo3ws** (boolean or index, True) - determines, whether or which trafo3ws 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
considered in the graph
**multi** (boolean, True) - True: The function generates a NetworkX MultiGraph, which allows
multiple parallel edges between nodes
False: NetworkX Graph (no multiple parallel edges)
**calc_branch_impedances** (boolean, False) - determines wether impedances are calculated
and added as a weight to all branches or not. Impedances can be added in ohm or per unit
(see branch_impedance unit parameter). DC Lines are considered as infinity.
**branch_impedance_unit** (str, "ohm") - defines the unit of the branch impedance for
calc_branch_impedances=True. If it is set to "ohm", the parameters 'r_ohm',
'x_ohm' and 'z_ohm' are added to each branch. If it is set to "pu", the
parameters are 'r_pu', 'x_pu' and 'z_pu'.
**include_out_of_service** (bool, False) - defines if out of service buses are included in the nx graph
OUTPUT:
**mg** - Returns the required NetworkX graph
EXAMPLE:
import pandapower.topology as top
mg = top.create_nx_graph(net, respect_switches = False)
# converts the pandapower network "net" to a MultiGraph. Open switches will be ignored.
"""
if multi:
if graph_tool_available and library == "graph_tool":
mg = GraphToolInterface(net.bus.index)
else:
mg = nx.MultiGraph()
else:
mg = nx.Graph()
if branch_impedance_unit not in ["ohm", "pu"]:
raise ValueError("branch impedance unit can be either 'ohm' or 'pu'")
if respect_switches:
open_sw = ~net.switch.closed.values.astype(bool)
if calc_branch_impedances:
ppc = get_nx_ppc(net)
line = get_edge_table(net, "line", include_lines)
if line is not None:
indices, parameter, in_service = init_par(line, calc_branch_impedances)
indices[:, F_BUS] = line.from_bus.values
indices[:, T_BUS] = line.to_bus.values
if respect_switches:
mask = (net.switch.et.values == "l") & open_sw
if mask.any():
open_lines = net.switch.element.values[mask]
open_lines_mask = np.in1d(indices[:, INDEX], open_lines)
in_service &= ~open_lines_mask
parameter[:, WEIGHT] = line.length_km.values
if calc_branch_impedances:
baseR = get_baseR(net, ppc, line.from_bus.values) \
if branch_impedance_unit == "pu" else 1
line_length = line.length_km.values / line.parallel.values
r = line.r_ohm_per_km.values * line_length
x = line.x_ohm_per_km.values * line_length
parameter[:, BR_R] = r / baseR
parameter[:, BR_X] = x / baseR
add_edges(mg, indices, parameter, in_service, net, "line", calc_branch_impedances,
branch_impedance_unit)
impedance = get_edge_table(net, "impedance", include_impedances)
if impedance is not None:
indices, parameter, in_service = init_par(impedance, calc_branch_impedances)
indices[:, F_BUS] = impedance.from_bus.values
indices[:, T_BUS] = impedance.to_bus.values
if calc_branch_impedances:
baseR = get_baseR(net, ppc, impedance.from_bus.values) \
if branch_impedance_unit == "ohm" else 1
r, x, _, _ = _calc_impedance_parameters_from_dataframe(net)
parameter[:, BR_R] = r * baseR
parameter[:, BR_X] = x * baseR
add_edges(mg, indices, parameter, in_service, net, "impedance",
calc_branch_impedances, branch_impedance_unit)
dclines = get_edge_table(net, "dcline", include_dclines)
if dclines is not None:
indices, parameter, in_service = init_par(dclines, calc_branch_impedances)
indices[:, F_BUS] = dclines.from_bus.values
indices[:, T_BUS] = dclines.to_bus.values
if calc_branch_impedances:
parameter[:, BR_R] = np.inf
parameter[:, BR_X] = np.inf
add_edges(mg, indices, parameter, in_service, net, "dcline",
calc_branch_impedances, branch_impedance_unit)
trafo = get_edge_table(net, "trafo", include_trafos)
if trafo is not None:
indices, parameter, in_service = init_par(trafo, calc_branch_impedances)
indices[:, F_BUS] = trafo.hv_bus.values
indices[:, T_BUS] = trafo.lv_bus.values
if respect_switches:
mask = (net.switch.et.values == "t") & open_sw
if mask.any():
open_trafos = net.switch.element.values[mask]
open_trafos_mask = np.in1d(indices[:, INDEX], open_trafos)
in_service &= ~open_trafos_mask
if calc_branch_impedances:
baseR = get_baseR(net, ppc, trafo.hv_bus.values) \
if branch_impedance_unit == "ohm" else 1
r, x, _, _, _ = _calc_branch_values_from_trafo_df(net, ppc, trafo)
parameter[:, BR_R] = r * baseR
parameter[:, BR_X] = x * baseR
add_edges(mg, indices, parameter, in_service, net, "trafo",
calc_branch_impedances, branch_impedance_unit)
trafo3w = get_edge_table(net, "trafo3w", include_trafo3ws)
if trafo3w is not None:
sides = ["hv", "mv", "lv"]
if calc_branch_impedances:
trafo_df = _trafo_df_from_trafo3w(net)
r_all, x_all, _, _, _ = _calc_branch_values_from_trafo_df(net, ppc, trafo_df)
baseR = get_baseR(net, ppc, trafo3w.hv_bus.values) \
if branch_impedance_unit == "ohm" else 1
r = {side: r for side, r in zip(sides, np.split(r_all, 3))}
x = {side: x for side, x in zip(sides, np.split(x_all, 3))}
if respect_switches:
# for trafo3ws the bus where the open switch is located also matters. Open switches
# are therefore defined by the tuple (idx, b) where idx is the trafo3w index and b
# is the bus. To make searching for the open 3w trafos a 1d problem, open 3w switches
# are represented with imaginary numbers as idx + b*1j
mask = (net.switch.et.values == "t3") & open_sw
open_trafo3w_index = net.switch.element.values[mask]
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[:, F_BUS] = trafo3w["%s_bus" % f].values
indices[:, T_BUS] = trafo3w["%s_bus" % t].values
if respect_switches and len(open_trafo3w):
for BUS in [F_BUS, T_BUS]:
open_switch = np.in1d(indices[:, INDEX] + indices[:, BUS] * 1j,
open_trafo3w)
in_service &= ~open_switch
if calc_branch_impedances:
parameter[:, BR_R] = (r[f] + r[t]) * baseR
parameter[:, BR_X] = (x[f] + x[t]) * baseR
add_edges(mg, indices, parameter, in_service, net, "trafo3w",
calc_branch_impedances, branch_impedance_unit)
switch = net.switch
if len(switch):
if respect_switches:
# add edges for closed bus-bus switches
in_service = (switch.et.values == "b") & ~open_sw
else:
# add edges for any bus-bus switches
in_service = (switch.et.values == "b")
indices, parameter = init_par(switch, calc_branch_impedances)
indices[:, F_BUS] = switch.bus.values
indices[:, T_BUS] = switch.element.values
add_edges(mg, indices, parameter, in_service, net, "switch",
calc_branch_impedances, branch_impedance_unit)
# add all buses that were not added when creating branches
if len(mg.nodes()) < len(net.bus.index):
if graph_tool_available and isinstance(mg, GraphToolInterface):
mg.add_vertex(max(net.bus.index) + 1)
else:
for b in set(net.bus.index) - set(mg.nodes()):
mg.add_node(b)
# remove nogobuses
if nogobuses is not None:
for b in nogobuses:
mg.remove_node(b)
# remove the edges pointing away of notravbuses
if notravbuses is not None:
for b in notravbuses:
for i in list(mg[b].keys()):
try:
del mg[b][i] # networkx versions < 2.0
except:
del mg._adj[b][i] # networkx versions 2.0
# remove out of service buses
if not include_out_of_service:
for b in net.bus.index[~net.bus.in_service.values]:
mg.remove_node(b)
return mg
def get_edge_table(net, table_name, include_edges):
"""
Returns the table for the specified name according to parameter include_edges. If it is True,
the whole table is returnedif False, None is returned. If it is an iterable, only the included
indices are chosen.
:param net: pandapower network
:type net: pandapowerNet
:param table_name: name of table to choose from
:type table_name: str
:param include_edges: whether to include this table or not, or the indices to include
:type include_edges: bool, iterable
:return: table - the chosen edge table
:rtype: pd.DataFrame
"""
table = None
if isinstance(include_edges, bool):
if include_edges and len(net[table_name]):
table = net[table_name]
elif len(include_edges):
table = net[table_name].loc[include_edges]
return table
def add_edges(mg, indices, parameter, in_service, net, element, calc_branch_impedances=False,
branch_impedance_unit="ohm"):
# this function is optimized for maximum perfomance, because the loops are called for every
# branch element. That is why the loop over the numpy array is copied for each use case instead
# of making a more generalized function or checking the different use cases inside the loop
if calc_branch_impedances:
parameter[:, BR_Z] = np.sqrt(parameter[:, BR_R] ** 2 + parameter[:, BR_X] ** 2)
if branch_impedance_unit == "ohm":
for idx, p in zip(indices[in_service], parameter[in_service]):
mg.add_edge(idx[F_BUS], idx[T_BUS], key=(element, idx[INDEX]), weight=p[WEIGHT],
path=1, r_ohm=p[BR_R], x_ohm=p[BR_X], z_ohm=p[BR_Z])
elif branch_impedance_unit == "pu":
for idx, p in zip(indices[in_service], parameter[in_service]):
mg.add_edge(idx[F_BUS], idx[T_BUS], key=(element, idx[INDEX]), weight=p[WEIGHT],
path=1, r_pu=p[BR_R], x_pu=p[BR_X], z_pu=p[BR_Z])
else:
for idx, p in zip(indices[in_service], parameter[in_service]):
mg.add_edge(idx[F_BUS], idx[T_BUS], key=(element, idx[INDEX]), weight=p[WEIGHT],
path=1)
def get_baseR(net, ppc, buses):
bus_lookup = net._pd2ppc_lookups["bus"]
base_kv = ppc["bus"][bus_lookup[buses], BASE_KV]
return np.square(base_kv) / net.sn_mva
def init_par(tab, calc_branch_impedances=False):
n = tab.shape[0]
indices = np.zeros((n, 3), dtype=np.int)
indices[:, INDEX] = tab.index
if calc_branch_impedances:
parameters = np.zeros((n, 4), dtype=np.float)
else:
parameters = np.zeros((n, 1), dtype=np.float)
if "in_service" in tab:
return indices, parameters, tab.in_service.values.copy()
else:
return indices, parameters
def get_nx_ppc(net):
_init_nx_options(net)
ppc = _init_ppc(net)
_build_bus_ppc(net, ppc)
return ppc