def test_missing_gen(): net = nw.case4gs() pp.rundcpp(net) res_gen = copy.deepcopy(net.res_gen.values) net.pop("res_gen") pp.rundcpp(net) assert np.allclose(net.res_gen.values, res_gen, equal_nan=True)
def evaluate_run(self, run): """Procedure to evaluate a single run (hour). Parameters ---------- run : int number of the evaluated run""" # copy network network = copy.deepcopy(self.network) # fetch run data sgen_run, load_run = self.regionalize_curves(network, run) # update network dataframe network.sgen.p_mw.update(sgen_run) network.load.p_mw.update(load_run) # set dcline direction network = self.direct_dclines(network, run) # evaluate loadflow pp.rundcpp(network) # evaluate the results of the run self.evaluate_run_result(network, run)
def test_PTDF(): net = nw.case9() pp.rundcpp(net) _, ppci = _pd2ppc(net) ptdf = makePTDF(ppci["baseMVA"], ppci["bus"], ppci["branch"], using_sparse_solver=False) _ = makePTDF(ppci["baseMVA"], ppci["bus"], ppci["branch"], result_side=1, using_sparse_solver=False) ptdf_sparse = makePTDF(ppci["baseMVA"], ppci["bus"], ppci["branch"], using_sparse_solver=True) if not np.allclose(ptdf, ptdf_sparse): raise AssertionError( "Sparse PTDF has differenct result against dense PTDF") if not ptdf.shape == (ppci["bus"].shape[0], ppci["branch"].shape[0]): raise AssertionError("PTDF has wrong dimension") if not np.all(~np.isnan(ptdf)): raise AssertionError("PTDF has NaN value")
def test_isolated_gen_lookup(): net=pp.create_empty_network() gen_bus=pp.create_bus(net,vn_kv=1., name='gen_bus') slack_bus=pp.create_bus(net,vn_kv=1., name='slack_bus') gen_iso_bus=pp.create_bus(net,vn_kv=1., name='iso_bus') pp.create_line(net, from_bus=slack_bus, to_bus=gen_bus, length_km=1, std_type="48-AL1/8-ST1A 10.0") pp.create_ext_grid(net, bus=slack_bus, vm_pu=1.) pp.create_gen(net, bus=gen_iso_bus, p_mw=1, vm_pu=1., name='iso_gen') pp.create_gen(net, bus=gen_bus, p_mw=1, vm_pu=1., name='oos_gen', in_service=False) pp.create_gen(net, bus=gen_bus, p_mw=2, vm_pu=1., name='gen') pp.rundcpp(net) assert np.allclose(net.res_gen.p_mw.values, [0, 0, 2]) pp.runpp(net) assert np.allclose(net.res_gen.p_mw.values, [0, 0, 2]) pp.create_xward(net, bus=gen_iso_bus, pz_mw=1., qz_mvar=1., ps_mw=1., qs_mvar=1., vm_pu=1., x_ohm=1., r_ohm=.1) pp.create_xward(net, bus=gen_bus, pz_mw=1., qz_mvar=1., ps_mw=1., qs_mvar=1., vm_pu=1., x_ohm=1., r_ohm=.1) pp.create_xward(net, bus=gen_iso_bus, pz_mw=1., qz_mvar=1., ps_mw=1., qs_mvar=1., vm_pu=1., x_ohm=1., r_ohm=.1, in_service=False) pp.rundcpp(net) assert np.allclose(net.res_gen.p_mw.values, [0, 0, 2]) assert np.allclose(net.res_xward.p_mw.values, [0, 2, 0]) pp.runpp(net) assert np.allclose(net.res_gen.p_mw.values, [0, 0, 2]) assert np.allclose(net.res_xward.p_mw.values, [0, 2, 0])
def _get_bus_ppc_mapping(net, bus_to_be_fused): bus_with_elements = set(net.load.bus).union(set(net.sgen.bus)).union( set(net.shunt.bus)).union(set(net.gen.bus)).union(set( net.ext_grid.bus)).union(set(net.ward.bus)).union( set(net.xward.bus)) # Run dc pp to get the ppc we need pp.rundcpp(net) bus_ppci = pd.DataFrame(data=net._pd2ppc_lookups['bus'], columns=["bus_ppci"]) bus_ppci['bus_with_elements'] = bus_ppci.index.isin(bus_with_elements) existed_bus = bus_ppci[bus_ppci.index.isin(net.bus.index)] bus_ppci['vn_kv'] = net.bus.loc[existed_bus.index, 'vn_kv'] ppci_bus_with_elements = bus_ppci.groupby( 'bus_ppci')['bus_with_elements'].sum() bus_ppci.loc[:, 'elements_in_cluster'] = ppci_bus_with_elements[ bus_ppci['bus_ppci'].values].values bus_ppci['bus_to_be_fused'] = False if bus_to_be_fused is not None: bus_ppci.loc[bus_to_be_fused, 'bus_to_be_fused'] = True bus_cluster_to_be_fused_mask = bus_ppci.groupby( 'bus_ppci')['bus_to_be_fused'].any() bus_ppci.loc[ bus_cluster_to_be_fused_mask[bus_ppci['bus_ppci'].values].values, 'bus_to_be_fused'] = True return bus_ppci
def test_ext_grid_gen_order_in_ppc(): net = pp.create_empty_network() for b in range(6): pp.create_bus(net, vn_kv=1., name=b) for l_bus in range(0, 5, 2): pp.create_line(net, from_bus=l_bus, to_bus=l_bus + 1, length_km=1, std_type="48-AL1/8-ST1A 10.0") for slack_bus in [0, 2, 5]: pp.create_ext_grid(net, bus=slack_bus, vm_pu=1.) for gen_bus in [0, 1, 2, 3, 4, 5, 5, 5, 5, 5, 5, 5, 5, 5]: pp.create_gen(net, bus=gen_bus, p_kw=-1, vm_pu=1.) pp.rundcpp(net) assert all(net.res_gen.p_kw == net.gen.p_kw) assert all(net.res_ext_grid.p_kw > 0) pp.runpp(net) assert all(net.res_gen.p_kw == net.gen.p_kw) assert all(net.res_ext_grid.p_kw > 0)
def run_dc_power_flow(self): """ Run a DC Power Flow for the network """ pp.rundcpp(self.network) return
def test_LODF(): net = nw.case9() pp.rundcpp(net) _, ppci = _pd2ppc(net) ptdf = makePTDF(ppci["baseMVA"], ppci["bus"], ppci["branch"]) lodf = makeLODF(ppci["branch"], ptdf) if not lodf.shape == (ppci["branch"].shape[0], ppci["branch"].shape[0]): raise AssertionError("LODF has wrong dimension")
def test_rundcpp_init(): net = pp.create_empty_network() b1, b2, l1 = add_grid_connection(net) b3 = pp.create_bus(net, vn_kv=0.4) tidx = pp.create_transformer(net, hv_bus=b2, lv_bus=b3, std_type="0.25 MVA 20/0.4 kV") pp.rundcpp(net)
def init(self): if self.pf == 'pf': pp.runpp(self.network) elif self.pf == 'dcpf': pp.rundcpp(self.network) elif self.pf == 'opf': pp.runopp(self.network) else: pp.rundcopp(self.network)
def test_test_sn_mva(): test_net_gen1 = result_test_network_generator_dcpp(sn_mva=1) test_net_gen2 = result_test_network_generator_dcpp(sn_mva=2) for net1, net2 in zip(test_net_gen1, test_net_gen2): pp.rundcpp(net1) pp.rundcpp(net2) try: assert_net_equal(net1, net2) except: raise UserWarning("Result difference due to sn_mva after adding %s" % net1.last_added_case)
def test_single_bus_network(): net = pp.create_empty_network() b = pp.create_bus(net, vn_kv=20.) pp.create_ext_grid(net, b) pp.runpp(net) assert net.converged pp.rundcpp(net) assert net.converged
def test_two_open_switches(): net = pp.create_empty_network() b1, b2, l1 = add_grid_connection(net) b3 = pp.create_bus(net, vn_kv=20.) l2 = create_test_line(net, b2, b3) create_test_line(net, b3, b1) pp.create_switch(net, b2, l2, et="l", closed=False) pp.create_switch(net, b3, l2, et="l", closed=False) pp.rundcpp(net) assert np.isnan(net.res_line.i_ka.at[l2]) or net.res_line.i_ka.at[l2] == 0
def runpf(self, is_dc=False): """ Now we just perform a powerflow with pandapower which can be done with either `rundcpp` for dc powerflow or with `runpp`for AC powerflow """ try: with warnings.catch_warnings(): # remove the warning if _grid non connex. And it that case load flow as not converged warnings.filterwarnings("ignore", category=scipy.sparse.linalg.MatrixRankWarning) warnings.filterwarnings("ignore", category=RuntimeWarning) if is_dc: pp.rundcpp(self._grid, check_connectivity=False) else: pp.runpp(self._grid, check_connectivity=False) return self._grid.converged except pp.powerflow.LoadflowNotConverged as exc_: # of the powerflow has not converged, results are Nan return False
def test_rundcpp_init_auxiliary_buses(): net = pp.create_empty_network() b1, b2, l1 = add_grid_connection(net, vn_kv=110.) b3 = pp.create_bus(net, vn_kv=20.) b4 = pp.create_bus(net, vn_kv=10.) tidx = pp.create_transformer3w(net, b2, b3, b4, std_type='63/25/38 MVA 110/20/10 kV') pp.create_load(net, b3, p_mw=5) pp.create_load(net, b4, p_mw=5) pp.create_xward(net, b4, 1, 1, 1, 1, 0.1, 0.1, 1.0) net.trafo3w.shift_lv_degree.at[tidx] = 120 net.trafo3w.shift_mv_degree.at[tidx] = 80 pp.rundcpp(net) va = net.res_bus.va_degree.at[b2] pp.rundcpp(net) assert np.allclose(va - net.trafo3w.shift_mv_degree.at[tidx], net.res_bus.va_degree.at[b3], atol=2) assert np.allclose(va - net.trafo3w.shift_lv_degree.at[tidx], net.res_bus.va_degree.at[b4], atol=2)
def step(self, *args, **kwargs): if self.pf == 'pf': a = pp.runpp(self.network) elif self.pf == 'dcpf': a = pp.rundcpp(self.network) elif self.pf == 'opf': a = pp.runopp(self.network) else: a = pp.rundcopp(self.network) return a
def assert_pf(net, dc=False): custom_file = os.path.join(os.path.abspath(os.path.dirname(pp.__file__)), "opf", "run_powermodels_powerflow.jl") if dc: # see https://github.com/lanl-ansi/PowerModels.jl/issues/612 for details pp.runpm(net, julia_file=custom_file, pm_model="DCMPPowerModel") else: pp.runpm(net, julia_file=custom_file, pm_model="ACPPowerModel") va_pm = copy.copy(net.res_bus.va_degree) vm_pm = copy.copy(net.res_bus.vm_pu) if dc: pp.rundcpp(net, calculate_voltage_angles=True) else: pp.runpp(net, calculate_voltage_angles=True) va_pp = copy.copy(net.res_bus.va_degree) vm_pp = copy.copy(net.res_bus.vm_pu) assert np.allclose(va_pm, va_pp) if not dc: assert np.allclose(vm_pm, vm_pp)
def runpf(self, is_dc=False): """ Now we just perform a powerflow with pandapower which can be done with either `rundcpp` for dc powerflow or with `runpp` for AC powerflow. This is really a 2 lines code. It is a bit more verbose here because we took care of silencing some warnings and try / catch some possible exceptions. """ try: with warnings.catch_warnings(): # remove the warning if _grid non connex. And it that case load flow as not converged warnings.filterwarnings( "ignore", category=scipy.sparse.linalg.MatrixRankWarning) warnings.filterwarnings("ignore", category=RuntimeWarning) if is_dc: pp.rundcpp(self._grid, check_connectivity=False) else: pp.runpp(self._grid, check_connectivity=False) return self._grid.converged, None except pp.powerflow.LoadflowNotConverged as exc_: # of the powerflow has not converged, results are Nan return False, exc_
def assert_pf(net, dc=False): if dc: model="DCMPPowerModel" else: model="ACPPowerModel" pp.runpm_pf(net, pm_model=model) va_pm = copy.deepcopy(net.res_bus.va_degree) vm_pm = copy.deepcopy(net.res_bus.vm_pu) if dc: pp.rundcpp(net, calculate_voltage_angles=True) else: pp.runpp(net, calculate_voltage_angles=True) va_pp = copy.deepcopy(net.res_bus.va_degree) vm_pp = copy.deepcopy(net.res_bus.vm_pu) assert np.allclose(va_pm, va_pp) if not dc: assert np.allclose(vm_pm, vm_pp)
def calc_sc_on_line(net, line_ix, distance_to_bus0, **kwargs): """ Calculate the shortcircuit in the middle of the line, returns a modified network with the shortcircuit calculation results and the bus added INPUT: **net** - panpdapower net **line_ix** (int) - The line of the shortcircuit **distance_to_bus0** (float) - The position of the shortcircuit should be between 0-1 OPTIONAL: **kwargs**** - the parameters required for the pandapower calc_sc function """ # Update network aux_net, aux_bus = _create_aux_net(net, line_ix, distance_to_bus0) pp.rundcpp(aux_net) calc_sc(aux_net, bus=aux_bus, **kwargs) # Return the new net and the aux bus return aux_net, aux_bus
def test_isolated_gen_lookup(): net = pp.create_empty_network() gen_bus = pp.create_bus(net, vn_kv=1., name='gen_bus') slack_bus = pp.create_bus(net, vn_kv=1., name='slack_bus') gen_iso_bus = pp.create_bus(net, vn_kv=1., name='iso_bus') pp.create_line(net, from_bus=slack_bus, to_bus=gen_bus, length_km=1, std_type="48-AL1/8-ST1A 10.0") pp.create_ext_grid(net, bus=slack_bus, vm_pu=1.) pp.create_gen(net, bus=gen_iso_bus, p_kw=-1, vm_pu=1., name='iso_gen') pp.create_gen(net, bus=gen_bus, p_kw=-2, vm_pu=1., name='gen') pp.rundcpp(net) assert net.res_gen.p_kw.values[1] == net.gen.p_kw.values[1] pp.runpp(net) assert net.res_gen.p_kw.values[1] == net.gen.p_kw.values[1]
def rundcpp_with_consistency_checks(net, **kwargs): pp.rundcpp(net, **kwargs) consistency_checks(net, test_q=False) return True
def run_ref_pf(self, net): pp.rundcpp(net, init="flat")
def runpf(self, is_dc=False): """ Run a power flow on the underlying _grid. This implements an optimization of the powerflow computation: if the number of buses has not changed between two calls, the previous results are re used. This speeds up the computation in case of "do nothing" action applied. """ # print("I called runpf") conv = True nb_bus = self.get_nb_active_bus() try: with warnings.catch_warnings(): # remove the warning if _grid non connex. And it that case load flow as not converged warnings.filterwarnings( "ignore", category=scipy.sparse.linalg.MatrixRankWarning) if nb_bus == self._nb_bus_before: self._pf_init = "results" init_vm_pu = "results" init_va_degree = "results" else: self._pf_init = "auto" init_vm_pu = None init_va_degree = None if is_dc: pp.rundcpp(self._grid, check_connectivity=False) self._nb_bus_before = None # if dc i start normally next time i call an ac powerflow else: pp.runpp(self._grid, check_connectivity=False, init=self._pf_init) self._nb_bus_before = nb_bus if self._grid.res_gen.isnull().values.any(): # sometimes pandapower does not detect divergence and put Nan. raise p.powerflow.LoadflowNotConverged # I retrieve the data once for the flows, so has to not re read multiple dataFrame self.p_or = self._aux_get_line_info("p_from_mw", "p_hv_mw") self.q_or = self._aux_get_line_info("q_from_mvar", "q_hv_mvar") self.v_or = self._aux_get_line_info("vm_from_pu", "vm_hv_pu") self.a_or = self._aux_get_line_info("i_from_ka", "i_hv_ka") * 1000 self.a_or[~np.isfinite(self.a_or)] = 0. self.p_ex = self._aux_get_line_info("p_to_mw", "p_lv_mw") self.q_ex = self._aux_get_line_info("q_to_mvar", "q_lv_mvar") self.v_ex = self._aux_get_line_info("vm_to_pu", "vm_lv_pu") self.a_ex = self._aux_get_line_info("i_to_ka", "i_lv_ka") * 1000 self.a_ex[~np.isfinite(self.a_ex)] = 0. self.v_or *= self.lines_or_pu_to_kv self.v_ex *= self.lines_ex_pu_to_kv return self._grid.converged except pp.powerflow.LoadflowNotConverged: # of the powerflow has not converged, results are Nan self.p_or = np.full(self.n_lines, dtype=np.float, fill_value=np.NaN) self.q_or = self.p_or self.v_or = self.p_or self.a_or = self.p_or self.p_ex = self.p_or self.q_ex = self.p_or self.v_ex = self.p_or self.a_ex = self.p_or self._nb_bus_before = None return False
def OPA_model(net, f_bus, f_gen, f_load, f_line, f_trafo): # This variable allows to stop the function when a total blackout situation is reached total_blackout = 0 # Set failed elements as out of service. # In the case of failed buses, all the elements connected to the bus are also removed f_bus = list(f_bus) pp.set_element_status(net, f_bus, in_service=False) f_gen = list(f_gen) net.gen.loc[f_gen, 'in_service'] = False f_load = list(f_load) net.load.loc[f_load, 'in_service'] = False f_line = list(f_line) net.line.loc[f_line, 'in_service'] = False f_trafo = list(f_trafo) net.trafo.loc[f_trafo, 'in_service'] = False # Variable for entering in the while loop cascade = True # Store the initial power level of each generator intermediate_gen_power = dict(net.gen.loc[:, 'p_mw']) order_dict_gen = collections.OrderedDict( sorted(intermediate_gen_power.items())) intermediate_gen_power = list(order_dict_gen.values()) intermediate_load_power = dict(net.load.loc[:, 'p_mw']) order_dict_load = collections.OrderedDict( sorted(intermediate_load_power.items())) intermediate_load_power = list(order_dict_load.values()) while cascade == True: # Create a networkx graph of the power network G = pp.topology.create_nxgraph(net) # Create a list of sets containing the islands of the network list_islands = list(nx.connected_components(G)) # Create a list of buses with generators in service list_bus_with_gen_in_service = list( net.gen.bus[net.gen.in_service == True]) list_bus_with_gen_in_service = list(set(list_bus_with_gen_in_service)) list_bus_with_gen_in_service.sort() list_bus_with_slack_gen_in_service = list( net.gen.bus[net.gen.in_service == True][net.gen.slack == True]) # Check the configuration of each island in the power network for island in list_islands: #print(island) island = list(island) # Check if the island has an external grid. If yes, it is ok and you can proceed to the next island. # If no, check if there is an availabl generator in the island. If yes, turn it into an external grid. # If no, go to the next island. if set(list_bus_with_slack_gen_in_service).isdisjoint(island): if set(list_bus_with_gen_in_service).isdisjoint(island): pass else: set_island = set(island) bus_with_gen_in_island = list( set_island.intersection(list_bus_with_gen_in_service)) slack_gen = list(net.gen.index[net.gen.bus == bus_with_gen_in_island[0]]) net.gen.loc[slack_gen[0], 'slack'] = True else: pass # Set the isolated islands (without ext. grid/slack gen.) out of service pp.set_isolated_areas_out_of_service(net) # Try to run a DC OPF try: pp.rundcopp(net, verbose=False) # If it converges, save a the power level at each generators and ext grids intermediate_gen_power = dict(net.res_gen.loc[:, 'p_mw']) order_dict_gen = collections.OrderedDict( sorted(intermediate_gen_power.items())) intermediate_gen_power = list(order_dict_gen.values()) intermediate_load_power = dict(net.res_load.loc[:, 'p_mw']) order_dict_load = collections.OrderedDict( sorted(intermediate_load_power.items())) intermediate_load_power = list(order_dict_load.values()) # If the DC OPF does not converge, increase the loads costs # A while loop which decreases the loads costs until the DC OPF converges or the costs reach a value of 0 except pp.OPFNotConverged: #print(f_line, f_bus, f_trafo) print('CONVERGENCE PROBLEMS') cost = 1 load_cost = -100 while cost == 1: load_cost += 2 #print(load_cost) net.poly_cost.cp1_eur_per_mw[net.poly_cost.et == 'load'] = load_cost try: pp.rundcopp(net) #pp.rundcopp(net, SCPDIPM_RED_IT=100, PDIPM_COSTTOL = 1e-3, PDIPM_GRADTOL = 1e-3) intermediate_gen_power = dict(net.res_gen.loc[:, 'p_mw']) order_dict_gen = collections.OrderedDict( sorted(intermediate_gen_power.items())) intermediate_gen_power = list(order_dict_gen.values()) intermediate_load_power = dict(net.res_load.loc[:, 'p_mw']) order_dict_load = collections.OrderedDict( sorted(intermediate_load_power.items())) intermediate_load_power = list(order_dict_load.values()) cost = 0 except pp.OPFNotConverged: pass # If the loads costs are set to 0, run a DC power flow with the last available generators power levels if load_cost == 0: net.gen.loc[:, 'p_mw'] = intermediate_gen_power net.load.loc[:, 'p_mw'] = intermediate_load_power pp.rundcpp(net) break # This is in case to generators is available (everything is failed basically) except UserWarning: total_blackout = 1 break # If there is a total blackout situation, break the loop if total_blackout == 1: break # Create a list of lines still in service list_line_in_service = list( net.line.loc[net.line.in_service == True].index) list_line_in_service.sort() # This variable is to check if there are new failures due to overloads new_failure = 0 for line in list_line_in_service: level_loading = net.res_line.loc[line, 'loading_percent'] # If a line a loading >= the 99% of its maximum capacity, it is considered overloaded if level_loading >= 99: print('OVERLOADS') new_failure = 1 net.line.loc[line, 'in_service'] = False # Same for transformer lines list_trafo_in_service = list( net.trafo.loc[net.trafo.in_service == True].index) list_trafo_in_service.sort() for trafo in list_trafo_in_service: level_loading = net.res_trafo.loc[trafo, 'loading_percent'] if level_loading >= 99: print('OVERLOADS') new_failure = 1 net.trafo.loc[trafo, 'in_service'] = False # If there are no overloads, break the while loop if new_failure == 0: break # Save the final power levels of each generator and load if total_blackout == 0: final_state_load = dict(net.res_load.loc[:, 'p_mw']) order_dict_load = collections.OrderedDict( sorted(final_state_load.items())) list_load_power = list(order_dict_load.values()) final_state_gen = dict(net.res_gen.loc[:, 'p_mw']) order_dict_gen = collections.OrderedDict( sorted(final_state_gen.items())) list_gen_power = list(order_dict_gen.values()) list_gen_power = np.array(list_gen_power) list_load_power = np.array(list_load_power) for i, j in enumerate(list_gen_power): if math.isnan(j): list_gen_power[i] = 0 for i, j in enumerate(list_load_power): if math.isnan(j): list_load_power[i] = 0 elif total_blackout == 1: list_gen_power = np.zeros(len(net.gen.index)) list_load_power = np.zeros(len(net.load.index)) return (list_gen_power, list_load_power)
def power_over_congestion(Grid, TypeElmnt, BranchIndTable, max_load_percent): """ Calculates delta power over from max power expected under the max_load_percent. Increases OverloadP_kW in 5% over max power branch limit. Returns (OverloadP_kW, loading_percent) """ # FLUJO DE LINEA CAMBIA SIGNO. Absolute values requiered loading_percent = Grid['res_' + TypeElmnt].at[BranchIndTable, 'loading_percent'] FluPAB_kW = abs( Grid['res_' + TypeElmnt].at[BranchIndTable, 'p_from_kw']) # da igual dirección (signo) MaxFluP_kW = FluPAB_kW * max_load_percent / loading_percent # >= 0 OverloadP_kW = FluPAB_kW - MaxFluP_kW OverloadP_kW += MaxFluP_kW * (0.05) # increase 5% to compensate fpl error return OverloadP_kW, loading_percent if __name__ == '__main__': # directly import conftest.py file (test only) import pandapower as pp from sys import path as sys__path sys__path.insert(0, "test/") # import test case from conftest import Sist5Bus Grid = Sist5Bus() redispatch(Grid, 'line', 0, max_load_percent=0.08) # redispatch(Grid, 'line', 0, max_load_percent = 2.8) pp.rundcpp(Grid)
def get_violation_number(grid3120, name): net = grid3120 pp.rundcpp(net) ppc = net["_ppc"] ##### Setup Grid fluctuation parameters and constraints ######## ## thresold on shift significance in DC-PF Eqs ## pwr = pwr_shf + np.real(bbus)*va ## shf_eps = 1e-4 ## Std for fluctuating loads divided by their nominal values: ## for small grids values 0.5-1 are realistic ## larger grids have cov_std = 0.1 -- 0.3 or less ## cov_std = 0.25 ## Phase angle difference limit ## small grids: pi/8 -- pi/6 ## large grids: pi/3 -- pi/4 ## bnd = math.pi / 2 ### Cut small probabilities threshold ### discard all probabilities than thrs* prb(closest hyperplane) ### ### ### Crucially affects time performance ### thrs = 0.001 ### Number of samples used in experiments ### 500 is often enough ### 10000 is a default value supresses the variance nsmp = 500 ### Step-sizes for KL and Var minimization ### works well with 0.1-0.01 eta_vm = 0.1 eta_kl = 0.1 ### Rounding threshold in optimization: ### if a (normalized on the simplex) hpl probability becomes lower then 0.001 ### we increase it to this level ### ### Crucially affects numerical stability ### eps = 0.001 ##### Setup power grid case in a convenient form for further sampling ######## ### find number of lines (m) and buses(n) m = net.line['to_bus'].size n = net.res_bus['p_mw'].size ### Construct adjacency matrix ### adj = np.zeros((2 * m, n)) for i in range(0, m): adj[i, net.line['to_bus'][i]] = 1 adj[i, net.line['from_bus'][i]] = -1 adj[i + m, net.line['to_bus'][i]] = -1 adj[i + m, net.line['from_bus'][i]] = 1 ### DC power flow equations have a form: ### ### pwr = pwr_shf + np.real(bbus)*va ### (compute all parameters) bbus = np.real(ppc['internal']['Bbus']) va = math.pi * net.res_bus['va_degree'] / 180 pwr = -net.res_bus['p_mw'] pwr_shf = pwr - bbus @ va ### pwr_shf is significant or not: ### ### if the shift is small: zero it out ### (simplifies testing and removes "math zeros") print("significant shift: ", np.max(pwr_shf) - np.min(pwr_shf) > shf_eps) if (np.max(pwr_shf) - np.min(pwr_shf) < shf_eps): pwr_shf[range(0, n)] = 0 ### Phase angle differences: ### ### va = pinv(bbus)*(pwr - pwr_shf) ### va_d = adj*va = adj*pinv(bbus)*(pwr - pwr_shf) ### va_d = pf_mat*pwr - va_shf bbus_pinv = pinvh(bbus.todense()) pf_mat = adj @ bbus_pinv va_shf = pf_mat @ pwr_shf ### Voltage angle differences: ### va_d = pf_mat @ pwr - va_shf ##### Distribution of fluctuations ###### ### assume the only one slack (a higher-level grid) in the grid ### supress all its fluctuations and balance the grid ### ### TODO: adjust to a general case ### slck = net.ext_grid['bus'] slck_mat = np.eye(n) slck_mat[slck] = -1 ## assign values to the whole array slck_mat[slck, slck] = 0 # and zero out for the slack itself ### set fluctuating components: either loads or gens or both ### loads = np.zeros(n) gens = np.zeros(n) ctrls = np.zeros(n) ## controllable loads + gens loads[net.load['bus']] = -net.res_load['p_mw'] gens[net.gen['bus']] = net.res_gen['p_mw'] ctrls = loads + gens ### assume only loads are fluctuating ### xi = loads ### Set covariance matrix and mean ### ### cov_sq = square of the covariance matrix ### Gaussian rv with covariance \Sigma is \Sigma^{1/2} * std_normal_rv ### ### TODO: change to LU/cholesky factorization ### cov_sq = cov_std * np.diag(np.abs(xi)) ### Final equations with fluctuations xi are then ### ### w/o fluctuations: ### va_d = pf_mat*pwr - va_shf ### with fluctuations: ### va_d = pf_mat@(pwr + slck_mat*cov_sq*xi) - va_shf ### va_d = pf_mat@pwr - va_shf + (pf_mat@(slck_mat@cov_sq))@xi_std ### va_d = mu + A@xi_std ### where xi_std is a standard normal with only fluctuating components ### A = (pf_mat @ slck_mat) @ cov_sq mu = pf_mat @ pwr - va_shf ### Feasibility Polytope Inequalities ### bnd \ge va_d = mu_f + A_f@xi_std ### incorporates both va_d \le b and va_d \ge -b as we have va_d's with 2 signs ### b = np.ones(2 * m) * bnd ### normalize the matrices to make it easier to compute a failure probability ### ### compute row norms of A nrms = np.maximum(la.norm(A, axis=1), 1e-20) ### normalize A and b so that b_n\ge A_n*xi_std b_n = (b - mu) / nrms A_n = [A[i] / nrms[i] for i in range(0, 2 * m)] ##### Assest equations feasibility ####### ### Power balance check ### print("Eqs balance check:", 0 == np.sum(np.sign(mu))) ### check positiveness of bnd - mu_f = RHS - LHS ### print("Inqs. feasibility check: ", np.min(b - mu) > 0) print("Min gap in phase angles = min(RHS - LHS)", np.min(b - mu)) ## positive value, otherwise the grid fails whp print("The RHS (phase angle diff max) = ", bnd) ### Compute probabilities: ### prb: probability of each hpl failure ### p_up, p_dwn: upper and lower bounds ### prb = norm.cdf(-b_n) p_up = np.sum(prb) p_dwn = np.max(prb) print("the union bound (upper):", p_up) print("the max bound (lower):", p_dwn) ### Keep only valuable probabilities: ### - use the union bound for all the rest ### - keep only the prbs higher than the thrs* p_dwn prbh_id = (prb > thrs * p_dwn) prb_rmd = np.sum(prb[~(prb > thrs * p_dwn)]) print("Remainder probability (omitted):", prb_rmd) ############ Preliminary steps for Sampling and Importance Sampling ############ ### normalize all active probabilities to one ### as we only play a hyperplane out of them ### ### NB: crucial steps in performance optimization ### x_id = np.where(prbh_id == True)[0] ### local normalized versions of A and b, ### reduced in size: number of rows now is equal to a number of constraints ### that have a high probability of violation ### x_bn = b_n[x_id] ### we do not care about the full matrix A and vector b ### only about important parts of them A_n = np.array(A_n) x_An = A_n[x_id] print("# hpls we care of: ", len(x_bn)) ############# Monte-Carlo ################## rv = norm() x_std = norm.rvs(size=[n, nsmp]) smp = x_An @ x_std ### fls_mc = failures in Monte-Carlo, e.g. ### when MC discovers a failure ### fls_mc = sum((x_bn <= smp.T[:]).T) print("Max # of hlps a sample if out of: ", np.max(fls_mc)) ### MC failure expectation and std ### mc_exp = (1 - np.sum(fls_mc == 0) / nsmp) * (1 - prb_rmd) + prb_rmd mc_std = (1 - prb_rmd) / math.sqrt(nsmp) violation_dict = {} for i in range(0, np.max(fls_mc) + 1): print(i, "hpls violated (exactly) vs # cases", np.sum(fls_mc == i)) violation_dict[i] = int(np.sum(fls_mc == i)) print("\nMC(exp, std):", (mc_exp, mc_std)) ## write into file #path_to_viol_dirs = os.path.join("results", "hplns_violations") violation_dict["Ineqs. Feasibility Check"] = bool(np.min(b - mu) > 0) violation_dict["Significant shift"] = bool( np.max(pwr_shf) - np.min(pwr_shf) > shf_eps) violation_dict["Min gap in phase angles = min(RHS - LHS)"] = float( np.min(b - mu)) violation_dict["The RHS (phase angle diff max) = "] = float(bnd) violation_dict["Probability"] = mc_exp ############# ALOE ################## ### ### Exactly follows to the Owen/Maximov/Chertkov paper, EJOS'19 ### ### sample z ~ N(0, I_n) ### sample u ~ U(0,1) ### compute y = F^{-1}(u F(-b_i)) ### compute x = - (a_i * y + (I - a_i.T * a_i) z) ### ### Ouput: union bound divided by the expected failure multiplicity ### ### Initialize samplers ### ### sample z ~ N(0, I_n) and u ~ U(0,1) ### rv = norm() rv_u = uniform() z = norm.rvs(size=[nsmp, n]) u = uniform.rvs(size=[nsmp]) ### x_alph is a vector of ALOE probabilities ### normalized by a unit simplex ### x_alph = prb[prbh_id] / np.sum(prb[prbh_id]) print("ALOE prbs for major hpls: ", x_alph) ### _hpl: how many smpls beyond each of the hpls ### _hpl = multinomial.rvs(n=nsmp, p=x_alph) ### print("# samples per hpl", _hpl) ### Get cummulative sums, which are easier to work with _hpl = list(itertools.accumulate(_hpl)) _hpl = np.array(_hpl) ### print("cusum of # hpls", _hpl) ### Generate samples ### x_aloe -- samples generated by ALOE ### ### TODO: seems optimizable, but I am not sure about memory mgmnt in python x_aloe = np.zeros([nsmp, n]) # index of the active hyperplane hpl_id = 0 ### get samples x_aloe according to the algorithm for i in range(0, nsmp): ### get index of a hyperplane to sample beyond hpl_id = (hpl_id, hpl_id + 1)[i >= _hpl[hpl_id]] y = norm.ppf(u[i] * norm.cdf(-x_bn[hpl_id])) x_aloe[i] = -x_An[hpl_id] * y - z[i] + np.outer( x_An[hpl_id], x_An[hpl_id]) @ z[i] ### test how many constraints are violated smp = x_An @ x_aloe.T ### compute expectation and std aloe_exp = p_up * np.sum( 1. / np.sum(x_bn <= smp.T[:], axis=1)) / nsmp + prb_rmd aloe_std = p_up * math.sqrt(2 * len(_hpl)) / math.sqrt(nsmp) # indeed len(_hpl) instead of 2*m in the Thrm print("ALOE (exp, std)", (aloe_exp, aloe_std)) ####### Optimization approach ###### ####### ####### Variance Minimization ###### ####### ### setup the initial values eta = eta_vm md_var = 0 md_exp = 0 grad = np.zeros(len(x_bn)) #gradient on each iteration _hpl = np.zeros(nsmp) # hpls choosen by the method ### intentionally use a copy instead of a reference ### alph is a vector of weigths to be updated in algorithm ### alph = x_alph[:] # values for Phi (x_bn) x_phi = [norm.cdf(-x_bn[i]) for i in range(0, len(x_bn))] ### grad normalization by prbs[i] factor is introduced to make computations numerically stable ### prbs = prb[prbh_id] for i in range(0, nsmp): ### sample x according to current alph hpl_id = np.where( multinomial.rvs(n=1, p=alph, size=1, random_state=None)[0] == 1)[0] _hpl[i] = hpl_id ### generate a sample following to the ALOE procedure y = norm.ppf(u[i] * norm.cdf(-x_bn[hpl_id])) x_smp = -x_An[hpl_id] * y - z[i] + np.outer(x_An[hpl_id], x_An[hpl_id]) @ z[i] ### the RHS' to be compared with x_bn x_smp = x_An @ x_smp.T ### results of constraints violations for each generated object cns_vlt = (x_bn <= x_smp.T[:])[0] ### weight vector defined by the multiplicity of constraint violation for each sample wgt = 1. / np.sum(np.multiply(cns_vlt, np.multiply(alph, 1. / x_alph))) ### compute gradient of the variance, see the paper (our + OMC) for details grad = [ -p_up * p_up * wgt * wgt * norm.pdf(x_smp[k])[0] * cns_vlt[k] / prbs[k] for k in range(len(x_smp)) ] grad = np.array(grad) ### The gradient is high -- signal about emergency as it can zero out all weights if (la.norm(eta * grad) > 1e4): print( "\n############## Extremely high gradient ############\n" ) print("Iteration: ", i, "\nGradient:", grad) ### make a ``simplex MD'' update alph = [ math.exp(-eta * grad[k]) * alph[k] for k in range(0, len(x_smp)) ] ### enter if some coordinates are too small and may cause numerical instability ### increase the corresponding weigths if (np.min(alph) < eps): print("########### some coordinates are small #################") alph = [alph[k] + eps for k in range(0, len(x_bn))] ### make a projection to the unit simplex alph = alph / np.sum(alph) ### adjust contribution to the errors md_exp = md_exp + wgt md_var = md_var + p_up * np.dot(grad.T, grad) print("Optimal weigths of MD-Var minimization: ", alph) print("Optimal weigths of ALOE", x_alph) ### normalize errors, compute standard deviation md_exp = p_up * md_exp / nsmp + prb_rmd md_std = p_up * math.sqrt(md_var) / nsmp print("MD-Var (exp, std)", (md_exp, md_std)) #print("assert normalization:", np.sum(alph), np.sum(x_alph)) ####### Optimization approach ###### ####### ####### KL Minimization ###### ####### ### SMD step-size eta = eta_kl ### setup initial values kl_exp = 0 kl_var = 0 grad = np.zeros(len(x_bn)) _hpl = np.zeros(nsmp) ## _hpl[i] = beyond which hpl we sample on iteration i ### intentionally use a copy instead of a reference ### alph is an optimization variable alph = x_alph[:] ### this normalization factor is introduced to make computations numerically stable prbs = prb[prbh_id] for i in range(0, nsmp): #,miniters=500): ### sample x according to current alph hpl_id = np.where( multinomial.rvs(n=1, p=alph, size=1, random_state=None)[0] == 1)[0] _hpl[i] = hpl_id ### generate a sample accordint to ALOE y = norm.ppf(u[i] * norm.cdf(-x_bn[hpl_id])) x_smp = -x_An[hpl_id] * y - z[i] + np.outer(x_An[hpl_id], x_An[hpl_id]) @ z[i] ### RHS to compare with x_bn x_smp = x_An @ x_smp.T ### results of constraints violations for the generated object cns_vlt = (x_bn <= x_smp.T[:])[0] ### object weight which is set according to ALOE wgt = 1. / np.sum(np.multiply(cns_vlt, np.multiply(alph, 1. / x_alph))) # the KL divergence's gradient grad = [ -p_up * wgt * norm.pdf(x_smp[k])[0] * cns_vlt[k] / prbs[k] for k in range(len(x_smp)) ] grad = np.array(grad) ### The gradient is high -- signal about emergency as it can zero out all weights if (la.norm(eta * grad) > 1e4): print( "\n############## Extremely high gradient ############\n" ) print("Iteration: ", i, "\nGradient:", grad) ### make a ``simplex MD'' update alph = [ math.exp(-eta * grad[k]) * alph[k] for k in range(0, len(x_smp)) ] ### enter if some coordinates are too small and may cause numerical instability ### increase the corresponding weigths if (np.min(alph) < eps): print("########### some coordinates are small #################") alph = [alph[k] + eps for k in range(0, len(x_bn))] ### make a projection to the unit simplex alph = alph / np.sum(alph) ### adjust contribution to the errors kl_exp = kl_exp + wgt kl_var = kl_var + p_up * np.dot(grad.T, grad) * wgt alph_KL = alph print("Optimal weigths of MD-KL minimization: ", alph_KL) print("Optimal weigths of ALOE", x_alph) ### normalize errors kl_exp = p_up * kl_exp / nsmp + prb_rmd kl_std = p_up * math.sqrt(kl_var) / nsmp print("MD-KL (exp, std)", (kl_exp, kl_std)) #print("assert normalization:", np.sum(alph), np.sum(x_alph)) ############## Output all probabilities ################## print("the union bound (up):", p_up) print("the max bound (lower):", p_dwn) print("MC(exp, std):", mc_exp, mc_std) print("ALOE(exp, std)", aloe_exp, aloe_std) print("MD-Var(exp, var)", md_exp, md_std) print("MD-KL(exp, var)", kl_exp, kl_std) violation_dict["MD-Var"] = (md_exp, md_std) violation_dict["MD-KL"] = (kl_exp, kl_std) violation_dict["ALOE"] = (aloe_exp, aloe_std) violation_dict["MD-Var-W"] = [float(a) for a in alph] violation_dict["MD-KL-W"] = [float(a) for a in alph_KL] violation_dict["ALOE-W"] = [float(a) for a in x_alph] with open(name + ".json", 'w+') as fp: json.dump(violation_dict, fp)
def runpf(self, is_dc=False): """ .. warning:: /!\\\\ Internal, do not use unless you know what you are doing /!\\\\ Run a power flow on the underlying _grid. This implements an optimization of the powerflow computation: if the number of buses has not changed between two calls, the previous results are re used. This speeds up the computation in case of "do nothing" action applied. """ conv = True nb_bus = self.get_nb_active_bus() try: with warnings.catch_warnings(): # remove the warning if _grid non connex. And it that case load flow as not converged warnings.filterwarnings("ignore", category=scipy.sparse.linalg.MatrixRankWarning) warnings.filterwarnings("ignore", category=RuntimeWarning) if self._nb_bus_before is None: self._pf_init = "dc" elif nb_bus == self._nb_bus_before: self._pf_init = "results" else: self._pf_init = "auto" if np.any(~self._grid.load["in_service"]): # TODO see if there is a better way here -> do not handle this here, but rather in Backend._next_grid_state raise pp.powerflow.LoadflowNotConverged("Isolated load") if np.any(~self._grid.gen["in_service"]): # TODO see if there is a better way here -> do not handle this here, but rather in Backend._next_grid_state raise pp.powerflow.LoadflowNotConverged("Isolated gen") if is_dc: pp.rundcpp(self._grid, check_connectivity=False) self._nb_bus_before = None # if dc i start normally next time i call an ac powerflow else: pp.runpp(self._grid, check_connectivity=False, init=self._pf_init, numba=numba_) if "_ppc" in self._grid: if "et" in self._grid["_ppc"]: self.comp_time += self._grid["_ppc"]["et"] if self._grid.res_gen.isnull().values.any(): # TODO see if there is a better way here -> do not handle this here, but rather in Backend._next_grid_state # sometimes pandapower does not detect divergence and put Nan. raise pp.powerflow.LoadflowNotConverged("Isolated gen") self.prod_p[:], self.prod_q[:], self.prod_v[:] = self._gens_info() self.load_p[:], self.load_q[:], self.load_v[:] = self._loads_info() if not is_dc: if not np.all(np.isfinite(self.load_v)): # TODO see if there is a better way here # some loads are disconnected: it's a game over case! raise pp.powerflow.LoadflowNotConverged("Isolated load") else: # fix voltages magnitude that are always "nan" for dc case # self._grid.res_bus["vm_pu"] is always nan when computed in DC self.load_v[:] = self.load_pu_to_kv # TODO # need to assign the correct value when a generator is present at the same bus # TODO optimize this ugly loop for l_id in range(self.n_load): if self.load_to_subid[l_id] in self.gen_to_subid: ind_gens = np.where(self.gen_to_subid == self.load_to_subid[l_id])[0] for g_id in ind_gens: if self._topo_vect[self.load_pos_topo_vect[l_id]] == self._topo_vect[self.load_pos_topo_vect[g_id]]: self.load_v[l_id] = self.prod_v[g_id] break self.line_status[:] = self._get_line_status() # I retrieve the data once for the flows, so has to not re read multiple dataFrame self.p_or[:] = self._aux_get_line_info("p_from_mw", "p_hv_mw") self.q_or[:] = self._aux_get_line_info("q_from_mvar", "q_hv_mvar") self.v_or[:] = self._aux_get_line_info("vm_from_pu", "vm_hv_pu") self.a_or[:] = self._aux_get_line_info("i_from_ka", "i_hv_ka") * 1000 self.a_or[~np.isfinite(self.a_or)] = 0. self.v_or[~np.isfinite(self.v_or)] = 0. self.p_ex[:] = self._aux_get_line_info("p_to_mw", "p_lv_mw") self.q_ex[:] = self._aux_get_line_info("q_to_mvar", "q_lv_mvar") self.v_ex[:] = self._aux_get_line_info("vm_to_pu", "vm_lv_pu") self.a_ex[:] = self._aux_get_line_info("i_to_ka", "i_lv_ka") * 1000 self.a_ex[~np.isfinite(self.a_ex)] = 0. self.v_ex[~np.isfinite(self.v_ex)] = 0. # it seems that pandapower does not take into account disconencted powerline for their voltage self.v_or[~self.line_status] = 0. self.v_ex[~self.line_status] = 0. self.v_or[:] *= self.lines_or_pu_to_kv self.v_ex[:] *= self.lines_ex_pu_to_kv self._nb_bus_before = None self._grid._ppc["gen"][self._iref_slack, 1] = 0. self._topo_vect[:] = self._get_topo_vect() return self._grid.converged except pp.powerflow.LoadflowNotConverged as exc_: # of the powerflow has not converged, results are Nan self.p_or[:] = np.NaN self.q_or[:] = np.NaN self.v_or[:] = np.NaN self.a_or[:] = np.NaN self.p_ex[:] = np.NaN self.q_ex[:] = np.NaN self.v_ex[:] = np.NaN self.a_ex[:] = np.NaN self.prod_p[:] = np.NaN self.prod_q[:] = np.NaN self.prod_v[:] = np.NaN self.load_p[:] = np.NaN self.load_q[:] = np.NaN self.load_v[:] = np.NaN self._nb_bus_before = None return False
def run_ref_pf(self, net): with warnings.catch_warnings(): warnings.filterwarnings("ignore") pp.rundcpp(net, init="flat")
def runpf(self, is_dc=False): """ Run a power flow on the underlying _grid. This implements an optimization of the powerflow computation: if the number of buses has not changed between two calls, the previous results are re used. This speeds up the computation in case of "do nothing" action applied. """ conv = True nb_bus = self.get_nb_active_bus() try: with warnings.catch_warnings(): # remove the warning if _grid non connex. And it that case load flow as not converged warnings.filterwarnings( "ignore", category=scipy.sparse.linalg.MatrixRankWarning) warnings.filterwarnings("ignore", category=RuntimeWarning) if self._nb_bus_before is None: self._pf_init = "dc" elif nb_bus == self._nb_bus_before: self._pf_init = "results" else: self._pf_init = "auto" if is_dc: pp.rundcpp(self._grid, check_connectivity=False) self._nb_bus_before = None # if dc i start normally next time i call an ac powerflow else: pp.runpp(self._grid, check_connectivity=False, init=self._pf_init, numba=numba_) if self._grid.res_gen.isnull().values.any(): # TODO see if there is a better way here -> do not handle this here, but rather in Backend._next_grid_state # sometimes pandapower does not detect divergence and put Nan. raise pp.powerflow.LoadflowNotConverged("Isolated gen") self.load_p[:], self.load_q[:], self.load_v[:] = self._loads_info( ) if not is_dc: if not np.all(np.isfinite(self.load_v)): # TODO see if there is a better way here # some loads are disconnected: it's a game over case! raise pp.powerflow.LoadflowNotConverged( "Isolated load") self.line_status[:] = self._get_line_status() # I retrieve the data once for the flows, so has to not re read multiple dataFrame self.p_or[:] = self._aux_get_line_info("p_from_mw", "p_hv_mw") self.q_or[:] = self._aux_get_line_info("q_from_mvar", "q_hv_mvar") self.v_or[:] = self._aux_get_line_info("vm_from_pu", "vm_hv_pu") self.a_or[:] = self._aux_get_line_info("i_from_ka", "i_hv_ka") * 1000 self.a_or[~np.isfinite(self.a_or)] = 0. self.v_or[~np.isfinite(self.v_or)] = 0. # it seems that pandapower does not take into account disconencted powerline for their voltage self.v_or[~self.line_status] = 0. self.v_ex[~self.line_status] = 0. self.p_ex[:] = self._aux_get_line_info("p_to_mw", "p_lv_mw") self.q_ex[:] = self._aux_get_line_info("q_to_mvar", "q_lv_mvar") self.v_ex[:] = self._aux_get_line_info("vm_to_pu", "vm_lv_pu") self.a_ex[:] = self._aux_get_line_info("i_to_ka", "i_lv_ka") * 1000 self.a_ex[~np.isfinite(self.a_ex)] = 0. self.v_ex[~np.isfinite(self.v_ex)] = 0. self.v_or[:] *= self.lines_or_pu_to_kv self.v_ex[:] *= self.lines_ex_pu_to_kv self.prod_p[:], self.prod_q[:], self.prod_v[:] = self._gens_info( ) self._nb_bus_before = None self._grid._ppc["gen"][self._iref_slack, 1] = 0. self._topo_vect[:] = self._get_topo_vect() return self._grid.converged except pp.powerflow.LoadflowNotConverged as exc_: # of the powerflow has not converged, results are Nan self.p_or[:] = np.NaN self.q_or[:] = np.NaN self.v_or[:] = np.NaN self.a_or[:] = np.NaN self.p_ex[:] = np.NaN self.q_ex[:] = np.NaN self.v_ex[:] = np.NaN self.a_ex[:] = np.NaN self.prod_p[:] = np.NaN self.prod_q[:] = np.NaN self.prod_v[:] = np.NaN self.load_p[:] = np.NaN self.load_q[:] = np.NaN self.load_v[:] = np.NaN self._nb_bus_before = None return False