def _run_bfsw_ppc(ppc, ppopt=None): """ SPARSE version of distribution power flow solution according to [1] :References: [1] Jen-Hao Teng, "A Direct Approach for Distribution System Load Flow Solutions", IEEE Transactions on Power Delivery, vol. 18, no. 3, pp. 882-887, July 2003. :param ppc: matpower-style case data :return: results (pypower style), success (flag about PF convergence) """ ppci = ppc ppopt = ppoption(ppopt) baseMVA, bus, gen, branch = \ ppci["baseMVA"], ppci["bus"], ppci["gen"], ppci["branch"] nbus = bus.shape[0] # get bus index lists of each type of bus ref, pv, pq = bustypes(bus, gen) # depth-first-search bus ordering and generating Direct Load Flow matrix DLF = BCBV * BIBC DLF, ppc_bfsw, buses_ordered_bfsw = bibc_bcbv(ppci) baseMVA_bfsw, bus_bfsw, gen_bfsw, branch_bfsw = \ ppc_bfsw["baseMVA"], ppc_bfsw["bus"], ppc_bfsw["gen"], ppc_bfsw["branch"] time_start = time() # starting pf calculation timing # initialize voltages to flat start and buses with gens to their setpoints V0 = np.ones(nbus, dtype=complex) V0[gen[:, GEN_BUS].astype(int)] = gen[:, VG] Sbus_bfsw = makeSbus(baseMVA_bfsw, bus_bfsw, gen_bfsw) # update data matrices with solution Ybus_bfsw, Yf_bfsw, Yt_bfsw = makeYbus(baseMVA_bfsw, bus_bfsw, branch_bfsw) ## get bus index lists of each type of bus ref_bfsw, pv_bfsw, pq_bfsw = bustypes(bus_bfsw, gen_bfsw) # #----- run the power flow ----- V_final, success = bfsw(DLF, bus_bfsw, gen_bfsw, branch_bfsw, baseMVA_bfsw, Ybus_bfsw, Sbus_bfsw, V0, ref_bfsw, pv_bfsw, pq_bfsw, ppopt=ppopt) V_final = V_final[np.argsort(buses_ordered_bfsw)] # return bus voltages in original bus order # #----- output results to ppc ------ ppci["et"] = time() - time_start # pf time end # generate results for original bus ordering Ybus, Yf, Yt = makeYbus(baseMVA, bus, branch) bus, gen, branch = pfsoln(baseMVA, bus, gen, branch, Ybus, Yf, Yt, V_final, ref, pv, pq) ppci["success"] = success ppci["bus"], ppci["gen"], ppci["branch"] = bus, gen, branch return ppci, success
def _get_pf_variables_from_ppci(ppci): ## default arguments if ppci is None: ValueError('ppci is empty') # ppopt = ppoption(ppopt) # get data for calc baseMVA, bus, gen, branch = \ ppci["baseMVA"], ppci["bus"], ppci["gen"], ppci["branch"] ## get bus index lists of each type of bus ref, pv, pq = bustypes(bus, gen) ## generator info on = find(gen[:, GEN_STATUS] > 0) ## which generators are on? gbus = gen[on, GEN_BUS].astype(int) ## what buses are they at? ## initial state # V0 = ones(bus.shape[0]) ## flat start V0 = bus[:, VM] * exp(1j * pi / 180 * bus[:, VA]) V0[gbus] = gen[on, VG] / abs(V0[gbus]) * V0[gbus] return baseMVA, bus, gen, branch, ref, pv, pq, on, gbus, V0
def _run_ac_pf_with_qlims_enforced(ppci, recycle, makeYbus, ppopt): baseMVA, bus, gen, branch, ref, pv, pq, on, gbus, V0 = _get_pf_variables_from_ppci( ppci) qlim = ppopt["ENFORCE_Q_LIMS"] limited = [] ## list of indices of gens @ Q lims fixedQg = zeros(gen.shape[0]) ## Qg of gens at Q limits while True: ppci, success, bus, gen, branch = _run_ac_pf_without_qlims_enforced( ppci, recycle, makeYbus, ppopt) ## find gens with violated Q constraints gen_status = gen[:, GEN_STATUS] > 0 qg_max_lim = gen[:, QG] > gen[:, QMAX] qg_min_lim = gen[:, QG] < gen[:, QMIN] mx = find(gen_status & qg_max_lim) mn = find(gen_status & qg_min_lim) if len(mx) > 0 or len(mn) > 0: ## we have some Q limit violations # No PV generators if len(pv) == 0: if ppopt["VERBOSE"]: if len(mx) > 0: logger.info( 'Gen %d [only one left] exceeds upper Q limit : INFEASIBLE PROBLEM\n' % mx + 1) else: logger.info( 'Gen %d [only one left] exceeds lower Q limit : INFEASIBLE PROBLEM\n' % mn + 1) success = 0 break ## one at a time? if qlim == 2: ## fix largest violation, ignore the rest k = argmax(r_[gen[mx, QG] - gen[mx, QMAX], gen[mn, QMIN] - gen[mn, QG]]) if k > len(mx): mn = mn[k - len(mx)] mx = [] else: mx = mx[k] mn = [] if ppopt["VERBOSE"] and len(mx) > 0: for i in range(len(mx)): logger.info('Gen ' + str(mx[i] + 1) + ' at upper Q limit, converting to PQ bus\n') if ppopt["VERBOSE"] and len(mn) > 0: for i in range(len(mn)): logger.info('Gen ' + str(mn[i] + 1) + ' at lower Q limit, converting to PQ bus\n') ## save corresponding limit values fixedQg[mx] = gen[mx, QMAX] fixedQg[mn] = gen[mn, QMIN] mx = r_[mx, mn].astype(int) ## convert to PQ bus gen[mx, QG] = fixedQg[mx] ## set Qg to binding for i in range(len( mx)): ## [one at a time, since they may be at same bus] gen[mx[i], GEN_STATUS] = 0 ## temporarily turn off gen, bi = gen[mx[i], GEN_BUS].astype(int) ## adjust load accordingly, bus[bi, [PD, QD]] = (bus[bi, [PD, QD]] - gen[mx[i], [PG, QG]]) if len(ref) > 1 and any(bus[gen[mx, GEN_BUS].astype(int), BUS_TYPE] == REF): raise ValueError('Sorry, pandapower cannot enforce Q ' 'limits for slack buses in systems ' 'with multiple slacks.') bus[gen[mx, GEN_BUS].astype(int), BUS_TYPE] = PQ ## & set bus type to PQ ## update bus index lists of each type of bus ref_temp = ref ref, pv, pq = bustypes(bus, gen) if ppopt["VERBOSE"] and ref != ref_temp: print('Bus %d is new slack bus\n' % ref) limited = r_[limited, mx].astype(int) else: break ## no more generator Q limits violated if len(limited) > 0: ## restore injections from limited gens [those at Q limits] gen[limited, QG] = fixedQg[limited] ## restore Qg value, for i in range(len( limited)): ## [one at a time, since they may be at same bus] bi = gen[limited[i], GEN_BUS].astype(int) ## re-adjust load, bus[bi, [PD, QD]] = bus[bi, [PD, QD]] + gen[limited[i], [PG, QG]] gen[limited[i], GEN_STATUS] = 1 ## and turn gen back on return ppci, success, bus, gen, branch
def _run_fbsw_dense(ppc, ppopt=None): """ DENSE version of distribution power flow solution according to [1] :References: [1] Jen-Hao Teng, "A Direct Approach for Distribution System Load Flow Solutions", IEEE Transactions on Power Delivery, vol. 18, no. 3, pp. 882-887, July 2003. :param ppc: matpower-style case data :return: results (pypower style), success (flag about PF convergence) """ # time_start = time() # path_search = nx.shortest_path # ppci = ext2int(ppc) ppci = ppc ppopt = ppoption(ppopt) baseMVA, bus, gen, branch = \ ppci["baseMVA"], ppci["bus"], ppci["gen"], ppci["branch"] nbus = bus.shape[0] # get bus index lists of each type of bus ref, pv, pq = bustypes(bus, gen) root_bus = ref[ 0] # reference bus is assumed as root bus for a radial network DLF, ppc_fbsw, buses_ordered_fbsw = bibc_bcbv_dense(ppci) baseMVA_fbsw, bus_fbsw, gen_fbsw, branch_fbsw = \ ppc_fbsw["baseMVA"], ppc_fbsw["bus"], ppc_fbsw["gen"], ppc_fbsw["branch"] time_start = time() # initialize voltages to flat start and buses with gens to their setpoints V0 = np.ones(nbus, dtype=complex) V0[gen[:, GEN_BUS].astype(int)] = gen[:, VG] Sbus_fbsw = makeSbus(baseMVA_fbsw, bus_fbsw, gen_fbsw) # update data matrices with solution Ybus_fbsw, Yf_fbsw, Yt_fbsw = makeYbus(baseMVA_fbsw, bus_fbsw, branch_fbsw) ## get bus index lists of each type of bus ref_fbsw, pv_fbsw, pq_fbsw = bustypes(bus_fbsw, gen_fbsw) ##----- run the power flow ----- # ### # LF initialization and calculation V_final, success = fbsw_dense(DLF, bus_fbsw, gen_fbsw, branch_fbsw, baseMVA_fbsw, Ybus_fbsw, Sbus_fbsw, V0, ref_fbsw, pv_fbsw, pq_fbsw, ppopt=ppopt) V_final = V_final[np.argsort( buses_ordered_fbsw)] # return bus voltages in original bus order ppci["et"] = time() - time_start Sbus = makeSbus(baseMVA, bus, gen) # update data matrices with solution Ybus, Yf, Yt = makeYbus(baseMVA, bus, branch) bus, gen, branch = pfsoln(baseMVA, bus, gen, branch, Ybus, Yf, Yt, V_final, ref, pv, pq) ppci["success"] = success ##----- output results ----- ppci["bus"], ppci["gen"], ppci["branch"] = bus, gen, branch results = ppci return results, success
def bibc_bcbv(ppc): """ performs depth-first-search bus ordering and creates Direct Load Flow (DLF) matrix which establishes direct relation between bus current injections and voltage drops from each bus to the root bus :param ppc: matpower-type case data :return: DLF matrix DLF = BIBC * BCBV where BIBC - Bus Injection to Branch-Current BCBV - Branch-Current to Bus-Voltage ppc with bfs ordering original bus names bfs ordered (used to convert voltage array back to normal) """ ppci = ppc baseMVA, bus, gen, branch = \ ppci["baseMVA"], ppci["bus"], ppci["gen"], ppci["branch"] nbus = bus.shape[0] # get bus index lists of each type of bus ref, pv, pq = bustypes(bus, gen) root_bus = ref[ 0] # reference bus is assumed as root bus for a radial network branches = branch[:, F_BUS:T_BUS + 1].real.astype(int) # creating networkx graph from list of branches G = nx.Graph() G.add_edges_from(branches) # ordering buses according to breadth-first-search (bfs) edges_ordered_bfs = list(nx.bfs_edges(G, root_bus)) indices = np.unique(np.array(edges_ordered_bfs).flatten(), return_index=True)[1] buses_ordered_bfs = np.array(edges_ordered_bfs).flatten()[sorted(indices)] buses_bfs_dict = dict(zip(buses_ordered_bfs, range(0, nbus))) # old to new bus names dictionary # renaming buses in graph and in ppc G = nx.relabel_nodes(G, buses_bfs_dict) root_bus = buses_bfs_dict[root_bus] ppc_bfs = bus_reindex(ppci, buses_bfs_dict) # ordered list of branches branches_ord = zip(ppc_bfs['branch'][:, F_BUS].real.astype(int), ppc_bfs['branch'][:, T_BUS].real.astype(int)) # searching loops in the graph if it is not a tree loops = [] branches_loops = [] if not nx.is_tree(G): # network is meshed, i.e. has loops G_bfs_tree = nx.bfs_tree(G, root_bus) branches_loops = list(set(G.edges()) - set(G_bfs_tree.edges())) G.remove_edges_from(branches_loops) # finding loops for i, j in branches_loops: G.add_edge(i, j) loops.append(nx.find_cycle(G)) G.remove_edge(i, j) nloops = len(loops) nbr_rad = len(G.edges()) # number of edges in the radial network # searching leaves of the tree succ = nx.bfs_successors(G, root_bus) leaves = set(G.nodes()) - set(succ.keys()) # dictionary with impedance values keyed by branch tuple (frombus, tobus) Z_brch_dict = dict( zip( branches_ord, ppc_bfs['branch'][:, BR_R].real + 1j * ppc_bfs['branch'][:, BR_X].real)) # #------ building BIBC and BCBV martrices ------ # order branches for BIBC and BCBV matrices and set loop-closing branches to the end branches_ord_radial = list(branches_ord) for brch in branches_loops: # TODO eliminated this for loop branches_ord_radial.remove(brch) branches_ind_dict = dict(zip(branches_ord_radial, range(0, nbr_rad))) branches_ind_dict.update( dict(zip(branches_loops, range(nbr_rad, nbr_rad + nloops)))) # add loop-closing branches rowi_BIBC = [] coli_BIBC = [] data_BIBC = [] data_BCBV = [] for bri, (i, j) in enumerate(branches_ord_radial): G.remove_edge(i, j) buses_down = set() for leaf in leaves: try: buses_down.update(nx.shortest_path(G, leaf, j)) except: pass rowi_BIBC += [bri] * len(buses_down) coli_BIBC += list(buses_down) data_BCBV += [Z_brch_dict[(i, j)]] * len(buses_down) data_BIBC += [1] * len(buses_down) G.add_edge(i, j) for loop_i, loop in enumerate(loops): loop_size = len(loop) coli_BIBC += [nbus + loop_i] * loop_size for brch in loop: if brch[0] < brch[1]: i, j = brch brch_direct = 1 data_BIBC.append(brch_direct) else: j, i = brch brch_direct = -1 data_BIBC.append(brch_direct) rowi_BIBC.append(branches_ind_dict[(i, j)]) data_BCBV.append(Z_brch_dict[(i, j)] * brch_direct) # construction of the BIBC matrix # column indices correspond to buses: assuming root bus is always 0 after ordering indices are subtracted by 1 BIBC = csr_matrix((data_BIBC, (rowi_BIBC, np.array(coli_BIBC) - 1)), shape=(nbus - 1 + nloops, nbus - 1 + nloops)) BCBV = csr_matrix( (data_BCBV, (rowi_BIBC, np.array(coli_BIBC) - 1)), shape=(nbus - 1 + nloops, nbus - 1 + nloops)).transpose() if BCBV.shape[0] > nbus - 1: # if nbrch > nbus - 1 -> network has loops DLF_loop = BCBV * BIBC # DLF = [A M.T ] # [M N ] A = DLF_loop[0:nbus - 1, 0:nbus - 1] M = DLF_loop[nbus - 1:, 0:nbus - 1] N = DLF_loop[nbus - 1:, nbus - 1:].A # considering the fact that number of loops is relatively small, N matrix is expected to be small and dense # ...in that case dense version is more efficient, i.e. N is transformed to dense and # inverted using sp.linalg.inv(N) DLF = A - M.T * csr_matrix(sp.linalg.inv(N)) * M # Kron's Reduction else: # no loops -> radial network DLF = BCBV * BIBC return DLF, ppc_bfs, buses_ordered_bfs
def bibc_bcbv_dense(ppc): """ creates 2 matrices required for direct LF: BIBC - Bus Injection to Branch-Current BCBV - Branch-Current to Bus-Voltage :param ppc: matpower-type power system dictionary :return: DLF matrix DLF = BIBC * BCBV in a dense form """ ppci = ppc baseMVA, bus, gen, branch = \ ppci["baseMVA"], ppci["bus"], ppci["gen"], ppci["branch"] nbus = bus.shape[0] nbrch = branch.shape[0] # get bus index lists of each type of bus ref, pv, pq = bustypes(bus, gen) root_bus = ref[ 0] # reference bus is assumed as root bus for a radial network branches = branch[:, F_BUS:T_BUS + 1].real.astype(int) # power system graph created from list of branches as a dictionary system_graph = graph_dict(branches) # ### # bus ordering according to BFS search # TODO check if bfs bus ordering is necessary for the direct power flow buses_ordered_bfs, edges_ordered_bfs, branches_loops = bfs_edges( system_graph, root_bus) buses_bfs_dict = dict(zip(buses_ordered_bfs, range(0, nbus))) # old to new bus names ppc_bfs = bus_reindex(ppci, buses_bfs_dict) branches_loops_bfs = [] if len(branches_loops) > 0: print( " {0} loops detected\n following branches are cut to obtain radial network: {1}" .format(len(branches_loops), branches_loops)) branches_loops_bfs = [(buses_bfs_dict[fbus], buses_bfs_dict[tbus]) for fbus, tbus in branches_loops] branches_loops_bfs = np.sort(branches_loops_bfs, axis=1) # sort in order to T_BUS > F_BUS branches_loops_bfs = list( zip(branches_loops_bfs[:, 0], branches_loops_bfs[:, 1])) buses = ppc_bfs['bus'][:, BUS_I] branches_bfs = zip(ppc_bfs['branch'][:, F_BUS], ppc_bfs['branch'][:, T_BUS]) nbus = buses.shape[0] mask_root = ~(ppc_bfs['bus'][:, BUS_TYPE] == 3) root_bus = buses[~mask_root][0] # dictionaries with bus/branch indices bus_ind_dict = dict(zip(buses[mask_root], range(nbus - 1))) # dictionary bus_name-> bus_ind bus_ind_dict[root_bus] = -1 # dictionary brch_name-> brch_ind brch_ind_dict = dict(zip(branches_bfs, range(nbrch))) # list of branches in the radial network, i.e. without branches that close loops branches_radial = list(branches_bfs) for brch_l in branches_loops_bfs: branches_radial.remove(brch_l) # reorder branches so that loop-forming branches are at the end Z_brchs = [] nloops = len(branches_loops_bfs) BIBC = np.zeros((nbrch - nloops, nbus - 1)) BCBV = np.zeros((nbus - 1, nbrch - nloops), dtype=complex) for br_i, branch in enumerate(branches_radial): bus_f = branch[0] bus_t = branch[1] bus_f_i = bus_ind_dict[bus_f] bus_t_i = bus_ind_dict[bus_t] brch_ind = brch_ind_dict[branch] if bus_f != root_bus and bus_t != root_bus: BIBC[:, bus_t_i] = BIBC[:, bus_f_i].copy() BCBV[bus_t_i, :] = BCBV[bus_f_i, :].copy() if branch[T_BUS] != root_bus: BIBC[br_i, bus_t_i] += 1 Z = (ppc_bfs['branch'][brch_ind, BR_R] + 1j * ppc_bfs['branch'][brch_ind, BR_X]) BCBV[bus_t_i, br_i] = Z Z_brchs.append(Z) for br_i, branch in enumerate(branches_loops_bfs): bus_f = branch[0] bus_t = branch[1] bus_f_i = bus_ind_dict[bus_f] bus_t_i = bus_ind_dict[bus_t] brch_ind = brch_ind_dict[branch] BIBC = np.vstack((BIBC, np.zeros(BIBC.shape[1]))) # add row BIBC = np.hstack((BIBC, np.zeros((BIBC.shape[0], 1)))) # add column last_col_i = BIBC.shape[1] - 1 # last column index BIBC[:, last_col_i] = BIBC[:, bus_f_i] - BIBC[:, bus_t_i] BIBC[last_col_i, last_col_i] += 1 BCBV = np.vstack((BCBV, np.zeros(BCBV.shape[1]))) # add row BCBV = np.hstack((BCBV, np.zeros((BCBV.shape[0], 1)))) # add column Z = (ppc_bfs['branch'][brch_ind, BR_R] + 1j * ppc_bfs['branch'][brch_ind, BR_X]) Z_brchs.append(Z) BCBV[BCBV.shape[0] - 1, :] = np.array(Z_brchs) * BIBC[:, last_col_i] # KVL for the loop if BCBV.shape[0] > nbus - 1: # if nbrch > nbus - 1 -> network has loops DLF_loop = sp.dot(BCBV, BIBC) A = DLF_loop[0:nbus - 1, 0:nbus - 1] M = DLF_loop[nbus - 1:, 0:nbus - 1] N = DLF_loop[nbus - 1:, nbus - 1:] # TODO: use more efficient inversion !? DLF = A - sp.dot(sp.dot(M.T, sp.linalg.inv(N)), M) # Krons Reduction else: # no loops -> radial network DLF = sp.dot(BCBV, BIBC) return DLF, ppc_bfs, buses_ordered_bfs
def _run_bfswpf(ppc, options, **kwargs): """ SPARSE version of distribution power flow solution according to [1] :References: [1] Jen-Hao Teng, "A Direct Approach for Distribution System Load Flow Solutions", IEEE Transactions on Power Delivery, vol. 18, no. 3, pp. 882-887, July 2003. :param ppc: matpower-style case data :return: results (pypower style), success (flag about PF convergence) """ time_start = time() # starting pf calculation timing tap_shift = ppc['branch'][:, SHIFT].real enforce_q_lims, tolerance_kva, max_iteration, calculate_voltage_angles, numba = _get_options( options) numba, makeYbus = _import_numba_extensions_if_flag_is_true(numba) ppci = ppc baseMVA, bus, gen, branch = \ ppci["baseMVA"], ppci["bus"], ppci["gen"], ppci["branch"] nbus = bus.shape[0] # generate results for original bus ordering Ybus, Yf, Yt = makeYbus(baseMVA, bus, branch) # get bus index lists of each type of bus ref, pv, pq = bustypes(bus, gen) # creating networkx graph from list of branches G = nx.Graph() G.add_edges_from((int(fb), int(tb), { "shift": float(shift) }) for fb, tb, shift in list( zip(branch[:, F_BUS].real, branch[:, T_BUS].real, tap_shift))) if not nx.is_connected(G): Graphs = list(nx.connected_component_subgraphs(G)) else: Graphs = [G] V_final = np.zeros(nbus, dtype=complex) V_tapshifts = np.zeros(nbus) for subi, G in enumerate(Graphs): ppci_sub = _cut_ppc(ppci, G.nodes()) nbus_sub = len(G) # depth-first-search bus ordering and generating Direct Load Flow matrix DLF = BCBV * BIBC DLF, ppc_bfsw, buses_ordered_bfsw = _bibc_bcbv(ppci_sub, G) ppc_bfsw['branch'][:, SHIFT] = 0 baseMVA_bfsw, bus_bfsw, gen_bfsw, branch_bfsw, ref_bfsw, pv_bfsw, pq_bfsw,\ on, gbus, V0 = _get_pf_variables_from_ppci(ppc_bfsw) Sbus_bfsw = makeSbus(baseMVA_bfsw, bus_bfsw, gen_bfsw) Ybus_bfsw, Yf_bfsw, Yt_bfsw = makeYbus(baseMVA_bfsw, bus_bfsw, branch_bfsw) # #----- run the power flow ----- V_res, success = _bfswpf(DLF, bus_bfsw, gen_bfsw, branch_bfsw, baseMVA, Ybus_bfsw, buses_ordered_bfsw, Sbus_bfsw, V0, ref_bfsw, pv_bfsw, pq_bfsw, enforce_q_lims, tolerance_kva, max_iteration, **kwargs) V_final[ buses_ordered_bfsw] = V_res # return bus voltages in original bus order # TODO: find the better way to consider transformer phase shift and remove this workaround if calculate_voltage_angles: predecessors = nx.bfs_predecessors(G, ref[subi]) branches = list(zip(branch[:, F_BUS].real, branch[:, T_BUS].real)) for bus_start in predecessors.iterkeys(): bus_pred = bus_start bus_next = bus_start while predecessors.get(bus_next) is not None: bus_next = predecessors.get(bus_pred) shift_angle = G.get_edge_data(bus_pred, bus_next)['shift'] if (bus_pred, bus_next) in branches: V_tapshifts[bus_start] += shift_angle else: V_tapshifts[bus_start] -= shift_angle bus_pred = bus_next V_final *= np.exp(1j * np.pi / 180 * V_tapshifts) # #----- output results to ppc ------ ppci["et"] = time() - time_start # pf time end bus, gen, branch = pfsoln(baseMVA, bus, gen, branch, Ybus, Yf, Yt, V_final, ref, pv, pq) ppci["success"] = success ppci["bus"], ppci["gen"], ppci["branch"] = bus, gen, branch return ppci, success
def _bfswpf(DLF, bus, gen, branch, baseMVA, Ybus, bus_ord_bfsw, Sbus, V0, ref, pv, pq, enforce_q_lims, tolerance_kva, max_iteration, **kwargs): """ distribution power flow solution according to [1] :param DLF: direct-Load-Flow matrix which relates bus current injections to voltage drops from the root bus :param bus: buses martix :param gen: generators matrix :param branch: branches matrix :param baseMVA: :param Ybus: bus admittance matrix :param Sbus: vector of power injections :param V0: initial voltage state vector :param ref: reference bus index :param pv: PV buses indices :param pq: PQ buses indices :return: power flow result :References: [1] Jen-Hao Teng, "A Direct Approach for Distribution System Load Flow Solutions", IEEE Transactions on Power Delivery, vol. 18, no. 3, pp. 882-887, July 2003. """ # setting options tolerance_mva = tolerance_kva * 1e-3 max_it = max_iteration # maximum iterations verbose = kwargs["VERBOSE"] # verbose is set in run._runpppf() # # tolerance for the inner loop for PV nodes if 'tolerance_kva_pv' in kwargs: tol_mva_inner = kwargs['tolerance_kva_pv'] * 1e-3 else: tol_mva_inner = 1.e-2 if 'max_iter_pv' in kwargs: max_iter_pv = kwargs['max_iter_pv'] else: max_iter_pv = 20 nbus = bus.shape[0] ngen = gen.shape[0] mask_root = ~(bus[:, BUS_TYPE] == 3) # mask for eliminating root bus root_bus_i = ref Vref = V0[ref] # bus_ind_mask_dict = dict(zip(bus[mask_root, BUS_I], range(nbus - 1))) bus_ord_bfsw_inv = np.argsort(bus_ord_bfsw) # compute shunt admittance # if Psh is the real power consumed by the shunt at V = 1.0 p.u. and Qsh is the reactive power injected by # the shunt at V = 1.0 p.u. then Psh - j Qsh = V * conj(Ysh * V) = conj(Ysh) = Gs - j Bs, # vector of shunt admittances Ysh = (bus[:, GS] + 1j * bus[:, BS]) / baseMVA # Line charging susceptance BR_B is also added as shunt admittance: # summation of charging susceptances per each bus stat = branch[:, BR_STATUS] ## ones at in-service branches Ys = stat / (branch[:, BR_R] + 1j * branch[:, BR_X]) ysh = (-branch[:, BR_B].imag + 1j * (branch[:, BR_B].real)) / 2 tap = branch[:, TAP] # * np.exp(1j * np.pi / 180 * branch[:, SHIFT]) ysh_f = Ys * (1 - tap) / (tap * np.conj(tap)) + ysh / (tap * np.conj(tap)) ysh_t = Ys * (tap - 1) / tap + ysh Gch = (np.bincount( branch[:, F_BUS].real.astype(int), weights=ysh_f.real, minlength=nbus) + np.bincount(branch[:, T_BUS].real.astype(int), weights=ysh_t.real, minlength=nbus)) Bch = (np.bincount( branch[:, F_BUS].real.astype(int), weights=ysh_f.imag, minlength=nbus) + np.bincount(branch[:, T_BUS].real.astype(int), weights=ysh_t.imag, minlength=nbus)) Ysh += Gch + 1j * Bch # Gch_f = - np.bincount(branch[:, F_BUS].real.astype(int), weights=branch[:, BR_B].imag / 2, minlength=nbus) # Bch_f = np.bincount(branch[:, F_BUS].real.astype(int), weights=branch[:, BR_B].real / 2, minlength=nbus) # Gch_t = - np.bincount(branch[:, T_BUS].real.astype(int), weights=branch[:, BR_B].imag / 2, minlength=nbus) # Bch_t = np.bincount(branch[:, T_BUS].real.astype(int), weights=branch[:, BR_B].real / 2, minlength=nbus) # Ysh += (Gch_f + Gch_t) + 1j * (Bch_f + Bch_t) # adding line charging to shunt impedance vector # detect generators on PV buses which have status ON gen_pv = np.in1d(gen[:, GEN_BUS], pv) & (gen[:, GEN_STATUS] > 0) qg_lim = np.zeros( ngen, dtype=bool) #initialize generators which violated Q limits V_iter = V0[mask_root].copy() # initial voltage vector without root bus V = V0.copy() Iinj = np.conj(Sbus / V) - Ysh * V # Initial current injections n_iter = 0 converged = 0 if verbose: print(' -- AC Power Flow Backward/Forward sweep\n') while not converged and n_iter < max_it: n_iter_inner = 0 n_iter += 1 deltaV = DLF * Iinj[mask_root] V_iter = np.ones(nbus - 1) * Vref + deltaV # ## # inner loop for considering PV buses inner_loop_converged = False while not inner_loop_converged and len(pv) > 0: pvi = pv - 1 # internal PV buses indices, assuming reference node is always 0 Vmis = (np.abs(gen[gen_pv, VG]))**2 - (np.abs(V_iter[pvi]))**2 dQ = (Vmis / (2 * DLF[pvi, pvi].A1.imag)).flatten() gen[gen_pv, QG] += dQ if enforce_q_lims: #check Q violation limits ## find gens with violated Q constraints qg_max_lim = (gen[:, QG] > gen[:, QMAX]) & gen_pv qg_min_lim = (gen[:, QG] < gen[:, QMIN]) & gen_pv if qg_min_lim.any(): gen[qg_min_lim, QG] = gen[qg_min_lim, QMIN] bus[gen[qg_min_lim, GEN_BUS].astype(int), BUS_TYPE] = 1 # convert to PQ bus if qg_max_lim.any(): gen[qg_max_lim, QG] = gen[qg_max_lim, QMAX] bus[gen[qg_max_lim, GEN_BUS].astype(int), BUS_TYPE] = 1 # convert to PQ bus # TODO: correct: once all the PV buses are converted to PQ buses, conversion back to PV is not possible qg_lim_new = qg_min_lim | qg_max_lim if qg_lim_new.any(): pq2pv = (qg_lim != qg_lim_new) & qg_lim # convert PQ to PV bus if pq2pv.any(): bus[gen[qg_max_lim, GEN_BUS].astype(int), BUS_TYPE] = 2 # convert to PV bus qg_lim = qg_lim_new.copy() ref, pv, pq = bustypes(bus, gen) Sbus = makeSbus(baseMVA, bus, gen) V = np.insert(V_iter, root_bus_i, Vref) Iinj = np.conj(Sbus / V) - Ysh * V deltaV = DLF * Iinj[mask_root] V_iter = np.ones(nbus - 1) * V0[root_bus_i] + deltaV if n_iter_inner > max_iter_pv: raise LoadflowNotConverged( " FBSW Power Flow did not converge - inner iterations for PV nodes " "reached maximum value of {0}!".format(max_iter_pv)) n_iter_inner += 1 if np.all(np.abs(dQ) < tol_mva_inner ): # inner loop termination criterion inner_loop_converged = True # testing termination criterion - V = np.insert(V_iter, root_bus_i, Vref) mis = V * np.conj(Ybus * V) - Sbus F = np.r_[mis[pv].real, mis[pq].real, mis[pq].imag] # check tolerance normF = np.linalg.norm(F, np.Inf) if normF < tolerance_mva: converged = 1 if verbose: print("\nFwd-back sweep power flow converged in " "{0} iterations.\n".format(n_iter)) elif n_iter == max_it: raise LoadflowNotConverged( " FBSW Power Flow did not converge - " "reached maximum iterations = {0}!".format(max_it)) # updating injected currents Iinj = np.conj(Sbus / V) - Ysh * V return V, converged
def _runpf(casedata=None, init='flat', ac=True, Numba=True, ppopt=None): """Runs a power flow. Similar to runpf() from pypower. See Pypower documentation for more information. Changes by University of Kassel (Florian Schaefer): Numba can be used for pf calculations. Changes in structure (AC as well as DC PF can be calculated) """ ## default arguments if casedata is None: casedata = join(dirname(__file__), 'case9') ppopt = ppoption(ppopt) ## options verbose = ppopt["VERBOSE"] ## read data ppci = loadcase(casedata) # get data for calc baseMVA, bus, gen, branch = \ ppci["baseMVA"], ppci["bus"], ppci["gen"], ppci["branch"] ## get bus index lists of each type of bus ref, pv, pq = bustypes(bus, gen) ## generator info on = find(gen[:, GEN_STATUS] > 0) ## which generators are on? gbus = gen[on, GEN_BUS].astype(int) ## what buses are they at? ##----- run the power flow ----- t0 = time() if not ac or (ac and init == 'dc'): # DC formulation if verbose: print(' -- DC Power Flow\n') ## initial state Va0 = bus[:, VA] * (pi / 180) ## build B matrices and phase shift injections B, Bf, Pbusinj, Pfinj = makeBdc(baseMVA, bus, branch) ## compute complex bus power injections [generation - load] ## adjusted for phase shifters and real shunts Pbus = makeSbus(baseMVA, bus, gen) - Pbusinj - bus[:, GS] / baseMVA ## "run" the power flow Va = dcpf(B, Pbus, Va0, ref, pv, pq) ## update data matrices with solution branch[:, [QF, QT]] = zeros((branch.shape[0], 2)) branch[:, PF] = (Bf * Va + Pfinj) * baseMVA branch[:, PT] = -branch[:, PF] bus[:, VM] = ones(bus.shape[0]) bus[:, VA] = Va * (180 / pi) ## update Pg for slack generator (1st gen at ref bus) ## (note: other gens at ref bus are accounted for in Pbus) ## Pg = Pinj + Pload + Gs ## newPg = oldPg + newPinj - oldPinj refgen = zeros(len(ref), dtype=int) for k in range(len(ref)): temp = find(gbus == ref[k]) refgen[k] = on[temp[0]] gen[refgen, PG] = gen[refgen, PG] + (B[ref, :] * Va - Pbus[ref]) * baseMVA success = 1 if ac and init == 'dc': # get results from DC powerflow for AC powerflow ppci["bus"], ppci["gen"], ppci["branch"] = bus, gen, branch if ac: ## AC formulation # options qlim = ppopt["ENFORCE_Q_LIMS"] ## enforce Q limits on gens? ## check if numba is available and the corresponding flag try: from numba import _version as nb_version # get Numba Version (in order to use it it must be > 0.25) nbVersion = float(nb_version.version_version[:4]) if nbVersion < 0.25: print( 'Warning: Numba version too old -> Upgrade to a version > 0.25. Numba is disabled\n' ) Numba = False except ImportError: # raise UserWarning('Numba cannot be imported. Call runpp() with Numba=False!') print( 'Warning: Numba cannot be imported. Numba is disabled. Call runpp() with Numba=False!\n' ) Numba = False if Numba: from pandapower.pypower_extensions.makeYbus import makeYbus else: from pypower.makeYbus import makeYbus alg = ppopt['PF_ALG'] if verbose > 0: if alg == 1: solver = 'Newton' elif alg == 2: solver = 'fast-decoupled, XB' elif alg == 3: solver = 'fast-decoupled, BX' elif alg == 4: solver = 'Gauss-Seidel' else: solver = 'unknown' print(' -- AC Power Flow (%s)\n' % solver) ## initial state # V0 = ones(bus.shape[0]) ## flat start V0 = bus[:, VM] * exp(1j * pi / 180 * bus[:, VA]) V0[gbus] = gen[on, VG] / abs(V0[gbus]) * V0[gbus] if qlim: ref0 = ref ## save index and angle of Varef0 = bus[ref0, VA] ## original reference bus(es) limited = [] ## list of indices of gens @ Q lims fixedQg = zeros(gen.shape[0]) ## Qg of gens at Q limits repeat = True while repeat: ## build admittance matrices Ybus, Yf, Yt = makeYbus(baseMVA, bus, branch) ## compute complex bus power injections [generation - load] Sbus = makeSbus(baseMVA, bus, gen) ## run the power flow alg = ppopt["PF_ALG"] if alg == 1: V, success, _ = newtonpf(Ybus, Sbus, V0, ref, pv, pq, ppopt, Numba) elif alg == 2 or alg == 3: Bp, Bpp = makeB(baseMVA, bus, branch, alg) V, success, _ = fdpf(Ybus, Sbus, V0, Bp, Bpp, ref, pv, pq, ppopt) elif alg == 4: V, success, _ = gausspf(Ybus, Sbus, V0, ref, pv, pq, ppopt) else: raise ValueError( 'Only Newton' 's method, fast-decoupled, and ' 'Gauss-Seidel power flow algorithms currently ' 'implemented.\n') ## update data matrices with solution bus, gen, branch = pfsoln(baseMVA, bus, gen, branch, Ybus, Yf, Yt, V, ref, pv, pq) if qlim: ## enforce generator Q limits ## find gens with violated Q constraints gen_status = gen[:, GEN_STATUS] > 0 qg_max_lim = gen[:, QG] > gen[:, QMAX] qg_min_lim = gen[:, QG] < gen[:, QMIN] mx = find(gen_status & qg_max_lim) mn = find(gen_status & qg_min_lim) if len(mx) > 0 or len( mn) > 0: ## we have some Q limit violations # No PV generators if len(pv) == 0: if verbose: if len(mx) > 0: print( 'Gen %d [only one left] exceeds upper Q limit : INFEASIBLE PROBLEM\n' % mx + 1) else: print( 'Gen %d [only one left] exceeds lower Q limit : INFEASIBLE PROBLEM\n' % mn + 1) success = 0 break ## one at a time? if qlim == 2: ## fix largest violation, ignore the rest k = argmax(r_[gen[mx, QG] - gen[mx, QMAX], gen[mn, QMIN] - gen[mn, QG]]) if k > len(mx): mn = mn[k - len(mx)] mx = [] else: mx = mx[k] mn = [] if verbose and len(mx) > 0: for i in range(len(mx)): print('Gen ' + str(mx[i] + 1) + ' at upper Q limit, converting to PQ bus\n') if verbose and len(mn) > 0: for i in range(len(mn)): print('Gen ' + str(mn[i] + 1) + ' at lower Q limit, converting to PQ bus\n') ## save corresponding limit values fixedQg[mx] = gen[mx, QMAX] fixedQg[mn] = gen[mn, QMIN] mx = r_[mx, mn].astype(int) ## convert to PQ bus gen[mx, QG] = fixedQg[mx] ## set Qg to binding for i in range( len(mx) ): ## [one at a time, since they may be at same bus] gen[mx[i], GEN_STATUS] = 0 ## temporarily turn off gen, bi = gen[mx[i], GEN_BUS] ## adjust load accordingly, bus[bi, [PD, QD]] = (bus[bi, [PD, QD]] - gen[mx[i], [PG, QG]]) if len(ref) > 1 and any(bus[gen[mx, GEN_BUS].astype(int), BUS_TYPE] == REF): raise ValueError('Sorry, PYPOWER cannot enforce Q ' 'limits for slack buses in systems ' 'with multiple slacks.') bus[gen[mx, GEN_BUS].astype(int), BUS_TYPE] = PQ ## & set bus type to PQ ## update bus index lists of each type of bus ref_temp = ref ref, pv, pq = bustypes(bus, gen) if verbose and ref != ref_temp: print('Bus %d is new slack bus\n' % ref) limited = r_[limited, mx].astype(int) else: repeat = 0 ## no more generator Q limits violated else: repeat = 0 ## don't enforce generator Q limits, once is enough if qlim and len(limited) > 0: ## restore injections from limited gens [those at Q limits] gen[limited, QG] = fixedQg[limited] ## restore Qg value, for i in range( len(limited )): ## [one at a time, since they may be at same bus] bi = gen[limited[i], GEN_BUS] ## re-adjust load, bus[bi, [PD, QD]] = bus[bi, [PD, QD]] + gen[limited[i], [PG, QG]] gen[limited[i], GEN_STATUS] = 1 ## and turn gen back on # if ref != ref0: # ## adjust voltage angles to make original ref bus correct # bus[:, VA] = bus[:, VA] - bus[ref0, VA] + Varef0 ppci["et"] = time() - t0 ppci["success"] = success ##----- output results ----- ppci["bus"], ppci["gen"], ppci["branch"] = bus, gen, branch results = ppci return results, success
def _bfswpf(DLF, bus, gen, branch, baseMVA, Ybus, Sbus, V0, ref, pv, pq, buses_ordered_bfs_nets, enforce_q_lims, tolerance_kva, max_iteration, **kwargs): """ distribution power flow solution according to [1] :param DLF: direct-Load-Flow matrix which relates bus current injections to voltage drops from the root bus :param bus: buses martix :param gen: generators matrix :param branch: branches matrix :param baseMVA: :param Ybus: bus admittance matrix :param Sbus: vector of power injections :param V0: initial voltage state vector :param ref: reference bus index :param pv: PV buses indices :param pq: PQ buses indices :return: power flow result """ # setting options tolerance_mva = tolerance_kva * 1e-3 max_it = max_iteration # maximum iterations verbose = kwargs["VERBOSE"] # verbose is set in run._runpppf() # # tolerance for the inner loop for PV nodes if 'tolerance_kva_pv' in kwargs: tol_mva_inner = kwargs['tolerance_kva_pv'] * 1e-3 else: tol_mva_inner = 1.e-2 if 'max_iter_pv' in kwargs: max_iter_pv = kwargs['max_iter_pv'] else: max_iter_pv = 20 nobus = bus.shape[0] ngen = gen.shape[0] mask_root = ~(bus[:, BUS_TYPE] == 3) # mask for eliminating root bus norefs = len(ref) # compute shunt admittance # if Psh is the real power consumed by the shunt at V = 1.0 p.u. and Qsh is the reactive power injected by # the shunt at V = 1.0 p.u. then Psh - j Qsh = V * conj(Ysh * V) = conj(Ysh) = Gs - j Bs, # vector of shunt admittances Ysh = (bus[:, GS] + 1j * bus[:, BS]) / baseMVA # Line charging susceptance BR_B is also added as shunt admittance: # summation of charging susceptances per each bus stat = branch[:, BR_STATUS] ## ones at in-service branches Ys = stat / (branch[:, BR_R] + 1j * branch[:, BR_X]) ysh = (-branch[:, BR_B].imag + 1j * (branch[:, BR_B].real)) / 2 tap = branch[:, TAP] # * np.exp(1j * np.pi / 180 * branch[:, SHIFT]) ysh_f = Ys * (1 - tap) / (tap * np.conj(tap)) + ysh / (tap * np.conj(tap)) ysh_t = Ys * (tap - 1) / tap + ysh Gch = (np.bincount( branch[:, F_BUS].real.astype(int), weights=ysh_f.real, minlength=nobus) + np.bincount(branch[:, T_BUS].real.astype(int), weights=ysh_t.real, minlength=nobus)) Bch = (np.bincount( branch[:, F_BUS].real.astype(int), weights=ysh_f.imag, minlength=nobus) + np.bincount(branch[:, T_BUS].real.astype(int), weights=ysh_t.imag, minlength=nobus)) Ysh += Gch + 1j * Bch # detect generators on PV buses which have status ON gen_pv = np.in1d(gen[:, GEN_BUS], pv) & (gen[:, GEN_STATUS] > 0) qg_lim = np.zeros( ngen, dtype=bool) #initialize generators which violated Q limits Iinj = np.conj(Sbus / V0) - Ysh * V0 # Initial current injections # initiate reference voltage vector V_ref = np.ones(nobus, dtype=complex) for neti, buses_ordered_bfs in enumerate(buses_ordered_bfs_nets): V_ref[buses_ordered_bfs] *= V0[ref[neti]] V = V0.copy() n_iter = 0 converged = 0 if verbose: print(' -- AC Power Flow (Backward/Forward sweep)\n') while not converged and n_iter < max_it: n_iter_inner = 0 n_iter += 1 deltaV = DLF * Iinj[mask_root] V[mask_root] = V_ref[mask_root] + deltaV # ## # inner loop for considering PV buses # TODO improve PV buses inner loop inner_loop_converged = False while not inner_loop_converged and len(pv) > 0: pvi = pv - norefs # internal PV buses indices, assuming reference node is always 0 Vmis = (np.abs(gen[gen_pv, VG]))**2 - (np.abs(V[pv]))**2 # TODO improve getting values from sparse DLF matrix - DLF[pvi, pvi] is unefficient dQ = (Vmis / (2 * DLF[pvi, pvi].A1.imag)).flatten() gen[gen_pv, QG] += dQ if enforce_q_lims: #check Q violation limits ## find gens with violated Q constraints qg_max_lim = (gen[:, QG] > gen[:, QMAX]) & gen_pv qg_min_lim = (gen[:, QG] < gen[:, QMIN]) & gen_pv if qg_min_lim.any(): gen[qg_min_lim, QG] = gen[qg_min_lim, QMIN] bus[gen[qg_min_lim, GEN_BUS].astype(int), BUS_TYPE] = 1 # convert to PQ bus if qg_max_lim.any(): gen[qg_max_lim, QG] = gen[qg_max_lim, QMAX] bus[gen[qg_max_lim, GEN_BUS].astype(int), BUS_TYPE] = 1 # convert to PQ bus # TODO: correct: once all the PV buses are converted to PQ buses, conversion back to PV is not possible qg_lim_new = qg_min_lim | qg_max_lim if qg_lim_new.any(): pq2pv = (qg_lim != qg_lim_new) & qg_lim # convert PQ to PV bus if pq2pv.any(): bus[gen[qg_max_lim, GEN_BUS].astype(int), BUS_TYPE] = 2 # convert to PV bus qg_lim = qg_lim_new.copy() ref, pv, pq = bustypes(bus, gen) # avoid calling makeSbus, update only Sbus for pv nodes Sbus = makeSbus(baseMVA, bus, gen) Iinj = np.conj(Sbus / V) - Ysh * V deltaV = DLF * Iinj[mask_root] V[mask_root] = V_ref[mask_root] + deltaV if n_iter_inner > max_iter_pv: raise LoadflowNotConverged( " FBSW Power Flow did not converge - inner iterations for PV nodes " "reached maximum value of {0}!".format(max_iter_pv)) n_iter_inner += 1 if np.all(np.abs(dQ) < tol_mva_inner ): # inner loop termination criterion inner_loop_converged = True # testing termination criterion - mis = V * np.conj(Ybus * V) - Sbus F = np.r_[mis[pv].real, mis[pq].real, mis[pq].imag] # check tolerance normF = np.linalg.norm(F, np.Inf) if normF < tolerance_mva: converged = 1 if verbose: print("\nFwd-back sweep power flow converged in " "{0} iterations.\n".format(n_iter)) elif n_iter == max_it: raise LoadflowNotConverged( " FBSW Power Flow did not converge - " "reached maximum iterations = {0}!".format(max_it)) # updating injected currents Iinj = np.conj(Sbus / V) - Ysh * V return V, converged