def get_r(pre_event_volts, Y, branch, unspecified_inds, powerflowpreVY): """ Compute r (reduced version) as described in Equations __ and (8) of the paper. """ postY = Y.copy() PowerNetwork.remove_line_from_Y(postY, branch) r = powerflow(pre_event_volts, postY) - powerflowpreVY r = r[unspecified_inds] return r
def single_FLiER_test(power_network, pmus, noise, branch_to_fail, use_filter=True, pre_event_volts = None, FLiER_type = "Jacobian", verbose=False): """ Run a FLiER test for a specific line failure. Inputs: power_network - The power network in which the test will occur. pmus - A list of bus indices with PMUs. line_to_fail - The transmission line that will fail. Outputs: score_dict - A dictionary with keys that are indices into the 'edges' list and values that are tij's. The "no line failure" case has key -1. ts_computed - The number of tij's that were computed (lines that got past the filter). """ powernet = power_network.copy() Y = powernet.Y.copy() try: if pre_event_volts is None: V = powernet.simulate() pre_event_volts = np.hstack([np.angle(V), np.abs(V)]) powernet.remove_branch(branch_to_fail) V = powernet.simulate() post_event_volts = np.hstack([np.angle(V), np.abs(V)]) except RuntimeError as e: # If the solver for the power flow equations fails to converge. if verbose: print str(e) return False, False, False if noise > 0: post_event_volts += np.random.normal(0, noise, len(post_event_volts)) # Get the matrix that projects a voltage vector onto the subspace # rendered observable by the PMUs. E = PowerNetwork.get_observable_projection_matrix(Y, pmus) # Run FLiER if FLiER_type == "Jacobian": res = FLiER(Y, powernet.branches, pre_event_volts, post_event_volts, E, powernet.buses, powernet.slack_inds, use_filter=use_filter, verbose=verbose) elif FLiER_type == "DC_Approximation": res = DC_FLiER(powernet.dc_mat, powernet.branches, pre_event_volts, post_event_volts, E, powernet.slack_inds, verbose=verbose) else: assert False score_dict, initial_score_dict, fraction_ts_computed = res return score_dict, initial_score_dict, fraction_ts_computed
def split_bus(powernet, sub, splitting_nodes): """Create a new Power_Network with a split substation. Inputs: powernet (Power_Network) - The original power network. sub (Ring_Substation) - The substation that will split. splitting_nodes (List of ints) - The substation nodes that will split from the substation. Output: Power_Network. The new power network that results from the substation splitting. """ new_buses = {bus.ID: bus.copy() for bus in powernet.buses} newbusID = max([bus.ID for bus in powernet.buses]) + 1 newbusind = len(powernet.buses) new_bus = Bus(newbusind, newbusID, voltage=sub.voltage) new_buses[newbusID] = new_bus old_bus = new_buses[sub.ID] for node in splitting_nodes: new_bus.load += sub.nodes[node].load old_bus.load -= sub.nodes[node].load new_bus.generation += sub.nodes[node].generation old_bus.generation -= sub.nodes[node].generation new_bus.shunt_adm += sub.nodes[node].shunt_adm old_bus.shunt_adm -= sub.nodes[node].shunt_adm splitting_branch_IDs = [ sub.branchID_at_node(sn) for sn in splitting_nodes if sub.branchID_at_node(sn) is not None ] new_branches = { branch.ID: branch.copy() for branch in powernet.branches.values() } for branch_ID in splitting_branch_IDs: branch = new_branches[branch_ID] assert len(branch.buses) == 2 if branch.buses[0].ID == old_bus.ID: branch.buses = (new_bus, branch.buses[1]) elif branch.buses[1].ID == old_bus.ID: branch.buses = (branch.buses[0], new_bus) else: assert False return PowerNetwork(new_buses, new_branches)
def main(args): """Run a test of FLiER. Parameters are passed to the function in pairs, where args[i] is a flag and args[i+1] is a parameter value. Valid flag, value combinations are the following: -network [filename]: A .cdf or .m file containing the network to be read in. A .cdf file is in the IEEE Common Data Format (see https://www.ee.washington.edu/research/pstca/). A .m file is in MATPOWER format. -use_filter ["True", "False"]: Whether or not to use the FLiER filtering procedure. Default is True. -pmus [int,...,int]: A list of the indices of buses on which to place PMUs. -test_type ["Full", "Single_Lines"]: If "Full", perform a test of a substation splitting into two nodes. If "Single_Lines", perform a line failure test. Default is "Full". -test_scenarios [**]: A list of the scenarios to test. -noise [double]: Add noise to pre- and post-event voltage readings. Value specifies standard deviation of noise. Default is 0.0. -write_file [filename]: Where to write the output data. Default is "out.txt". -verbose ["True", "False"]: Turn verbose output to the command-line on or off. Default is "False". **test_scenarios format: A semicolon-separated list in which each item is a scenario. Each item is a substation index, a comma, and then a space-separated list of substation nodes that split from the substation. For example, "86,1 2 3;176,1 2;539,7;702,4 5". """ print "Running FLiER test with the following options:" print "\n".join([ "{0}: {1}".format(args[i], args[i + 1]) for i in range(1, len(args), 2) ]) out = read_inputs(args) (filename, pmus, write_filename, test_type, noise, test_scenarios, use_filter, verbose) = out if filename.endswith('.m'): powernet = PowerNetwork.powernet_from_matpower(filename) else: powernet = PowerNetwork.powernet_from_IEEECDF(filename) # Simulate the pre-event network voltages. V = powernet.simulate() pre_event_volts = np.hstack([np.angle(V), np.abs(V)]) curr_FLiER = FLiER_Substation(powernet, pre_event_volts, pmus, verbose=verbose) solution_ranks = [] filter_ranks = [] num_tests = 0 num_missed = 0 file = open(write_filename, 'w') # Iterate over test cases. For each case, make the specified change to the # network and simulate the resulting voltages. Then run FLiER to attempt to # identify the change that occurred. for sub in curr_FLiER.substations: # For each substation, iterate over possible tests. The set of tests # returned by the iterator depends on test_type. for splitting_nodes in sub.node_iterator(test_type): key = (sub.index, tuple(splitting_nodes)) if test_scenarios is not None and key not in test_scenarios: continue if verbose: if test_type == 'Full': print "On bus {0}, splitting nbrs {1}".format( sub.index, splitting_nodes) else: bab = sub.bus_across_branch(splitting_nodes[0]) print "On line {0}".format((sub.index, bab.index)) try: # Make change according to test scenario, simulate results. # Some changes result in simulation nonconvergence. In this # case, skip the scenario. post_event_volts = get_post_event_volts( sub, splitting_nodes, powernet, curr_FLiER) except RuntimeError as e: print str(e) continue if noise > 0: # Add noise if called for. noisy_prev = pre_event_volts.copy() noisy_prev += np.random.normal(0, noise, len(noisy_prev)) noisy_postv = post_event_volts.copy() noisy_postv += np.random.normal(0, noise, len(noisy_postv)) noisy_FLiER = FLiER_Substation(powernet, noisy_prev, pmus) out = noisy_FLiER.find_topology_error(noisy_postv, test_type, use_filter) else: # Run FLiER out = curr_FLiER.find_topology_error(post_event_volts, test_type, use_filter) scores, filter_scores, fraction_ts_computed = out solkey = (sub.index, tuple(splitting_nodes)) # Solution key num_tests += 1 if scores is False: continue if solkey not in scores: if verbose: print "Solution not present" num_missed += 1 score_of_solution = np.Inf else: score_of_solution = scores[solkey] rank = np.sum(scores.values() <= score_of_solution) buses_ordered = [ y[0][0] if y[0] != -1 else -1 for y in sorted(scores.items(), key=lambda x: x[1]) ] # bus_rank is the rank of the correct answer if we only care about # getting the substation index correct and not the specific nodes # that split. bus_rank = buses_ordered.index(sub.index) + 1 if score_of_solution == np.Inf: # Bit of a hack here. Occasionally the filter removes the # correct answer entirely from the list of possibilities. In # this case we just set the rank of the solution to something # large. rank = 300 bus_rank = 300 if verbose: print "Solution score: {0}".format(score_of_solution) print "Solution rank: {0}".format(rank) print "Correct bus rank: {0}".format(bus_rank) print "Number scores not filtered: {0}".format(len(scores)) # Update scoring data structures. Write the results of this test to # the output file. solution_ranks.append(rank) sol_filter_score = filter_scores[solkey] filter_ranks.append( np.sum(filter_scores.values() <= sol_filter_score)) write_to_file(file, solkey, out, curr_FLiER.substations) file.close() print num_tests print "{0} missed entirely".format(num_missed) sorted_solranklist, sorted_filtersolranklist = zip( *sorted(zip(solution_ranks, filter_ranks), key=lambda x: x[0])) print "Mean solution rank: {0}, Mean filtered solution rank: {1}".format( np.mean(sorted_solranklist), np.mean(sorted_filtersolranklist)) print "Median sol rank: {0}, median filtered sol rank: {1}".format( np.median(sorted_solranklist), np.median(sorted_filtersolranklist)) print "Frac correct: {0}, frac filtered correct: {1}".format( np.sum(np.array(sorted_solranklist) == 1) / float(num_tests), np.sum(np.array(sorted_filtersolranklist) == 1) / float(num_tests)) print "Frac in top 3: {0}, frac filtered in top 3: {1}".format( np.sum(np.array(sorted_solranklist) <= 3) / float(num_tests), np.sum(np.array(sorted_filtersolranklist) <= 3) / float(num_tests))
def single_FLiER_test(power_network, pmus, noise, branch_to_fail, use_filter=True, pre_event_volts=None, FLiER_type="Jacobian", verbose=False): """ Run a FLiER test for a specific line failure. Inputs: power_network - The power network in which the test will occur. pmus - A list of bus indices with PMUs. line_to_fail - The transmission line that will fail. Outputs: score_dict - A dictionary with keys that are indices into the 'edges' list and values that are tij's. The "no line failure" case has key -1. ts_computed - The number of tij's that were computed (lines that got past the filter). """ powernet = power_network.copy() Y = powernet.Y.copy() try: if pre_event_volts is None: V = powernet.simulate() pre_event_volts = np.hstack([np.angle(V), np.abs(V)]) powernet.remove_branch(branch_to_fail) V = powernet.simulate() post_event_volts = np.hstack([np.angle(V), np.abs(V)]) except RuntimeError as e: # If the solver for the power flow equations fails to converge. if verbose: print str(e) return False, False, False if noise > 0: post_event_volts += np.random.normal(0, noise, len(post_event_volts)) # Get the matrix that projects a voltage vector onto the subspace # rendered observable by the PMUs. E = PowerNetwork.get_observable_projection_matrix(Y, pmus) # Run FLiER if FLiER_type == "Jacobian": res = FLiER(Y, powernet.branches, pre_event_volts, post_event_volts, E, powernet.buses, powernet.slack_inds, use_filter=use_filter, verbose=verbose) elif FLiER_type == "DC_Approximation": res = DC_FLiER(powernet.dc_mat, powernet.branches, pre_event_volts, post_event_volts, E, powernet.slack_inds, verbose=verbose) else: assert False score_dict, initial_score_dict, fraction_ts_computed = res return score_dict, initial_score_dict, fraction_ts_computed
def network_FLiER_test(network_filename, pmus, noise, use_filter=True, test_branches=None, FLiER_type="Jacobian", verbose=False): """ Simulate the failure of each line in the network one at a run, and run FLiER for each case. Inputs: network_filename - The path to the file containing the network in IEEE CDF format. pmus - A list of bus indices with PMUs on them. Outputs: """ if network_filename.endswith('.m'): powernet = PowerNetwork.powernet_from_matpower(network_filename) else: powernet = PowerNetwork.powernet_from_IEEECDF(network_filename) num_lines = len(powernet.branches) solution_ranks = [] # Contains ranks of correct answers score_list = [] # Contains tij's fraction_ts_computed_list = [] # How many tij's computed per test. dict_of_score_dicts = dict() num_filtered_out = 0 V = powernet.simulate() pre_event_volts = np.hstack([np.angle(V), np.abs(V)]) if test_branches is None: test_branches = range(len(powernet.branches)) for i in test_branches: branch = powernet.branches[i] if verbose: print "On line {0}".format([bus.index for bus in branch.buses]) out = single_FLiER_test(powernet, pmus, noise, branch_to_fail=branch, use_filter=use_filter, pre_event_volts=pre_event_volts, FLiER_type=FLiER_type, verbose=verbose) score_dict, initial_score_dict, fraction_ts_computed = out if score_dict == False: # then power flow equation solver did not converge continue fraction_ts_computed_list.append(fraction_ts_computed) if i in score_dict: score_of_solution = score_dict[i] rank = np.sum(score_dict.values() <= score_of_solution) solution_ranks.append(rank) else: # Then the correct line was filtered out. # Just choose the rank as last, and some large tij # filler value. if verbose: print "Solution filtered out." score_of_solution = 100 rank = num_lines solution_ranks.append(rank) num_filtered_out += 1 dict_of_score_dicts[i] = (score_dict, initial_score_dict, fraction_ts_computed) # Produce a list of (key, value) dictionary elements sorted by value. # Then split that list to create a sorted list of keys and a sorted list of # values. Recall that the keys are indices into the list of edges. branch_inds, scores = zip( *sorted(score_dict.iteritems(), key=lambda x: x[1])) if verbose: print score_of_solution, rank, len(score_dict) score_list.append((scores, score_of_solution)) if verbose: print "{0} filtered out entirely.".format(num_filtered_out) return score_list, dict_of_score_dicts, powernet.branches, solution_ranks, fraction_ts_computed_list
def main(args): """Run a test of FLiER. Parameters are passed to the function in pairs, where args[i] is a flag and args[i+1] is a parameter value. Valid flag, value combinations are the following: -network [filename]: A .cdf or .m file containing the network to be read in. A .cdf file is in the IEEE Common Data Format (see https://www.ee.washington.edu/research/pstca/). A .m file is in MATPOWER format. -use_filter ["True", "False"]: Whether or not to use the FLiER filtering procedure. Default is True. -pmus [int,...,int]: A list of the indices of buses on which to place PMUs. -test_type ["Full", "Single_Lines"]: If "Full", perform a test of a substation splitting into two nodes. If "Single_Lines", perform a line failure test. Default is "Full". -test_scenarios [**]: A list of the scenarios to test. -noise [double]: Add noise to pre- and post-event voltage readings. Value specifies standard deviation of noise. Default is 0.0. -write_file [filename]: Where to write the output data. Default is "out.txt". -verbose ["True", "False"]: Turn verbose output to the command-line on or off. Default is "False". **test_scenarios format: A semicolon-separated list in which each item is a scenario. Each item is a substation index, a comma, and then a space-separated list of substation nodes that split from the substation. For example, "86,1 2 3;176,1 2;539,7;702,4 5". """ print "Running FLiER test with the following options:" print "\n".join(["{0}: {1}".format(args[i], args[i+1]) for i in range(1, len(args), 2)]) out = read_inputs(args) (filename, pmus, write_filename, test_type, noise, test_scenarios, use_filter, verbose) = out if filename.endswith('.m'): powernet = PowerNetwork.powernet_from_matpower(filename) else: powernet = PowerNetwork.powernet_from_IEEECDF(filename) # Simulate the pre-event network voltages. V = powernet.simulate() pre_event_volts = np.hstack([np.angle(V), np.abs(V)]) curr_FLiER = FLiER_Substation(powernet, pre_event_volts, pmus, verbose=verbose) solution_ranks = [] filter_ranks = [] num_tests = 0 num_missed = 0 file = open(write_filename, 'w') # Iterate over test cases. For each case, make the specified change to the # network and simulate the resulting voltages. Then run FLiER to attempt to # identify the change that occurred. for sub in curr_FLiER.substations: # For each substation, iterate over possible tests. The set of tests # returned by the iterator depends on test_type. for splitting_nodes in sub.node_iterator(test_type): key = (sub.index, tuple(splitting_nodes)) if test_scenarios is not None and key not in test_scenarios: continue if verbose: if test_type == 'Full': print "On bus {0}, splitting nbrs {1}".format(sub.index, splitting_nodes) else: bab = sub.bus_across_branch(splitting_nodes[0]) print "On line {0}".format((sub.index, bab.index)) try: # Make change according to test scenario, simulate results. # Some changes result in simulation nonconvergence. In this # case, skip the scenario. post_event_volts = get_post_event_volts(sub, splitting_nodes, powernet, curr_FLiER) except RuntimeError as e: print str(e) continue if noise > 0: # Add noise if called for. noisy_prev = pre_event_volts.copy() noisy_prev += np.random.normal(0, noise, len(noisy_prev)) noisy_postv = post_event_volts.copy() noisy_postv += np.random.normal(0, noise, len(noisy_postv)) noisy_FLiER = FLiER_Substation(powernet, noisy_prev, pmus) out = noisy_FLiER.find_topology_error(noisy_postv, test_type, use_filter) else: # Run FLiER out = curr_FLiER.find_topology_error(post_event_volts, test_type, use_filter) scores, filter_scores, fraction_ts_computed = out solkey = (sub.index, tuple(splitting_nodes)) # Solution key num_tests += 1 if scores is False: continue if solkey not in scores: if verbose: print "Solution not present" num_missed += 1 score_of_solution = np.Inf else: score_of_solution = scores[solkey] rank = np.sum(scores.values() <= score_of_solution) buses_ordered = [y[0][0] if y[0] != -1 else -1 for y in sorted(scores.items(), key = lambda x : x[1])] # bus_rank is the rank of the correct answer if we only care about # getting the substation index correct and not the specific nodes # that split. bus_rank = buses_ordered.index(sub.index) + 1 if score_of_solution == np.Inf: # Bit of a hack here. Occasionally the filter removes the # correct answer entirely from the list of possibilities. In # this case we just set the rank of the solution to something # large. rank = 300 bus_rank = 300 if verbose: print "Solution score: {0}".format(score_of_solution) print "Solution rank: {0}".format(rank) print "Correct bus rank: {0}".format(bus_rank) print "Number scores not filtered: {0}".format(len(scores)) # Update scoring data structures. Write the results of this test to # the output file. solution_ranks.append(rank) sol_filter_score = filter_scores[solkey] filter_ranks.append(np.sum(filter_scores.values() <= sol_filter_score)) write_to_file(file, solkey, out, curr_FLiER.substations) file.close() print num_tests print "{0} missed entirely".format(num_missed) sorted_solranklist, sorted_filtersolranklist = zip(*sorted(zip(solution_ranks, filter_ranks), key = lambda x : x[0])) print "Mean solution rank: {0}, Mean filtered solution rank: {1}".format(np.mean(sorted_solranklist), np.mean(sorted_filtersolranklist)) print "Median sol rank: {0}, median filtered sol rank: {1}".format(np.median(sorted_solranklist), np.median(sorted_filtersolranklist)) print "Frac correct: {0}, frac filtered correct: {1}".format(np.sum(np.array(sorted_solranklist) == 1) / float(num_tests), np.sum(np.array(sorted_filtersolranklist) == 1) / float(num_tests)) print "Frac in top 3: {0}, frac filtered in top 3: {1}".format(np.sum(np.array(sorted_solranklist) <= 3) / float(num_tests), np.sum(np.array(sorted_filtersolranklist) <= 3) / float(num_tests))
def network_FLiER_test(network_filename, pmus, noise, use_filter=True, test_branches = None, FLiER_type = "Jacobian", verbose=False): """ Simulate the failure of each line in the network one at a run, and run FLiER for each case. Inputs: network_filename - The path to the file containing the network in IEEE CDF format. pmus - A list of bus indices with PMUs on them. Outputs: """ if network_filename.endswith('.m'): powernet = PowerNetwork.powernet_from_matpower(network_filename) else: powernet = PowerNetwork.powernet_from_IEEECDF(network_filename) num_lines = len(powernet.branches) solution_ranks = [] # Contains ranks of correct answers score_list = [] # Contains tij's fraction_ts_computed_list = [] # How many tij's computed per test. dict_of_score_dicts = dict() num_filtered_out = 0 V = powernet.simulate() pre_event_volts = np.hstack([np.angle(V), np.abs(V)]) if test_branches is None: test_branches = range(len(powernet.branches)) for i in test_branches: branch = powernet.branches[i] if verbose: print "On line {0}".format([bus.index for bus in branch.buses]) out = single_FLiER_test(powernet, pmus, noise, branch_to_fail=branch, use_filter=use_filter, pre_event_volts=pre_event_volts, FLiER_type=FLiER_type, verbose=verbose) score_dict, initial_score_dict, fraction_ts_computed = out if score_dict == False: # then power flow equation solver did not converge continue fraction_ts_computed_list.append(fraction_ts_computed) if i in score_dict: score_of_solution = score_dict[i] rank = np.sum(score_dict.values() <= score_of_solution) solution_ranks.append(rank) else: # Then the correct line was filtered out. # Just choose the rank as last, and some large tij # filler value. if verbose: print "Solution filtered out." score_of_solution = 100 rank = num_lines solution_ranks.append(rank) num_filtered_out += 1 dict_of_score_dicts[i] = (score_dict, initial_score_dict, fraction_ts_computed) # Produce a list of (key, value) dictionary elements sorted by value. # Then split that list to create a sorted list of keys and a sorted list of # values. Recall that the keys are indices into the list of edges. branch_inds, scores = zip(*sorted(score_dict.iteritems(), key=lambda x: x[1])) if verbose: print score_of_solution, rank, len(score_dict) score_list.append((scores, score_of_solution)) if verbose: print "{0} filtered out entirely.".format(num_filtered_out) return score_list, dict_of_score_dicts, powernet.branches, solution_ranks, fraction_ts_computed_list