e2nIEE / pandapower
1
# -*- coding: utf-8 -*-
2

3
# Copyright (c) 2016-2021 by University of Kassel and Fraunhofer Institute for Energy Economics
4
# and Energy System Technology (IEE), Kassel. All rights reserved.
5

6

7 1
import networkx as nx
8 1
import pandas as pd
9 1
from collections import deque
10 1
from itertools import combinations
11

12 1
from pandapower.topology.create_graph import create_nxgraph
13

14

15 1
def connected_component(mg, bus, notravbuses=[]):
16
    """
17
    Finds all buses in a NetworkX graph that are connected to a certain bus.
18

19
    INPUT:
20
        **mg** (NetworkX graph) - NetworkX Graph or MultiGraph that represents a pandapower network.
21

22
        **bus** (integer) - Index of the bus at which the search for connected components originates
23

24

25
    OPTIONAL:
26
     **notravbuses** (list/set) - indices of notravbuses: lines connected to these buses are
27
                                     not being considered in the graph
28

29
    OUTPUT:
30
        **cc** (generator) - Returns a generator that yields all buses connected to the input bus
31

32
    EXAMPLE:
33
         import pandapower.topology as top
34

35
         mg = top.create_nxgraph(net)
36

37
         cc = top.connected_component(mg, 5)
38

39
    """
40 1
    yield bus
41 1
    visited = {bus}
42 1
    stack = deque([iter(mg[bus])])
43 1
    while stack:
44 1
        for child in stack.pop():
45 1
            if child not in visited:
46 1
                yield child
47 1
                visited.add(child)
48 1
                if child not in notravbuses:
49 1
                    stack.append(iter(mg[child]))
50

51

52 1
def connected_components(mg, notravbuses=set()):
53
    """
54
     Clusters all buses in a NetworkX graph that are connected to each other.
55

56
     INPUT:
57
        **mg** (NetworkX graph) - NetworkX Graph or MultiGraph that represents a pandapower network.
58

59

60
     OPTIONAL:
61
     **notravbuses** (set) - Indices of notravbuses: lines connected to these buses are
62
                                       not being considered in the graph
63

64
     OUTPUT:
65
        **cc** (generator) - Returns a generator that yields all clusters of buses connected
66
                             to each other.
67

68
     EXAMPLE:
69
         import pandapower.topology as top
70

71
         mg = top.create_nxgraph(net)
72

73
         cc = top.connected_components(net, 5)
74

75
    """
76

77 1
    nodes = set(mg.nodes()) - notravbuses
78 1
    while nodes:
79 1
        cc = set(connected_component(mg, nodes.pop(), notravbuses=notravbuses))
80 1
        yield cc
81 1
        nodes -= cc
82
    # the above does not work if two notravbuses are directly connected
83 1
    if len(notravbuses) > 0:
84 1
        for f, t in mg.edges(notravbuses):
85 1
            if f in notravbuses and t in notravbuses:
86 0
                yield set([f, t])
87

88

89 1
def calc_distance_to_bus(net, bus, respect_switches=True, nogobuses=None,
90
                         notravbuses=None, weight='weight'):
91
    """
92
        Calculates the shortest distance between a source bus and all buses connected to it.
93

94
     INPUT:
95
        **net** (pandapowerNet) - Variable that contains a pandapower network.
96

97
        **bus** (integer) - Index of the source bus.
98

99

100
     OPTIONAL:
101
        **respect_switches** (boolean, True) - True: open line switches are being considered
102
                                                     (no edge between nodes)
103
                                               False: open line switches are being ignored
104

105
        **nogobuses** (integer/list, None) - nogobuses are not being considered
106

107
        **notravbuses** (integer/list, None) - lines connected to these buses are not being
108
                                              considered
109
        **weight** (string, None) – Edge data key corresponding to the edge weight
110

111
     OUTPUT:
112
        **dist** - Returns a pandas series with containing all distances to the source bus
113
                   in km. If weight=None dist is the topological distance (int).
114

115
     EXAMPLE:
116
         import pandapower.topology as top
117

118
         dist = top.calc_distance_to_bus(net, 5)
119

120
    """
121 1
    g = create_nxgraph(net, respect_switches=respect_switches,
122
                       nogobuses=nogobuses, notravbuses=notravbuses)
123 1
    return pd.Series(nx.single_source_dijkstra_path_length(g, bus, weight=weight))
124

125

126 1
def unsupplied_buses(net, mg=None, slacks=None, respect_switches=True):
127
    """
128
     Finds buses, that are not connected to an external grid.
129

130
     INPUT:
131
        **net** (pandapowerNet) - variable that contains a pandapower network
132

133
     OPTIONAL:
134
        **mg** (NetworkX graph) - NetworkX Graph or MultiGraph that represents a pandapower network.
135

136
        **in_service_only** (boolean, False) - Defines whether only in service buses should be
137
            included in unsupplied_buses.
138

139
        **slacks** (set, None) - buses which are considered as root / slack buses. If None, all
140
            existing slack buses are considered.
141

142
        **respect_switches** (boolean, True) - Fixes how to consider switches - only in case of no
143
            given mg.
144

145
     OUTPUT:
146
        **ub** (set) - unsupplied buses
147

148
     EXAMPLE:
149
         import pandapower.topology as top
150

151
         top.unsupplied_buses(net)
152
    """
153

154 1
    mg = mg or create_nxgraph(net, respect_switches=respect_switches)
155 1
    if slacks is None:
156 1
        slacks = set(net.ext_grid[net.ext_grid.in_service].bus.values) | set(
157
            net.gen[net.gen.in_service & net.gen.slack].bus.values)
158 1
    not_supplied = set()
159 1
    for cc in nx.connected_components(mg):
160 1
        if not set(cc) & slacks:
161 1
            not_supplied.update(set(cc))
162

163 1
    return not_supplied
164

165

166 1
def find_basic_graph_characteristics(g, roots, characteristics):
167
    """
168
    Determines basic characteristics of the given graph like connected buses, stubs, bridges,
169
    and articulation points.
170

171
    .. note::
172

173
        This is the base function for find_graph_characteristics. Please use the latter
174
        function instead!
175
    """
176 1
    connected = 'connected' in characteristics
177 1
    stub_buses = 'stub_buses' in characteristics
178 1
    bridges = {'bridges', 'required_bridges'} & set(characteristics)
179 1
    articulation_points = {'articulation_points', 'notn1_areas'} & set(characteristics)
180 1
    notn1_starts = 'notn1_areas' in characteristics
181

182 1
    char_dict = {'connected': set(), 'stub_buses': set(), 'bridges': set(),
183
                 'articulation_points': set(), 'notn1_starts': set()}
184

185 1
    discovery = {root: 0 for root in roots}  # "time" of first discovery of node during search
186 1
    low = {root: 0 for root in roots}
187 1
    visited = set(roots)
188 1
    path = []
189 1
    stack = [(root, root, iter(g[root])) for root in roots]
190 1
    while stack:
191 1
        grandparent, parent, children = stack[-1]
192 1
        try:
193 1
            child = next(children)
194 1
            if stub_buses:
195 1
                if child not in visited:
196 1
                    path.append(child)  # keep track of movement through the graph
197 1
            if grandparent == child:
198 1
                continue
199 1
            if child in visited:
200 1
                if discovery[child] <= discovery[parent]:  # back edge
201 1
                    low[parent] = min(low[parent], discovery[child])
202
            else:
203 1
                low[child] = discovery[child] = len(discovery)
204 1
                visited.add(child)
205 1
                stack.append((parent, child, iter(g[child])))
206 1
        except StopIteration:
207 1
            back = stack.pop()
208 1
            path.append(back[0])
209 1
            if low[parent] >= discovery[grandparent]:
210
                # Articulation points and start of not n-1 safe buses
211 1
                if grandparent not in roots:
212 1
                    if articulation_points:
213 1
                        char_dict['articulation_points'].add(grandparent)
214 1
                    if notn1_starts:
215 1
                        char_dict['notn1_starts'].add(parent)
216 1
                if low[parent] > discovery[grandparent]:
217
                    # Bridges
218 1
                    if bridges:
219 1
                        char_dict['bridges'].add((grandparent, parent))
220

221
                    # Stub buses
222 1
                    if stub_buses:
223 1
                        stub = path.pop()
224 1
                        if stub != grandparent:
225 0
                            char_dict['stub_buses'].add(stub)
226 1
                        while path and path[-1] != grandparent and path[-1] not in roots:
227 1
                            stub = path.pop()
228 1
                            char_dict['stub_buses'].add(stub)
229 1
            low[grandparent] = min(low[parent], low[grandparent])
230

231 1
    if connected:
232 1
        char_dict['connected'] = visited
233 1
    return char_dict
234

235

236 1
def find_graph_characteristics(g, roots, characteristics):
237
    """
238
    Finds and returns different characteristics of the given graph which can be specified.
239

240
    INPUT:
241
        **g** (NetworkX graph) - Graph of the network
242

243
        **roots** (list) - Root buses of the graphsearch
244

245
        **characteristics** (list) - List of characteristics this function determines and returns
246

247
        .. note::
248

249
            Possible characteristics:
250

251
            - 'connected' - All buses which have a connection to at least one of the root buses
252
            - 'articulation_points' - Buses which lead to disconnected areas if they get removed
253
            - 'bridges' - Edges which lead to disconnected areas if they get removed
254
            - 'stub_buses' - Buses which arent't connected if one specific edge gets removed
255
            - 'required_bridges' - Bridges which are strictly needed to connect a specific bus
256
            - 'notn1_areas' - Areas which aren't connected if one specific bus gets removed
257

258
    OUTPUT:
259

260
        **char_dict** (dict) - dictionary which contains the wanted characteristics
261

262
        ======================= ================================================================
263
        key                     dict value
264
        ======================= ================================================================
265
        'connected'             set of all connected buses
266
        'articulation_points'   set of all articulation points
267
        'bridges'               set of tuples which represent start and end bus of each bridge
268
        'stub_buses'            set of all buses which lie on a stub
269
        'required_bridges'      dict of all buses which are connected via at least one bridge.
270
                                The dict values contain a set of bridges which are needed to
271
                                connect the key buses
272
        'notn1_areas'           dict of not n-1 safe areas. The dict values contain a set of
273
                                not n-1 safe buses which aren't connected if the key bus gets
274
                                removed
275
        ======================= ================================================================
276

277
    EXAMPLE::
278

279
        import topology as top
280
        g = top.create_nxgraph(net, respect_switches=False)
281
        char_dict = top.find_graph_characteristics(g, roots=[0, 3], characteristics=['connected', 'stub_buses'])
282
    """
283 1
    char_dict = find_basic_graph_characteristics(g, roots, characteristics)
284

285 1
    required_bridges = 'required_bridges' in characteristics
286 1
    notn1_areas = 'notn1_areas' in characteristics
287

288 1
    if not required_bridges and not notn1_areas:
289 1
        return {key: char_dict[key] for key in characteristics}
290

291 1
    char_dict.update({'required_bridges': dict(), 'notn1_areas': dict()})
292

293 1
    visited = set(roots)
294 1
    visited_bridges = []
295 1
    notn1_area_start = None
296 1
    curr_notn1_area = []
297

298 1
    stack = [(root, root, iter(g[root])) for root in roots]
299 1
    while stack:
300 1
        grandparent, parent, children = stack[-1]
301 1
        try:
302 1
            child = next(children)
303 1
            if child == grandparent:
304 1
                continue
305 1
            if child not in visited:
306 1
                visited.add(child)
307 1
                stack.append((parent, child, iter(g[child])))
308 1
                if required_bridges and ((parent, child) in char_dict['bridges'] or
309
                                         (child, parent) in char_dict['bridges']):
310 1
                    visited_bridges.append((parent, child))
311

312 1
                if notn1_areas:
313 1
                    if child in char_dict['notn1_starts'] and not notn1_area_start:
314 1
                        notn1_area_start = parent
315 1
                    if notn1_area_start:
316 1
                        curr_notn1_area.append(child)
317

318 1
        except StopIteration:
319 1
            stack.pop()
320 1
            if required_bridges:
321 1
                if len(visited_bridges) > 0:
322 1
                    char_dict['required_bridges'][parent] = visited_bridges[:]
323 1
                if ((parent, grandparent) in char_dict['bridges'] or
324
                    (grandparent, parent) in char_dict['bridges']):
325 1
                    visited_bridges.pop()
326

327 1
            if notn1_areas and grandparent == notn1_area_start:
328 1
                if grandparent in char_dict["notn1_areas"]:
329 0
                    char_dict["notn1_areas"][grandparent].update(set(curr_notn1_area[:]))
330
                else:
331 1
                    char_dict["notn1_areas"][grandparent] = set(curr_notn1_area[:])
332 1
                del curr_notn1_area[:]
333 1
                notn1_area_start = None
334

335 1
    return {key: char_dict[key] for key in characteristics}
336

337

338 1
def get_2connected_buses(g, roots):
339
    """
340
    Get all buses which have at least two connections to the roots
341

342
    INPUT:
343
        **g** (NetworkX graph) - NetworkX Graph or MultiGraph that represents a pandapower network
344

345
        **roots** - Roots of the graphsearch
346
    """
347 1
    char_dict = find_graph_characteristics(g, roots, characteristics=['connected', 'stub_buses'])
348 1
    connected, stub_buses = char_dict['connected'], char_dict['stub_buses']
349 1
    two_connected = connected - stub_buses
350 1
    return connected, two_connected
351

352

353 1
def determine_stubs(net, roots=None, mg=None, respect_switches=False):
354
    """
355
     Finds stubs in a network. Open switches are being ignored. Results are being written in a new
356
     column in the bus table ("on_stub") and line table ("is_stub") as True/False value.
357

358

359
     INPUT:
360
        **net** (pandapowerNet) - Variable that contains a pandapower network.
361

362
     OPTIONAL:
363
        **roots** (integer/list, None) - indices of buses that should be excluded (by default, the
364
                                         ext_grid buses will be set as roots)
365

366
     EXAMPLE:
367
         import pandapower.topology as top
368

369
         top.determine_stubs(net, roots = [0, 1])
370

371

372
    """
373 1
    if mg is None:
374 1
        mg = create_nxgraph(net, respect_switches=respect_switches)
375
    # remove buses with degree lower 2 until none left
376 1
    if roots is None:
377 1
        roots = set(net.ext_grid.bus)
378
    #    mg.add_edges_from((a, b) for a, b in zip(list(roots)[:-1], list(roots)[1:]))
379
    #    while True:
380
    #        dgo = {g for g, d in list(mg.degree().items()) if d < 2} #- roots
381
    #        if not dgo:
382
    #            break
383
    #        mg.remove_nodes_from(dgo)
384
    #    n1_buses = mg.nodes()
385 1
    _, n1_buses = get_2connected_buses(mg, roots)
386 1
    net.bus["on_stub"] = True
387 1
    net.bus.loc[n1_buses, "on_stub"] = False
388 1
    net.line["is_stub"] = ~((net.line.from_bus.isin(n1_buses)) & (net.line.to_bus.isin(n1_buses)))
389 1
    stubs = set(net.bus.index) - set(n1_buses)
390 1
    return stubs
391

392

393 1
def lines_on_path(mg, path):
394
    """
395
     Finds all lines that connect a given path of buses.
396

397
     INPUT:
398
        **mg** (NetworkX graph) - NetworkX Graph or MultiGraph that represents a pandapower network.
399

400
        **path** (list) - List of connected buses.
401

402
     OUTPUT:
403
        **lines** (list) - Returns a list of all lines on the path.
404

405
     EXAMPLE:
406
         import topology as top
407

408
         mg = top.create_nxgraph(net)
409
         lines = top.lines_on_path(mg, [4, 5, 6])
410

411
     """
412

413 1
    return elements_on_path(mg, path, "line")
414

415

416 1
def elements_on_path(mg, path, element="line"):
417
    """
418
     Finds all elements that connect a given path of buses.
419

420
     INPUT:
421
        **mg** (NetworkX graph) - NetworkX Graph or MultiGraph that represents a pandapower network.
422

423
        **path** (list) - List of connected buses.
424

425
        **element** (string, "l") - element type
426

427
        **multi** (boolean, True) - True: Applied on a NetworkX MultiGraph
428
                                    False: Applied on a NetworkX Graph
429

430
     OUTPUT:
431
        **elements** (list) - Returns a list of all lines on the path.
432

433
     EXAMPLE:
434
         import topology as top
435

436
         mg = top.create_nxgraph(net)
437
         elements = top.elements_on_path(mg, [4, 5, 6])
438

439
     """
440 1
    if element not in ["line", "switch", "trafo", "trafo3w"]:
441 1
        raise ValueError("Invalid element type %s"%element)
442 1
    if isinstance(mg, nx.MultiGraph):
443 1
        return [edge[1] for b1, b2 in zip(path, path[1:]) for edge in mg.get_edge_data(b1, b2).keys()
444
                if edge[0]==element]
445
    else:
446 1
        return [mg.get_edge_data(b1, b2)["key"][1] for b1, b2 in zip(path, path[1:])
447
                if mg.get_edge_data(b1, b2)["key"][0]==element]
448

449

450 1
def get_end_points_of_continuously_connected_lines(net, lines):
451 1
    mg = nx.MultiGraph()
452 1
    line_buses = net.line.loc[lines, ["from_bus", "to_bus"]].values
453 1
    mg.add_edges_from(line_buses)
454 1
    switch_buses = net.switch[["bus", "element"]].values[net.switch.et.values=="b"]
455 1
    mg.add_edges_from(switch_buses)
456

457 1
    all_buses = set(line_buses.flatten())
458 1
    longest_path = []
459 1
    for b1, b2 in combinations(all_buses, 2):
460 1
        try:
461 1
            path = nx.shortest_path(mg, b1, b2)
462 1
        except nx.NetworkXNoPath:
463 1
            raise UserWarning("Lines not continuously connected")
464 1
        if len(path) > len(longest_path):
465 1
            longest_path = path
466 1
    if all_buses - set(longest_path):
467 1
        raise UserWarning("Lines have branching points")
468 1
    return longest_path[0], longest_path[-1]

Read our documentation on viewing source code .

Loading