How NetworkModel Works


In [1]:
import pandas as pd
import numpy as np

from psst.network.graph import (
    NetworkModel, NetworkViewBase, NetworkView
)

from psst.case import read_matpower
case = read_matpower('../cases/case118.m')

I. Creating a NetworkModel


In [2]:
# Create the model from a PSSTCase, optionally passing a sel_bus
m = NetworkModel(case, sel_bus='Bus1')

In the __init__, the NetworkModel...


In [3]:
display(m.case)     # saves the case
display(m.network)  # creates a PSSTNetwork
display(m.G)        # stores the networkX graph (an attribute of the PSSTNetwork)
display(m.model)    # builds/solves the model


<psst.case.PSSTCase(name=case118, Generators=54, Buses=118, Branches=186)>
<psst.network.PSSTNetwork(nodes=290, edges=351)>
<networkx.classes.graph.Graph at 0x7f223c0ec630>
<psst.model.PSSTModel(status=solved)>

In [4]:
# Creates df of x,y positions for each node (bus, load, gen), based off self.network.positions
m.all_pos.head(n=10)


Out[4]:
x y
Bus1 1256.40 309.10
Bus10 1160.10 125.97
Bus100 302.49 229.74
Bus101 264.54 249.43
Bus102 244.91 278.67
Bus103 223.94 180.12
Bus104 254.56 190.97
Bus105 239.27 146.20
Bus106 278.94 166.79
Bus107 266.64 122.28

In [5]:
# Creates a df of start and end x,y positions for each edge, based off self.G.edges()
m.all_edges.head(n=10)


Out[5]:
start_x end_x start_y end_y
start end
Bus1 GenCo0 1256.4 1293.9 309.10 304.33
Bus2 1256.4 1224.4 309.10 320.97
Bus3 1256.4 1208.9 309.10 284.35
Load_Bus1 1256.4 1288.0 309.10 329.57
Bus2 Bus12 1224.4 1171.4 320.97 301.64
Load_Bus2 1224.4 1251.0 320.97 346.28
Bus3 Bus5 1208.9 1164.1 284.35 239.66
Bus12 1208.9 1171.4 284.35 301.64
Load_Bus3 1208.9 1242.1 284.35 286.85
Bus4 GenCo1 1187.6 1200.6 226.31 190.94

The sel_bus and view_buses attributes


In [6]:
# `sel_bus` is a single bus, upon which the visualization is initially centered.
# It can be changed programatically, or via the dropdown menu.

m.sel_bus


Out[6]:
'Bus1'

In [7]:
# At first, it is the only bus in view_buses.
# More buses get added to view_buses as they are clicked.

m.view_buses


Out[7]:
['Bus1']

II. Creating a NetworkView from the model


In [8]:
# Create the view from the model
# (It can, alternatively, be created from a case.)

v = NetworkView(model=m)

In [9]:
v


III. Generating the x,y data for the view

  • Whenever the view_buses list get changed, it triggers the callback _callback_view_change
    • This function first calls subset_positions and subset_edges
    • Then, the subsetted DataFrames get segregated into seperate ones for bus, gen, and load
    • Finally, the x,y coordinates are extracted into a format the NetworkView can use.

In [10]:
# The subsetting that occurs is all based on `view_buses`
m.view_buses


Out[10]:
['Bus1']

The subset_positions() call


In [11]:
# Subset positions creates self.pos
m.pos


Out[11]:
x y
GenCo0 1293.9 304.33
Bus2 1224.4 320.97
Bus3 1208.9 284.35
Bus1 1256.4 309.10
Load_Bus1 1288.0 329.57

The function looks like this:

def subset_positions(self):
    """Subset self.all_pos to include only nodes adjacent to those in view_buses list."""
    nodes = [list(self.G.adj[item].keys()) for item in self.view_buses]  # get list of nodes adj to selected buses
    nodes = set(itertools.chain.from_iterable(nodes))  # chain lists together, eliminate duplicates w/ set
    nodes.update(self.view_buses)  # Add the view_buses themselves to the set
    return self.all_pos.loc[nodes]  # Subset df of all positions to include only desired nodes.

The subset_edges() call


In [12]:
# Subset edges creates self.edges
m.edges


Out[12]:
start_x end_x start_y end_y
start end
Bus1 GenCo0 1256.4 1293.9 309.1 304.33
Bus2 1256.4 1224.4 309.1 320.97
Bus3 1256.4 1208.9 309.1 284.35
Load_Bus1 1256.4 1288.0 309.1 329.57

The function looks like this:

def subset_edges(self):
    """Subset all_edges, with G.edges() info, based on view_buses list."""
    edge_list = self.G.edges(nbunch=self.view_buses)  # get edges of view_buses as list of tuples
    edges_fwd = self.all_edges.loc[edge_list]  # query all_pos with edge_list
    edge_list_rev = [tuple(reversed(tup)) for tup in edge_list]  # reverse order of each tuple
    edges_rev = self.all_edges.loc[edge_list_rev]  # query all_pos again, with reversed edge_list
    edges = edges_fwd.append(edges_rev).dropna(subset=['start_x'])  # combine results, dropping false hits
    return edges

If you want a closer look...


In [13]:
m.view_buses = ['Bus2','Bus3']

In [14]:
edge_list = m.G.edges(nbunch=m.view_buses)  # get edges of view_buses as list of tuples
edge_list


Out[14]:
[('Bus2', 'Bus1'),
 ('Bus2', 'Bus12'),
 ('Bus2', 'Load_Bus2'),
 ('Bus3', 'Bus1'),
 ('Bus3', 'Bus5'),
 ('Bus3', 'Bus12'),
 ('Bus3', 'Load_Bus3')]

In [15]:
edges_fwd = m.all_edges.loc[edge_list]  # query all_pos with edge_list
edges_fwd


Out[15]:
start_x end_x start_y end_y
start end
Bus2 Bus1 NaN NaN NaN NaN
Bus12 1224.4 1171.4 320.97 301.64
Load_Bus2 1224.4 1251.0 320.97 346.28
Bus3 Bus1 NaN NaN NaN NaN
Bus5 1208.9 1164.1 284.35 239.66
Bus12 1208.9 1171.4 284.35 301.64
Load_Bus3 1208.9 1242.1 284.35 286.85

In [16]:
edge_list_rev = [tuple(reversed(tup)) for tup in edge_list]  # reverse order of each tuple
edge_list_rev


Out[16]:
[('Bus1', 'Bus2'),
 ('Bus12', 'Bus2'),
 ('Load_Bus2', 'Bus2'),
 ('Bus1', 'Bus3'),
 ('Bus5', 'Bus3'),
 ('Bus12', 'Bus3'),
 ('Load_Bus3', 'Bus3')]

In [17]:
edges_rev = m.all_edges.loc[edge_list_rev]  # query all_pos again, with reversed edge_list
edges_rev


Out[17]:
start_x end_x start_y end_y
start end
Bus1 Bus2 1256.4 1224.4 309.1 320.97
Bus12 Bus2 NaN NaN NaN NaN
Load_Bus2 Bus2 NaN NaN NaN NaN
Bus1 Bus3 1256.4 1208.9 309.1 284.35
Bus5 Bus3 NaN NaN NaN NaN
Bus12 Bus3 NaN NaN NaN NaN
Load_Bus3 Bus3 NaN NaN NaN NaN

In [18]:
edges = edges_fwd.append(edges_rev).dropna(subset=['start_x'])  # combine results, dropping false hits
edges


Out[18]:
start_x end_x start_y end_y
start end
Bus2 Bus12 1224.4 1171.4 320.97 301.64
Load_Bus2 1224.4 1251.0 320.97 346.28
Bus3 Bus5 1208.9 1164.1 284.35 239.66
Bus12 1208.9 1171.4 284.35 301.64
Load_Bus3 1208.9 1242.1 284.35 286.85
Bus1 Bus2 1256.4 1224.4 309.10 320.97
Bus3 1256.4 1208.9 309.10 284.35

Segregating DataFrames and extracting data

  • The DataFrames are segregated into bus, case, and load, using the names in case.bus, case.gen, and case.load
  • x,y data is extracted, ready to be plotted by NetworkView

Extracting bus data looks like this:

bus_pos = self.pos[self.pos.index.isin(self.case.bus_name)]
self.bus_x_vals = bus_pos['x']
self.bus_y_vals = bus_pos['y']
self.bus_names = list(bus_pos.index)

(Similar for the other nodes)


In [19]:
print("x_vals: ", m.bus_x_vals)
print("y_vals: ", m.bus_y_vals)
print("names:  ", m.bus_names)


x_vals:  [ 1171.4  1224.4  1208.9  1164.1  1256.4]
y_vals:  [ 301.64  320.97  284.35  239.66  309.1 ]
names:   ['Bus12', 'Bus2', 'Bus3', 'Bus5', 'Bus1']

Extracting branch data looks like this:

edges = self.edges.reset_index()

_df = edges.loc[edges.start.isin(self.case.bus_name) & edges.end.isin(self.case.bus_name)]
self.bus_x_edges = [tuple(edge) for edge in _df[['start_x', 'end_x']].values]
self.bus_y_edges = [tuple(edge) for edge in _df[['start_y', 'end_y']].values]

(Similar for the other edges)


In [20]:
print("bus_x_edges:")
print(m.bus_x_edges)

print("\nbus_y_edges:")
print(m.bus_y_edges)


bus_x_edges:
[[ 1224.4  1171.4]
 [ 1208.9  1164.1]
 [ 1208.9  1171.4]
 [ 1256.4  1224.4]
 [ 1256.4  1208.9]]

bus_y_edges:
[[ 320.97  301.64]
 [ 284.35  239.66]
 [ 284.35  301.64]
 [ 309.1   320.97]
 [ 309.1   284.35]]