Skip to content

Commit

Permalink
Merge pull request #2191 from e2nIEE/feature/otdf
Browse files Browse the repository at this point in the history
add function to compute OTDF and evaluate outages using OTDF
  • Loading branch information
rbolgaryn authored Dec 20, 2023
2 parents 875a3cb + fa20250 commit 401d573
Show file tree
Hide file tree
Showing 3 changed files with 182 additions and 1 deletion.
2 changes: 2 additions & 0 deletions CHANGELOG.rst
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,8 @@ Change Log
- [FIXED] :code:`convert_format.py`: update the attributes of the characteristic objects to match the new characteristic
- [FIXED] additional arguments from mpc saved to net._options: create "_options" if it does not exist
- [CHANGED] cim2pp: extracted getting default classes, added generic setting datatypes from CGMES XMI schema
- [ADDED] function :code:`getOTDF` to obtain Outage Transfer Distribution Factors, that can be used to analyse outages using the DC approximation of the power system
- [ADDED] function :code:`outage_results_OTDF` to obtain the matrix of results for all outage scenarios, with rows as outage scenarios and columns as branch power flows in that scenario


[2.13.1] - 2023-05-12
Expand Down
115 changes: 115 additions & 0 deletions pandapower/pypower/makeLODF.py
Original file line number Diff line number Diff line change
Expand Up @@ -66,3 +66,118 @@ def makeLODF(branch, PTDF):
update_LODF_diag(LODF)

return LODF


def makeOTDF(PTDF, LODF, outage_branches):
"""
Compute the Outage Transfer Distribution Factors (OTDF) matrix.
This function creates the OTDF matrix that relates bus power injections
to branch flows for specified outage scenarios. It's essential that
outage branches do not lead to isolated nodes or disconnected islands
in the grid.
The grid cannot have isolated nodes or disconnected islands. Use the
pandapower.topology module to identify branches that, if outaged, would
lead to isolated nodes (determine_stubs) or islands (find_graph_characteristics).
The resulting matrix has a width equal to the number of nodes and a length
equal to the number of outage branches multiplied by the total number of branches.
The dot product of OTDF and the bus power vector in generation reference frame
(positive for generation, negative for consumption - the opposite of res_bus.p_mw)
yields an array with outage branch power flows for every outage scenario,
facilitating the analysis of all outage scenarios under a DC
power flow approximation.
Parameters
----------
PTDF : numpy.ndarray
The Power Transfer Distribution Factor matrix, defining the sensitivity
of branch flows to bus power injections.
LODF : numpy.ndarray
The Line Outage Distribution Factor matrix, describing how branch flows
are affected by outages of other branches.
outage_branches : list or numpy.ndarray
Indices of branches for which outage scenarios are to be considered.
Returns
-------
OTDF : numpy.ndarray
The Outage Transfer Distribution Factor matrix. Rows correspond to
outage scenarios, and columns correspond to branch flows.
Examples
--------
>>> H = makePTDF(baseMVA, bus, branch)
>>> LODF = makeLODF(branch, H)
>>> outage_branches = [0, 2] # Example branch indices for outage scenarios
>>> OTDF = makeOTDF(H, LODF, outage_branches)
>>> # To obtain a 2D array with the outage results:
>>> outage_results = (OTDF @ Pbus).reshape(len(outage_branches), -1)
Notes
-----
- The function assumes a DC power flow model.
- Ensure that the specified outage branches do not lead to grid
disconnection or isolated nodes.
"""
OTDF = np.vstack([PTDF + LODF[:, [i]] @ PTDF[[i], :] for i in outage_branches])
return OTDF


def outage_results_OTDF(OTDF, Pbus, outage_branches):
"""
Calculate the branch power flows for each outage scenario based on the given
Outage Transfer Distribution Factors (OTDF), bus power injections (Pbus), and
specified outage branches.
This function computes how branch flows are affected under N-1 contingency
scenarios (i.e., for each branch outage specified). It uses the OTDF matrix and
the bus power vector (Pbus) to determine the branch flows in each outage scenario.
Pbus should represent the net power at each bus in the generation reference case.
Parameters
----------
OTDF : numpy.ndarray
The Outage Transfer Distribution Factor matrix, which relates bus power
injections to branch flows under specific outage scenarios. Its shape
should be (num_outage_scenarios * num_branches, num_buses).
Pbus : numpy.ndarray
A vector representing the net power injections at each bus. Positive values
for generation, negative for consumption. Its length should be equal to
the total number of buses.
outage_branches : numpy.ndarray
An array of indices representing the branches that are outaged in each
scenario. Its length should be equal to the number of outage scenarios.
Returns
-------
numpy.ndarray
A 2D array where each row corresponds to an outage scenario and each column
represents the resulting power flow in a branch. The number of rows is equal
to the number of outage scenarios, and the number of columns is equal to the
number of branches.
Examples
--------
>>> OTDF = np.array([...]) # example OTDF matrix
>>> Pbus = np.array([...]) # example bus power vector
>>> outage_branches = np.array([...]) # example outage branches
>>> branch_flows = outage_results_OTDF(OTDF,Pbus,outage_branches)
Notes
-----
The function assumes a linear relationship between bus power injections and
branch flows, which is typical in DC power flow models.
"""
# get branch flows as an array first:
nminus1_otdf = (OTDF @ Pbus.reshape(-1, 1))
# reshape to a 2D array with rows relating to outage scenarios and columns to
# the resulting branch power flows
nminus1_otdf = nminus1_otdf.reshape(outage_branches.shape[0], -1)
return nminus1_otdf




66 changes: 65 additions & 1 deletion pandapower/test/loadflow/test_PTDF_LODF.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
import pandapower.networks as nw
from pandapower.pd2ppc import _pd2ppc
from pandapower.pypower.makePTDF import makePTDF
from pandapower.pypower.makeLODF import makeLODF
from pandapower.pypower.makeLODF import makeLODF, makeOTDF, outage_results_OTDF

from pandapower.test.loadflow.result_test_network_generator import result_test_network_generator_dcpp
from pandapower.test.helper_functions import add_grid_connection, create_test_line, assert_net_equal
Expand Down Expand Up @@ -67,5 +67,69 @@ def test_LODF():
raise AssertionError("LODF has wrong dimension")


def test_OTDF():
net = nw.case9()
mg = pp.topology.create_nxgraph(net, respect_switches=True)
# roots = np.r_[net.ext_grid.bus.values, net.gen.bus.values]
# stubs = pp.topology.determine_stubs(net, roots=roots, mg=mg, respect_switches=True) # no lines are stubs here?
# stubs = pp.toolbox.get_connected_elements(net, "line", roots) # because not n-1 lines here are those
c = pp.topology.find_graph_characteristics(g=mg, roots=net.ext_grid.bus.values, characteristics=["bridges"])
bridges = np.array([pp.topology.lines_on_path(mg, p) for p in c["bridges"]]).flatten()
# outage_lines = [i for i in net.line.index.values if i not in stubs and i not in bridges]
outage_lines = np.array([i for i in net.line.index.values if i not in bridges])
pp.rundcpp(net)
_, ppci = _pd2ppc(net)
ptdf = makePTDF(ppci["baseMVA"], ppci["bus"], ppci["branch"])
lodf = makeLODF(ppci["branch"], ptdf)
OTDF = makeOTDF(ptdf, lodf, outage_lines)
Pbus = -net.res_bus.p_mw.values # must be in generation reference frame
nminus1_otdf = (OTDF @ Pbus.reshape(-1, 1)).reshape(outage_lines.shape[0], -1)

# Test selected outages
n_lines = len(net.line)
for outage, line in enumerate(outage_lines):
otdf_outage_result = (OTDF[outage * n_lines:outage * n_lines + n_lines, :] @ Pbus)

# Run power flow for the outage scenario
net.line.at[line, "in_service"] = False
pp.rundcpp(net)
pf_outage_result = net.res_line.p_from_mw.values
net.line.at[line, "in_service"] = True

# Compare the results
assert np.allclose(otdf_outage_result, pf_outage_result, rtol=0, atol=1e-12)


def test_OTDF_outage_results():
net = nw.case9()
mg = pp.topology.create_nxgraph(net, respect_switches=True)
# roots = np.r_[net.ext_grid.bus.values, net.gen.bus.values]
# stubs = pp.topology.determine_stubs(net, roots=roots, mg=mg, respect_switches=True) # no lines are stubs here?
# stubs = pp.toolbox.get_connected_elements(net, "line", roots) # because not n-1 lines here are those
c = pp.topology.find_graph_characteristics(g=mg, roots=net.ext_grid.bus.values, characteristics=["bridges"])
bridges = np.array([pp.topology.lines_on_path(mg, p) for p in c["bridges"]]).flatten()
# outage_lines = [i for i in net.line.index.values if i not in stubs and i not in bridges]
outage_lines = np.array([i for i in net.line.index.values if i not in bridges])
pp.rundcpp(net)
_, ppci = _pd2ppc(net)
ptdf = makePTDF(ppci["baseMVA"], ppci["bus"], ppci["branch"])
lodf = makeLODF(ppci["branch"], ptdf)
OTDF = makeOTDF(ptdf, lodf, outage_lines)
Pbus = -net.res_bus.p_mw.values # must be in generation reference frame
nminus1_otdf = outage_results_OTDF(OTDF, Pbus, outage_lines)

# now obtain the outage results by performing power flow calculations:
nminus1_pf = []
for i in outage_lines:
net.line.at[i, "in_service"] = False
pp.rundcpp(net)
nminus1_pf.append(net.res_line.p_from_mw.values.copy())
net.line.at[i, "in_service"] = True

nminus1_pf = np.vstack(nminus1_pf)

assert np.allclose(nminus1_otdf, nminus1_pf, rtol=0, atol=1e-12)


if __name__ == "__main__":
pytest.main([__file__, "-xs"])

0 comments on commit 401d573

Please sign in to comment.