def mu(Ybus, Ibus, J, incS, dV, dx, pvpq, pq, npv, npq): """ Calculate the Iwamoto acceleration parameter as described in: "A Load Flow Calculation Method for Ill-Conditioned Power Systems" by Iwamoto, S. and Tamura, Y." Args: Ybus: Admittance matrix J: Jacobian matrix incS: mismatch vector dV: voltage increment (in complex form) dx: solution vector as calculated dx = solve(J, incS) pvpq: array of the pq and pv indices pq: array of the pq indices Returns: the Iwamoto's optimal multiplier for ill conditioned systems """ # evaluate the Jacobian of the voltage derivative # theoretically this is the second derivative matrix # since the Jacobian (J2) has been calculated with dV instead of V # generate lookup pvpq -> index pvpq (used in createJ) pvpq_lookup = np.zeros(np.max(Ybus.indices) + 1, dtype=int) pvpq_lookup[pvpq] = np.arange(len(pvpq)) createJ = get_fastest_jacobian_function(pvpq, pq) J2 = _create_J_with_numba(Ybus, dV, pvpq, pq, createJ, pvpq_lookup, npv, npq) # J2 = Jacobian(Ybus, dV, Ibus, pq, pvpq) a = incS b = J * dx c = 0.5 * dx * J2 * dx g0 = -a.dot(b) g1 = b.dot(b) + 2 * a.dot(c) g2 = -3.0 * b.dot(c) g3 = 2.0 * c.dot(c) roots = np.roots([g3, g2, g1, g0]) # three solutions are provided, the first two are complex, only the real solution is valid return roots[2].real
def levenberg_marquardt_pf(Ybus, Sbus, V0, Ibus, pv, pq, tol, max_it=50): """ Solves the power flow problem by the Levenberg-Marquardt power flow algorithm. It is usually better than Newton-Raphson, but it takes an order of magnitude more time to converge. Args: Ybus: Admittance matrix Sbus: Array of nodal power injections V0: Array of nodal voltages (initial solution) Ibus: Array of nodal current injections pv: Array with the indices of the PV buses pq: Array with the indices of the PQ buses tol: Tolerance max_it: Maximum number of iterations Returns: Voltage solution, converged?, error, calculated power injections @Author: Santiago Peñate Vera """ start = time.time() # initialize V = V0 Va = np.angle(V) Vm = np.abs(V) dVa = np.zeros_like(Va) dVm = np.zeros_like(Vm) # set up indexing for updating V pvpq = np.r_[pv, pq] npv = len(pv) npq = len(pq) # j1:j2 - V angle of pv and pq buses j1 = 0 j2 = npv + npq # j2:j3 - V mag of pq buses j3 = j2 + npq if (npq + npv) > 0: normF = 100000 update_jacobian = True converged = False iter_ = 0 nu = 2.0 lbmda = 0 f_prev = 1e9 # very large number nn = 2 * npq + npv ii = np.linspace(0, nn-1, nn) Idn = sparse((np.ones(nn), (ii, ii)), shape=(nn, nn)) # csc_matrix identity # generate lookup pvpq -> index pvpq (used in createJ) pvpq_lookup = np.zeros(np.max(Ybus.indices) + 1, dtype=int) pvpq_lookup[pvpq] = np.arange(len(pvpq)) createJ = get_fastest_jacobian_function(pvpq, pq) while not converged and iter_ < max_it: # evaluate Jacobian if update_jacobian: H = _create_J_with_numba(Ybus, V, pvpq, pq, createJ, pvpq_lookup, npv, npq) # H = Jacobian(Ybus, V, Ibus, pq, pvpq) # evaluate the solution error F(x0) Scalc = V * np.conj(Ybus * V - Ibus) mis = Scalc - Sbus # compute the mismatch dz = np.r_[mis[pvpq].real, mis[pq].imag] # mismatch in the Jacobian order # system matrix # H1 = H^t H1 = H.transpose().tocsr() # H2 = H1·H H2 = H1.dot(H) # set first value of lmbda if iter_ == 0: lbmda = 1e-3 * H2.diagonal().max() # compute system matrix A = H^T·H - lambda·I A = H2 + lbmda * Idn # right hand side # H^t·dz rhs = H1.dot(dz) # Solve the increment dx = linear_solver(A, rhs) # objective function to minimize f = 0.5 * dz.dot(dz) # decision function val = dx.dot(lbmda * dx + rhs) if val > 0.0: rho = (f_prev - f) / (0.5 * val) else: rho = -1.0 # lambda update if rho >= 0: update_jacobian = True lbmda *= max([1.0 / 3.0, 1 - (2 * rho - 1) ** 3]) nu = 2.0 # reassign the solution vector dVa[pvpq] = dx[j1:j2] dVm[pq] = dx[j2:j3] # update Vm and Va again in case we wrapped around with a negative Vm Vm -= dVm Va -= dVa V = Vm * np.exp(1.0j * Va) else: update_jacobian = False lbmda *= nu nu *= 2.0 # check convergence Scalc = V * np.conj(Ybus.dot(V)) ds = Sbus - Scalc e = np.r_[ds[pvpq].real, ds[pq].imag] normF = 0.5 * np.dot(e, e) converged = normF < tol f_prev = f # update iteration counter iter_ += 1 else: normF = 0 converged = True Scalc = Sbus # V * np.conj(Ybus * V - Ibus) iter_ = 0 end = time.time() elapsed = end - start return V, converged, normF, Scalc, iter_, elapsed
def IwamotoNR(Ybus, Sbus, V0, Ibus, pv, pq, tol, max_it=15, robust=False): """ Solves the power flow using a full Newton's method with the Iwamoto optimal step factor. Args: Ybus: Admittance matrix Sbus: Array of nodal power injections V0: Array of nodal voltages (initial solution) Ibus: Array of nodal current injections pv: Array with the indices of the PV buses pq: Array with the indices of the PQ buses tol: Tolerance max_it: Maximum number of iterations robust: Boolean variable for the use of the Iwamoto optimal step factor. Returns: Voltage solution, converged?, error, calculated power injections @author: Ray Zimmerman (PSERC Cornell) @Author: Santiago Penate Vera """ start = time.time() # initialize converged = 0 iter_ = 0 V = V0 Va = np.angle(V) Vm = np.abs(V) dVa = np.zeros_like(Va) dVm = np.zeros_like(Vm) # set up indexing for updating V pvpq = np.r_[pv, pq] npv = len(pv) npq = len(pq) # j1:j2 - V angle of pv buses j1 = 0 j2 = npv + npq # j2:j3 - V mag of pq buses j3 = j2 + npq if (npq + npv) > 0: # generate lookup pvpq -> index pvpq (used in createJ) pvpq_lookup = np.zeros(np.max(Ybus.indices) + 1, dtype=int) pvpq_lookup[pvpq] = np.arange(len(pvpq)) createJ = get_fastest_jacobian_function(pvpq, pq) # evaluate F(x0) Scalc = V * np.conj(Ybus * V - Ibus) mis = Scalc - Sbus # compute the mismatch f = np.r_[mis[pvpq].real, mis[pq].imag] # check tolerance norm_f = np.linalg.norm(f, np.Inf) if norm_f < tol: converged = 1 # do Newton iterations while not converged and iter_ < max_it: # update iteration counter iter_ += 1 # evaluate Jacobian # J = Jacobian(Ybus, V, Ibus, pq, pvpq) J = _create_J_with_numba(Ybus, V, pvpq, pq, createJ, pvpq_lookup, npv, npq) # compute update step try: dx = linear_solver(J, f) except: print(J) converged = False iter_ = max_it + 1 # exit condition # reassign the solution vector dVa[pvpq] = dx[j1:j2] dVm[pq] = dx[j2:j3] dV = dVm * np.exp(1j * dVa) # voltage mismatch # update voltage if robust: # if dV contains zeros will crash the second Jacobian derivative if not (dV == 0.0).any(): # calculate the optimal multiplier for enhanced convergence mu_ = mu(Ybus, Ibus, J, f, dV, dx, pvpq, pq, npv, npq) else: mu_ = 1.0 else: mu_ = 1.0 Vm -= mu_ * dVm Va -= mu_ * dVa V = Vm * np.exp(1j * Va) Vm = np.abs(V) # update Vm and Va again in case Va = np.angle(V) # we wrapped around with a negative Vm # evaluate F(x) Scalc = V * np.conj(Ybus * V - Ibus) mis = Scalc - Sbus # complex power mismatch f = np.r_[mis[pvpq].real, mis[pq].imag] # concatenate again # check for convergence norm_f = np.linalg.norm(f, np.Inf) if norm_f < tol: converged = 1 else: norm_f = 0 converged = True Scalc = Sbus end = time.time() elapsed = end - start return V, converged, norm_f, Scalc, iter_, elapsed
def NR_LS(Ybus, Sbus, V0, Ibus, pv, pq, tol, max_it=15, acceleration_parameter=0.05, error_registry=None): """ Solves the power flow using a full Newton's method with backtrack correction. @Author: Santiago Peñate Vera :param Ybus: Admittance matrix :param Sbus: Array of nodal power injections :param V0: Array of nodal voltages (initial solution) :param Ibus: Array of nodal current injections :param pv: Array with the indices of the PV buses :param pq: Array with the indices of the PQ buses :param tol: Tolerance :param max_it: Maximum number of iterations :param acceleration_parameter: parameter used to correct the "bad" iterations, should be be between 1e-3 ~ 0.5 :param error_registry: list to store the error for plotting :return: Voltage solution, converged?, error, calculated power injections """ start = time.time() # initialize back_track_counter = 0 back_track_iterations = 0 converged = 0 iter_ = 0 V = V0 Va = np.angle(V) Vm = np.abs(V) dVa = np.zeros_like(Va) dVm = np.zeros_like(Vm) # set up indexing for updating V pvpq = np.r_[pv, pq] npv = len(pv) npq = len(pq) # j1:j2 - V angle of pv and pq buses j1 = 0 j2 = npv + npq # j2:j3 - V mag of pq buses j3 = j2 + npq if (npq + npv) > 0: # generate lookup pvpq -> index pvpq (used in createJ) pvpq_lookup = np.zeros(np.max(Ybus.indices) + 1, dtype=int) pvpq_lookup[pvpq] = np.arange(len(pvpq)) createJ = get_fastest_jacobian_function(pvpq, pq) # evaluate F(x0) Scalc = V * np.conj(Ybus * V - Ibus) dS = Scalc - Sbus # compute the mismatch f = np.r_[dS[pvpq].real, dS[pq].imag] # check tolerance norm_f = 0.5 * f.dot(f) if error_registry is not None: error_registry.append(norm_f) if norm_f < tol: converged = 1 # do Newton iterations while not converged and iter_ < max_it: # update iteration counter iter_ += 1 # evaluate Jacobian # J = Jacobian(Ybus, V, Ibus, pq, pvpq) J = _create_J_with_numba(Ybus, V, pvpq, pq, createJ, pvpq_lookup, npv, npq) # compute update step dx = linear_solver(J, f) # reassign the solution vector dVa[pvpq] = dx[j1:j2] dVm[pq] = dx[j2:j3] # update voltage the Newton way (mu=1) mu_ = 1.0 Vm -= mu_ * dVm Va -= mu_ * dVa Vnew = Vm * np.exp(1.0j * Va) # compute the mismatch function f(x_new) Scalc = Vnew * np.conj(Ybus * Vnew - Ibus) dS = Scalc - Sbus # complex power mismatch f_new = np.r_[dS[pvpq].real, dS[pq].imag] # concatenate to form the mismatch function norm_f_new = 0.5 * f_new.dot(f_new) if error_registry is not None: error_registry.append(norm_f_new) cond = norm_f_new > norm_f # condition to back track (no improvement at all) if not cond: back_track_counter += 1 l_iter = 0 while not cond and l_iter < 10 and mu_ > 0.01: # line search back # update voltage with a closer value to the last value in the Jacobian direction mu_ *= acceleration_parameter Vm -= mu_ * dVm Va -= mu_ * dVa Vnew = Vm * np.exp(1.0j * Va) # compute the mismatch function f(x_new) Scalc = Vnew * np.conj(Ybus * Vnew - Ibus) dS = Scalc - Sbus # complex power mismatch f_new = np.r_[dS[pvpq].real, dS[pq].imag] # concatenate to form the mismatch function norm_f_new = 0.5 * f_new.dot(f_new) cond = norm_f_new > norm_f if error_registry is not None: error_registry.append(norm_f_new) l_iter += 1 back_track_iterations += 1 # update calculation variables V = Vnew f = f_new # check for convergence if l_iter == 0: # no correction loop executed, hence compute the error fresh norm_f = 0.5 * f_new.dot(f_new) else: # pick the latest computer error in the correction loop norm_f = norm_f_new if error_registry is not None: error_registry.append(norm_f) if norm_f < tol: converged = 1 else: norm_f = 0 converged = True Scalc = Sbus end = time.time() elapsed = end - start return V, converged, norm_f, Scalc, iter_, elapsed