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 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 corrector(Ybus, Ibus, Sbus, V0, pv, pq, lam0, Sxfr, Vprv, lamprv, z, step, parametrization, tol, max_it, pvpq_lookup, verbose, mu_0=1.0, acceleration_parameter=0.5): """ Solves the corrector step of a continuation power flow using a full Newton method with selected parametrization scheme. solves for bus voltages and lambda given the full system admittance matrix (for all buses), the complex bus power injection vector (for all buses), the initial vector of complex bus voltages, and column vectors with the lists of bus indices for the swing bus, PV buses, and PQ buses, respectively. The bus voltage vector contains the set point for generator (including ref bus) buses, and the reference angle of the swing bus, as well as an initial guess for remaining magnitudes and angles. Uses default options if this parameter is not given. Returns the final complex voltages, a flag which indicates whether it converged or not, the number of iterations performed, and the final lambda. :param Ybus: Admittance matrix (CSC sparse) :param Ibus: Bus current injections :param Sbus: Bus power injections :param V0: Bus initial voltages :param pv: list of pv nodes :param pq: list of pq nodes :param lam0: initial value of lambda (loading parameter) :param Sxfr: [delP+j*delQ] transfer/loading vector for all buses :param Vprv: final complex V corrector solution from previous continuation step :param lamprv: final lambda corrector solution from previous continuation step :param z: normalized predictor for all buses :param step: continuation step size :param parametrization: :param tol: Tolerance (p.u.) :param max_it: max iterations :param verbose: print information? :return: Voltage, converged, iterations, lambda, power error, calculated power """ # initialize i = 0 V = V0 Va = np.angle(V) Vm = np.abs(V) lam = lam0 # set lam to initial lam0 dVa = np.zeros_like(Va) dVm = np.zeros_like(Vm) dlam = 0 # set up indexing for updating V npv = len(pv) npq = len(pq) pvpq = np.r_[pv, pq] nj = npv + npq * 2 # j1:j2 - V angle of pv and pq buses j1 = 0 j2 = npv + npq # j2:j3 - V mag of pq buses j3 = j2 + npq # evaluate F(x0, lam0), including Sxfr transfer/loading Scalc = V * np.conj(Ybus * V) mismatch = Scalc - Sbus - lam * Sxfr # F = np.r_[mismatch[pvpq].real, mismatch[pq].imag] # evaluate P(x0, lambda0) P = cpf_p(parametrization, step, z, V, lam, Vprv, lamprv, pv, pq, pvpq) # augment F(x,lambda) with P(x,lambda) F = np.r_[mismatch[pvpq].real, mismatch[pq].imag, P] # check tolerance normF = np.linalg.norm(F, np.Inf) converged = normF < tol if verbose: print('\nConverged!\n') dF_dlam = -np.r_[Sxfr[pvpq].real, Sxfr[pq].imag] # do Newton iterations while not converged and i < max_it: # update iteration counter i += 1 # evaluate Jacobian J = _create_J_with_numba(Ybus, V, pvpq, pq, pvpq_lookup, npv, npq) dP_dV, dP_dlam = cpf_p_jac(parametrization, z, V, lam, Vprv, lamprv, pv, pq, pvpq) # augment J with real/imag - Sxfr and z^T ''' J = [ J dF_dlam dP_dV dP_dlam ] ''' J = vstack( [hstack([J, dF_dlam.reshape(nj, 1)]), hstack([dP_dV, dP_dlam])], format="csc") # compute update step dx = spsolve(J, F) dVa[pvpq] = dx[j1:j2] dVm[pq] = dx[j2:j3] dlam = dx[j3] # set the restoration values prev_Vm = Vm.copy() prev_Va = Va.copy() prev_lam = lam # set the values and correct with an adaptive mu if needed mu = mu_0 # ideally 1.0 back_track_condition = True l_iter = 0 normF_new = 0.0 while back_track_condition and l_iter < max_it and mu > tol: # restore the previous values if we are backtracking (the first iteration is the normal NR procedure) if l_iter > 0: Va = prev_Va.copy() Vm = prev_Vm.copy() lam = prev_lam # update the variables from the solution Va -= mu * dVa Vm -= mu * dVm lam -= mu * dlam # update Vm and Va again in case we wrapped around with a negative Vm V = Vm * np.exp(1j * Va) # evaluate F(x, lam) Scalc = V * np.conj(Ybus * V) mismatch = Scalc - Sbus - lam * Sxfr # evaluate the parametrization function P(x, lambda) P = cpf_p(parametrization, step, z, V, lam, Vprv, lamprv, pv, pq, pvpq) # compose the mismatch vector F = np.r_[mismatch[pvpq].real, mismatch[pq].imag, P] # check for convergence normF_new = np.linalg.norm(F, np.Inf) back_track_condition = normF_new > normF mu *= acceleration_parameter l_iter += 1 if verbose: print('\n#3d #10.3e', i, normF) if l_iter > 1 and back_track_condition: # this means that not even the backtracking was able to correct the solution so, restore and end Va = prev_Va.copy() Vm = prev_Vm.copy() V = Vm * np.exp(1.0j * Va) return V, converged, i, lam, normF, Scalc else: normF = normF_new converged = normF < tol if verbose: print('\nNewton' 's method corrector converged in ', i, ' iterations.\n') if verbose: if not converged: print('\nNewton method corrector did not converge in ', i, ' iterations.\n') return V, converged, i, lam, normF, Scalc
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
def predictor(V, Ibus, lam, Ybus, Sxfr, pv, pq, step, z, Vprv, lamprv, parametrization: CpfParametrization, pvpq_lookup): """ Computes a prediction (approximation) to the next solution of the continuation power flow using a normalized tangent predictor. :param V: complex bus voltage vector at current solution :param Ibus: :param lam: scalar lambda value at current solution :param Ybus: complex bus admittance matrix :param Sxfr: complex vector of scheduled transfers (difference between bus injections in base and target cases) :param pv: vector of indices of PV buses :param pq: vector of indices of PQ buses :param step: continuation step length :param z: normalized tangent prediction vector from previous step :param Vprv: complex bus voltage vector at previous solution :param lamprv: scalar lambda value at previous solution :param parametrization: Value of cpf parametrization option. :return: V0 : predicted complex bus voltage vector LAM0 : predicted lambda continuation parameter Z : the normalized tangent prediction vector """ # sizes nb = len(V) npv = len(pv) npq = len(pq) pvpq = np.r_[pv, pq] nj = npv + npq * 2 # compute Jacobian for the power flow equations J = _create_J_with_numba(Ybus, V, pvpq, pq, pvpq_lookup, npv, npq) dF_dlam = -np.r_[Sxfr[pvpq].real, Sxfr[pq].imag] dP_dV, dP_dlam = cpf_p_jac(parametrization, z, V, lam, Vprv, lamprv, pv, pq, pvpq) # linear operator for computing the tangent predictor ''' J2 = [ J dF_dlam dP_dV dP_dlam ] ''' J2 = vstack( [hstack([J, dF_dlam.reshape(nj, 1)]), hstack([dP_dV, dP_dlam])], format="csc") Va_prev = np.angle(V) Vm_prev = np.abs(V) # compute normalized tangent predictor s = np.zeros(npv + 2 * npq + 1) # increase in the direction of lambda s[npv + 2 * npq] = 1 # tangent vector z[np.r_[pvpq, nb + pq, 2 * nb]] = spsolve(J2, s) # normalize_string tangent predictor (dividing by the euclidean norm) z /= np.linalg.norm(z) Va0 = Va_prev Vm0 = Vm_prev # lam0 = lam # prediction for next step Va0[pvpq] = Va_prev[pvpq] + step * z[pvpq] Vm0[pq] = Vm_prev[pq] + step * z[pq + nb] lam0 = lam + step * z[2 * nb] V0 = Vm0 * np.exp(1j * Va0) return V0, lam0, z
def corrector(Ybus, Ibus, Sbus, V0, pv, pq, lam0, Sxfr, Vprv, lamprv, z, step, parametrization, tol, max_it, pvpq_lookup, verbose): """ Solves the corrector step of a continuation power flow using a full Newton method with selected parametrization scheme. solves for bus voltages and lambda given the full system admittance matrix (for all buses), the complex bus power injection vector (for all buses), the initial vector of complex bus voltages, and column vectors with the lists of bus indices for the swing bus, PV buses, and PQ buses, respectively. The bus voltage vector contains the set point for generator (including ref bus) buses, and the reference angle of the swing bus, as well as an initial guess for remaining magnitudes and angles. Uses default options if this parameter is not given. Returns the final complex voltages, a flag which indicates whether it converged or not, the number of iterations performed, and the final lambda. :param Ybus: Admittance matrix (CSC sparse) :param Ibus: Bus current injections :param Sbus: Bus power injections :param V0: Bus initial voltages :param pv: list of pv nodes :param pq: list of pq nodes :param lam0: initial value of lambda (loading parameter) :param Sxfr: [delP+j*delQ] transfer/loading vector for all buses :param Vprv: final complex V corrector solution from previous continuation step :param lamprv: final lambda corrector solution from previous continuation step :param z: normalized predictor for all buses :param step: continuation step size :param parametrization: :param tol: Tolerance (p.u.) :param max_it: max iterations :param verbose: print information? :return: Voltage, converged, iterations, lambda, power error, calculated power """ # initialize converged = False i = 0 V = V0 Va = angle(V) Vm = np.abs(V) lam = lam0 # set lam to initial lam0 # set up indexing for updating V npv = len(pv) npq = len(pq) pvpq = r_[pv, pq] nj = npv + npq * 2 # j1:j2 - V angle of pv and pq buses j1 = 0 j2 = npv + npq # j2:j3 - V mag of pq buses j3 = j2 + npq # evaluate F(x0, lam0), including Sxfr transfer/loading Scalc = V * np.conj(Ybus * V) mismatch = Scalc - Sbus - lam * Sxfr # F = r_[mismatch[pvpq].real, mismatch[pq].imag] # evaluate P(x0, lambda0) P = cpf_p(parametrization, step, z, V, lam, Vprv, lamprv, pv, pq, pvpq) # augment F(x,lambda) with P(x,lambda) F = r_[mismatch[pvpq].real, mismatch[pq].imag, P] # check tolerance normF = linalg.norm(F, Inf) converged = normF < tol if verbose: print('\nConverged!\n') dF_dlam = -r_[Sxfr[pvpq].real, Sxfr[pq].imag] # do Newton iterations while not converged and i < max_it: # update iteration counter i += 1 # evaluate Jacobian J = _create_J_with_numba(Ybus, V, pvpq, pq, pvpq_lookup, npv, npq) dP_dV, dP_dlam = cpf_p_jac(parametrization, z, V, lam, Vprv, lamprv, pv, pq, pvpq) # augment J with real/imag - Sxfr and z^T ''' J = [ J dF_dlam dP_dV dP_dlam ] ''' J = vstack( [hstack([J, dF_dlam.reshape(nj, 1)]), hstack([dP_dV, dP_dlam])], format="csc") # compute update step dx = spsolve(J, F) # update voltage if npv: Va[pvpq] -= dx[j1:j2] if npq: Vm[pq] -= dx[j2:j3] # update lambda lam -= dx[j3] # update Vm and Va again in case we wrapped around with a negative Vm V = Vm * exp(1j * Va) Vm = np.abs(V) Va = angle(V) # evaluate F(x, lam) Scalc = V * conj(Ybus * V) mismatch = Scalc - Sbus - lam * Sxfr # evaluate the parametrization function P(x, lambda) P = cpf_p(parametrization, step, z, V, lam, Vprv, lamprv, pv, pq, pvpq) # compose the mismatch vector F = r_[mismatch[pvpq].real, mismatch[pq].imag, P] # check for convergence normF = linalg.norm(F, Inf) if verbose: print('\n#3d #10.3e', i, normF) converged = normF < tol if verbose: print('\nNewton' 's method corrector converged in ', i, ' iterations.\n') if verbose: if not converged: print('\nNewton method corrector did not converge in ', i, ' iterations.\n') return V, converged, i, lam, normF, Scalc
def levenberg_marquardt_pf( Ybus, Sbus_, V0, Ibus, pv_, pq_, Qmin, Qmax, tol, max_it=50, control_q=ReactivePowerControlMode.NoControl ) -> NumericPowerFlowResults: """ 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 Qmin: Qmax: tol: Tolerance max_it: Maximum number of iterations control_q: Type of reactive power control Returns: Voltage solution, converged?, error, calculated power injections @Author: Santiago Peñate Vera """ start = time.time() # initialize Sbus = Sbus_.copy() 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 pq = pq_.copy() pv = pv_.copy() pvpq = np.r_[pv, pq] npv = len(pv) npq = len(pq) npvpq = npq + npv if npvpq > 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 Idn = sp.diags(np.ones(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)) while not converged and iter_ < max_it: # evaluate Jacobian if update_jacobian: H = _create_J_with_numba(Ybus, V, pvpq, pq, 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[:npvpq] dVm[pq] = dx[npvpq:] # 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) # review reactive power limits # it is only worth checking Q limits with a low error # since with higher errors, the Q values may be far from realistic # finally, the Q control only makes sense if there are pv nodes if control_q != ReactivePowerControlMode.NoControl and normF < 1e-2 and npv > 0: # check and adjust the reactive power # this function passes pv buses to pq when the limits are violated, # but not pq to pv because that is unstable n_changes, Scalc, Sbus, pv, pq, pvpq = control_q_inside_method( Scalc, Sbus, pv, pq, pvpq, Qmin, Qmax) if n_changes > 0: # adjust internal variables to the new pq|pv values npv = len(pv) npq = len(pq) npvpq = npv + npq pvpq_lookup = np.zeros(np.max(Ybus.indices) + 1, dtype=int) pvpq_lookup[pvpq] = np.arange(npvpq) nn = 2 * npq + npv ii = np.linspace(0, nn - 1, nn) Idn = sparse((np.ones(nn), (ii, ii)), shape=(nn, nn)) # csc_matrix identity # recompute the error based on the new Sbus 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 NumericPowerFlowResults(V, converged, normF, Scalc, None, None, None, iter_, elapsed)
def IwamotoNR(Ybus, Sbus_, V0, Ibus, pv_, pq_, Qmin, Qmax, tol, max_it=15, control_q=ReactivePowerControlMode.NoControl, robust=False) -> NumericPowerFlowResults: """ 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: use of the Iwamoto optimal step factor?. Returns: Voltage solution, converged?, error, calculated power injections @Author: Santiago Penate Vera """ start = time.time() # initialize Sbus = Sbus_.copy() 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 pq = pq_.copy() pv = pv_.copy() pvpq = np.r_[pv, pq] npv = len(pv) npq = len(pq) npvpq = npv + npq if npvpq > 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)) # 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) converged = norm_f < tol # 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, pvpq_lookup, npv, npq) # compute update step try: dx = linear_solver(J, f) except: print(J) converged = False iter_ = max_it + 1 # exit condition end = time.time() elapsed = end - start return NumericPowerFlowResults(V, converged, norm_f, Scalc, None, None, None, iter_, elapsed) # assign the solution vector dVa[pvpq] = dx[:npvpq] dVm[pq] = dx[npvpq:] 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, pvpq_lookup, 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) # review reactive power limits # it is only worth checking Q limits with a low error # since with higher errors, the Q values may be far from realistic # finally, the Q control only makes sense if there are pv nodes if control_q != ReactivePowerControlMode.NoControl and norm_f < 1e-2 and npv > 0: # check and adjust the reactive power # this function passes pv buses to pq when the limits are violated, # but not pq to pv because that is unstable n_changes, Scalc, Sbus, pv, pq, pvpq = control_q_inside_method( Scalc, Sbus, pv, pq, pvpq, Qmin, Qmax) if n_changes > 0: # adjust internal variables to the new pq|pv values npv = len(pv) npq = len(pq) npvpq = npv + npq pvpq_lookup = np.zeros(np.max(Ybus.indices) + 1, dtype=int) pvpq_lookup[pvpq] = np.arange(npvpq) # recompute the error based on the new Sbus dS = Scalc - Sbus # complex power mismatch f = np.r_[dS[pvpq].real, dS[ pq].imag] # concatenate to form the mismatch function norm_f = np.linalg.norm(f, np.inf) # check convergence converged = norm_f < tol else: norm_f = 0 converged = True Scalc = Sbus end = time.time() elapsed = end - start return NumericPowerFlowResults(V, converged, norm_f, Scalc, None, None, None, iter_, elapsed)
def NR_LS( Ybus, Sbus_, V0, Ibus, pv_, pq_, Qmin, Qmax, tol, max_it=15, mu_0=1.0, acceleration_parameter=0.05, error_registry=None, control_q=ReactivePowerControlMode.NoControl ) -> NumericPowerFlowResults: """ 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 Qmin: array of lower reactive power limits per bus :param Qmax: array of upper reactive power limits per bus :param tol: Tolerance :param max_it: Maximum number of iterations :param mu_0: initial acceleration value :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 :param control_q: Control reactive power :return: Voltage solution, converged?, error, calculated power injections """ start = time.time() # initialize iter_ = 0 Sbus = Sbus_.copy() 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 pq = pq_.copy() pv = pv_.copy() pvpq = np.r_[pv, pq] npv = len(pv) npq = len(pq) npvpq = npv + npq # j1 = 0 # j2 = npv + npq # j1:j2 - V angle of pv and pq buses # j3 = j2 + npq # j2:j3 - V mag of pq buses if npvpq > 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(npvpq) # 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] norm_f = np.linalg.norm(f, np.inf) converged = norm_f < tol if error_registry is not None: error_registry.append(norm_f) # to be able to compare Ybus.sort_indices() # 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, pvpq_lookup, npv, npq) # compute update step dx = linear_solver(J, f) # reassign the solution vector dVa[pvpq] = dx[:npvpq] dVm[pq] = dx[npvpq:] # set the restoration values prev_Vm = Vm.copy() prev_Va = Va.copy() # set the values and correct with an adaptive mu if needed mu = mu_0 # ideally 1.0 back_track_condition = True l_iter = 0 norm_f_new = 0.0 while back_track_condition and l_iter < max_it and mu > tol: # restore the previous values if we are backtracking (the first iteration is the normal NR procedure) if l_iter > 0: Va = prev_Va.copy() Vm = prev_Vm.copy() # update voltage the Newton way Vm -= mu * dVm Va -= mu * dVa V = Vm * np.exp(1.0j * Va) # compute the mismatch function f(x_new) Scalc = V * np.conj(Ybus * V - Ibus) dS = Scalc - Sbus # complex power mismatch f = np.r_[ dS[pvpq].real, dS[pq].imag] # concatenate to form the mismatch function norm_f_new = np.linalg.norm(f, np.inf) back_track_condition = norm_f_new > norm_f mu *= acceleration_parameter l_iter += 1 if l_iter > 1 and back_track_condition: # this means that not even the backtracking was able to correct the solution so, restore and end Va = prev_Va.copy() Vm = prev_Vm.copy() V = Vm * np.exp(1.0j * Va) end = time.time() elapsed = end - start return NumericPowerFlowResults(V, converged, norm_f_new, Scalc, None, None, None, iter_, elapsed) else: norm_f = norm_f_new # review reactive power limits # it is only worth checking Q limits with a low error # since with higher errors, the Q values may be far from realistic # finally, the Q control only makes sense if there are pv nodes if control_q != ReactivePowerControlMode.NoControl and norm_f < 1e-2 and npv > 0: # check and adjust the reactive power # this function passes pv buses to pq when the limits are violated, # but not pq to pv because that is unstable n_changes, Scalc, Sbus, pv, pq, pvpq = control_q_inside_method( Scalc, Sbus, pv, pq, pvpq, Qmin, Qmax) if n_changes > 0: # adjust internal variables to the new pq|pv values npv = len(pv) npq = len(pq) npvpq = npv + npq pvpq_lookup = np.zeros(np.max(Ybus.indices) + 1, dtype=int) pvpq_lookup[pvpq] = np.arange(npvpq) # recompute the error based on the new Sbus dS = Scalc - Sbus # complex power mismatch f = np.r_[dS[pvpq].real, dS[ pq].imag] # concatenate to form the mismatch function norm_f = np.linalg.norm(f, np.inf) if error_registry is not None: error_registry.append(norm_f) converged = norm_f < tol else: norm_f = 0 converged = True Scalc = Sbus end = time.time() elapsed = end - start return NumericPowerFlowResults(V, converged, norm_f, Scalc, None, None, None, iter_, elapsed)