Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

adding LODF and GLODF code #449

Open
wants to merge 2 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions andes/routines/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
all_routines = OrderedDict([('pflow', ['PFlow']),
('tds', ['TDS']),
('eig', ['EIG']),
('glodf', ['GLODF']),
])

class_names = list_flatten(list(all_routines.values()))
Expand Down
242 changes: 242 additions & 0 deletions andes/routines/glodf.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,242 @@
"""
Generalized Line Outage Distribution Factors (GLODF)

The GLODFs are useful for analyzing the impact of the failure of multiple
transmission lines on the power flow of the entire system. The GLODFs are a set
of coefficients that quantify the proportional change in power flows on all
other lines due to a change in power flow on a specific line. They are
computed using the power transfer distribution factors (PTDFs), which describe
the impact of a change in power flow on a specific generator or load on the
power flows of all other generators or loads.
"""

import andes
import pandas as pd
import numpy as np

class GLODF:
"""
Computes the Global Line Outage Distribution Factors (GLODF) for a power system.

Parameters
system : The power system model.
"""
def __init__(self, system):
self.system = system
self.Bs = self.get_branch_susceptance()
self.Ar = self.get_reduced_incidence()
self.Bn = self.get_reduced_nodal_susceptance()

def get_branch_susceptance(self):
"""
Returns the branch susceptance matrix.

Returns
-------
numpy.ndarray : Bs
The L x L diagonal matrix of branch susceptances.
"""
return np.diag(1/self.system.Line.x.v)

def get_reduced_incidence(self):
"""
Returns the reduced incidence matrix.

Returns
-------
numpy.ndarray : Ar
The L x (N-s) reduced incidence matrix where 's' is the number of
slack buses.
"""
num_line = self.system.Line.n
num_bus = self.system.Bus.n

A = np.zeros((num_line, num_bus))

for l in range(num_line):
A[l,self.system.Line.bus1.v[l]-1] = -1
A[l,self.system.Line.bus2.v[l]-1] = 1

# delete all slack rows as required
Ar = np.delete(A, np.asarray(self.system.Bus.idx2uid(self.system.Slack.bus.v)), axis=1)
return Ar

def get_reduced_nodal_susceptance(self):
"""
Returns the reduced nodal susceptance matrix.

Returns
-------
numpy.ndarray : Bn
The (N-s) x (N-s) reduced nodal susceptance matrix where 's' is the
number of slack buses.
"""
return -1.0 * np.dot(np.dot(self.Ar.T, self.Bs), self.Ar)

def get_isf(self):
"""
Returns the injection shift factor (ISF) matrix.

Returns
-------
numpy.ndarray : isf
The L x (N-s) injection shift factor matrix.
"""
psi = np.dot(np.dot(self.Bs, self.Ar), np.linalg.inv(self.Bn))

return psi

def get_ptdf(self, change_lines):
"""
Returns the power transfer distribution factors for the given lines.

Parameters
----------
change_lines : line indeces
List of line indices for which the GLODFs are to be computed.

Returns
-------
numpy.ndarray : ptdf
The L x (lines) matrix.
"""
change_lines = np.atleast_1d(change_lines)
psi = self.get_isf()

slack = np.array(self.system.Bus.idx2uid(self.system.Slack.bus.v))
non_slack = np.delete(np.arange(self.system.Line.n), slack)

bfrom = self.system.Bus.idx2uid(self.system.Line.get('bus1', change_lines))
bto = self.system.Bus.idx2uid(self.system.Line.get('bus2', change_lines))

bfrom_idx = np.zeros_like(bfrom)
bto_idx = np.zeros_like(bto)

for i in range(np.size(change_lines)):
if (bfrom[i] in slack):
bfrom_idx[i] = -1
else:
bfrom_idx[i] = np.argwhere(non_slack == bfrom[i])

if (bto[i] in slack):
bto_idx[i] = -1
else:
bto_idx[i] = np.argwhere(non_slack == bto[i])

# zeros row is needed because power injection at slack bus should yield psi = 0
zeros_row = np.zeros((psi.shape[0], 1))
psi_zero = np.hstack((psi, zeros_row))
phi = psi_zero[:,bfrom_idx] - psi_zero[:,bto_idx]
return phi

def lodf(self, change_line):
"""
Returns the line outage distribution factors (LODFs) matrix for single
line outage

Parameters
----------
change_line : line index
line indix for which the LODFs are to be computed.

Returns
-------
numpy.ndarray : glodf
The 1 x L injection shift factor matrix, where 'o' is the number of
outages.
"""
phi = self.get_ptdf(change_line)

uid_lines = self.system.Line.idx2uid(change_line)

sigma = phi / (1 - phi[uid_lines])
sigma[uid_lines] = 0

return sigma #[np.arange(sigma.size) != change_line - 1]

def flow_after_lodf(self, change_line):
"""
Returns the line flows after the line outage

Parameters
----------
change_line : line index
line indix for which the LODFs are to be computed.

Returns
-------
numpy.ndarray : flow_after
The length L vector of line flows after the outage
"""
sigma = np.squeeze(self.lodf(change_line))

uid_lines = self.system.Line.idx2uid(change_line)

flow_before = self.system.Line.a1.e

flow_after = flow_before + flow_before[uid_lines] * sigma
flow_after[uid_lines] = 0

return flow_after

def glodf(self, change_lines):
"""
Returns the generalized line outage distribution factors (GLODFs)
matrix for the line outages in the change_lines list.

Parameters
----------
change_lines : line indices
List of line indices for which the GLODFs are to be computed.

Returns
-------
numpy.ndarray : glodf
The o x L injection shift factor matrix, where 'o' is the number of
outages.
"""
uid_lines = np.atleast_1d(self.system.Line.idx2uid(change_lines))

phi = self.get_ptdf(change_lines)

# right side of equation is all lines
right_side = phi.T

# left side is identity - Phi of change lines
Phi = right_side[:,uid_lines]
left_side = (np.eye(np.shape(Phi)[0]) - Phi)

xi = np.linalg.solve(left_side, right_side)

return xi

def flow_after_glodf(self, change_lines):
"""
Returns the line flows after the line outages given in change_lines

Parameters
----------
change_lines : line indices
List of line indices for which the GLODFs are to be computed.

Returns
-------
numpy.ndarray : flow_after
The length L vector of line flows after the outages
"""
xi = self.glodf(change_lines)

uid_lines = np.atleast_1d(self.system.Line.idx2uid(change_lines))

flow_before = self.system.Line.a1.e


#GLODFs times flow before
delta_flow = xi.T @ flow_before[uid_lines]
flow_after = flow_before + delta_flow

# ensure lines that are out have no flow
flow_after[uid_lines] = 0

return flow_after

60 changes: 60 additions & 0 deletions tests/test_glodf.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
"""
GLODF Test Code

Test to ensure IEEE 14 bus system has less than 5% error with the following
line outages:
['Line_1', 'Line_3', 'Line_5', 'Line_7', 'Line_9',]
"""

from andes.routines.glodf import GLODF
import andes
import numpy as np
import pandas as pd

def test_flow_after_glodf():
"""
Example code to test the GLODF class
"""
print("GLODF Test")
# load system
ss = andes.load(andes.get_case("ieee14/ieee14_linetrip.xlsx"))

# solve system
ss.PFlow.run()

# create GLODF object
g = GLODF(ss)

# lines to be taken out
change_lines = ['Line_1', 'Line_3', 'Line_5', 'Line_7', 'Line_9',]

lines_before = np.copy(ss.Line.a1.e)
lines_after = g.flow_after_glodf(change_lines)

np.set_printoptions(precision=5)

uid_lines = np.atleast_1d(ss.Line.idx2uid(change_lines))
# turn off lines and resolve1
for i in range(np.size(change_lines)):
ss.Line.u.v[uid_lines] = 0
ss.PFlow.run()

lineflows = {"bus1": ss.Line.bus1.v,
"bus2": ss.Line.bus2.v,
"P1 before": lines_before,
"P1 GLODF": lines_after,
"P1 re-solved": ss.Line.a1.e,
"error": np.abs(lines_after - ss.Line.a1.e) }

df_lineflow = pd.DataFrame(lineflows, index=ss.Line.idx.v)

print(df_lineflow)

percent_error = np.mean(np.abs((ss.Line.a1.e - lines_after))) / np.mean(np.abs(ss.Line.a1.e)) * 100
print("normalized absolute percent error: {:.3f}%".format(percent_error))

# Check if lines_after is close to ss.Line.a1.e
assert np.allclose(percent_error, 0, atol=5) # error should be less than 5%

if __name__ == "__main__":
test_flow_after_glodf()