diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 2ba11297f..b22224b5b 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -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 diff --git a/pandapower/pypower/makeLODF.py b/pandapower/pypower/makeLODF.py index 65d960b9c..97b5fc93e 100644 --- a/pandapower/pypower/makeLODF.py +++ b/pandapower/pypower/makeLODF.py @@ -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 + + + + diff --git a/pandapower/test/loadflow/test_PTDF_LODF.py b/pandapower/test/loadflow/test_PTDF_LODF.py index f0ac6e58f..7e42fa507 100644 --- a/pandapower/test/loadflow/test_PTDF_LODF.py +++ b/pandapower/test/loadflow/test_PTDF_LODF.py @@ -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 @@ -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"])