def helm_coefficients_josep(Ybus, Yseries, V0, S0, Ysh0, pq, pv, sl, pqpv, tolerance=1e-6, max_coeff=30, verbose=False): """ Holomorphic Embedding LoadFlow Method as formulated by Josep Fanals Batllori in 2020 THis function just returns the coefficients for further usage in other routines :param Yseries: Admittance matrix of the series elements :param V0: vector of specified voltages :param S0: vector of specified power :param Ysh0: vector of shunt admittances (including the shunts of the branches) :param pq: list of pq nodes :param pv: list of pv nodes :param sl: list of slack nodes :param pqpv: sorted list of pq and pv nodes :param tolerance: target error (or tolerance) :param max_coeff: maximum number of coefficients :param verbose: print intermediate information :return: U, X, Q, iterations """ npqpv = len(pqpv) npv = len(pv) nsl = len(sl) n = Yseries.shape[0] # --------------------------- PREPARING IMPLEMENTATION ------------------------------------------------------------- U = np.zeros((max_coeff, npqpv), dtype=complex) # voltages W = np.zeros((max_coeff, npqpv), dtype=complex) # compute X=1/conj(U) Q = np.zeros((max_coeff, npqpv), dtype=complex) # unknown reactive powers Vm0 = np.abs(V0) Vm2 = Vm0 * Vm0 if n < 2: return U, W, Q, 0 if verbose: print('Yseries') print(Yseries.toarray()) df = pd.DataFrame(data=np.c_[Ysh0.imag, S0.real, S0.imag, Vm0], columns=['Ysh', 'P0', 'Q0', 'V0']) print(df) Yred = Yseries[np.ix_(pqpv, pqpv)] # admittance matrix without slack buses Yslack = -Yseries[np.ix_(pqpv, sl)] # yes, it is the negative of this Yslack_vec = Yslack.sum(axis=1).A1 G = np.real(Yred) # real parts of Yij B = np.imag(Yred) # imaginary parts of Yij P_red = S0.real[pqpv] Q_red = S0.imag[pqpv] Vslack = V0[sl] Ysh_red = Ysh0[pqpv] # indices 0 based in the internal scheme nsl_counted = np.zeros(n, dtype=int) compt = 0 for i in range(n): if i in sl: compt += 1 nsl_counted[i] = compt pq_ = pq - nsl_counted[pq] pv_ = pv - nsl_counted[pv] # .......................CALCULATION OF TERMS [0] ------------------------------------------------------------------ U[0, :] = spsolve(Yred, Yslack_vec) W[0, :] = 1 / np.conj(U[0, :]) # .......................CALCULATION OF TERMS [1] ------------------------------------------------------------------ valor = np.zeros(npqpv, dtype=complex) # get the current injections that appear due to the slack buses reduction I_inj_slack = Yslack * Vslack valor[pq_] = I_inj_slack[pq_] - Yslack_vec[pq_] + (P_red[pq_] - Q_red[pq_] * 1j) * W[0, pq_] - U[0, pq_] * Ysh_red[pq_] valor[pv_] = I_inj_slack[pv_] - Yslack_vec[pv_] + P_red[pv_] * W[0, pv_] - U[0, pv_] * Ysh_red[pv_] # compose the right-hand side vector RHS = np.r_[valor.real, valor.imag, Vm2[pv] - (U[0, pv_] * U[0, pv_]).real] # Form the system matrix (MAT) Upv = U[0, pv_] Xpv = W[0, pv_] VRE = coo_matrix((2 * Upv.real, (np.arange(npv), pv_)), shape=(npv, npqpv)).tocsc() VIM = coo_matrix((2 * Upv.imag, (np.arange(npv), pv_)), shape=(npv, npqpv)).tocsc() XIM = coo_matrix((-Xpv.imag, (pv_, np.arange(npv))), shape=(npqpv, npv)).tocsc() XRE = coo_matrix((Xpv.real, (pv_, np.arange(npv))), shape=(npqpv, npv)).tocsc() EMPTY = csc_matrix((npv, npv)) MAT = vs((hs((G, -B, XIM)), hs((B, G, XRE)), hs((VRE, VIM, EMPTY))), format='csc') if verbose: print('MAT') print(MAT.toarray()) # factorize (only once) MAT_LU = factorized(MAT.tocsc()) # solve LHS = MAT_LU(RHS) # update coefficients U[1, :] = LHS[:npqpv] + 1j * LHS[npqpv:2 * npqpv] Q[0, pv_] = LHS[2 * npqpv:] W[1, :] = -W[0, :] * np.conj(U[1, :]) / np.conj(U[0, :]) # .......................CALCULATION OF TERMS [>=2] ---------------------------------------------------------------- iter_ = 1 range_pqpv = np.arange(npqpv, dtype=np.int64) V = V0.copy() c = 2 converged = False norm_f = tolerance + 1.0 # any number that violates the convergence while c < max_coeff and not converged: # c defines the current depth valor[pq_] = (P_red[pq_] - Q_red[pq_] * 1j) * W[c - 1, pq_] - U[c - 1, pq_] * Ysh_red[pq_] valor[pv_] = -1j * conv2(W, Q, c, pv_) - U[c - 1, pv_] * Ysh_red[pv_] + W[c - 1, pv_] * P_red[pv_] RHS = np.r_[valor.real, valor.imag, -conv3(U, U, c, pv_).real] LHS = MAT_LU(RHS) # update voltage coefficients U[c, :] = LHS[:npqpv] + 1j * LHS[npqpv:2 * npqpv] # update reactive power Q[c - 1, pv_] = LHS[2 * npqpv:] # update voltage inverse coefficients W[c, range_pqpv] = -conv1(U, W, c, range_pqpv) / np.conj(U[0, range_pqpv]) # compute power mismatch if not np.mod(c, 2): # check the mismatch every 4 iterations V[pqpv] = U.sum(axis=0) Scalc = V * np.conj(Ybus * V) dP = np.abs(S0[pqpv].real - Scalc[pqpv].real) dQ = np.abs(S0[pq].imag - Scalc[pq].imag) norm_f = np.linalg.norm(np.r_[dP, dQ], np.inf) # same as max(abs()) # check convergence converged = norm_f < tolerance print('mismatch check at c=', c) c += 1 iter_ += 1 return U, W, Q, iter_, norm_f
def helm_josep(Ybus, Yseries, V0, S0, Ysh0, pq, pv, sl, pqpv, tolerance=1e-6, max_coeff=30, use_pade=True, verbose=False, return_structs=False): """ Holomorphic Embedding LoadFlow Method as formulated by Josep Fanals Batllori in 2020 :param Ybus: Complete admittance matrix :param Yseries: Admittance matrix of the series elements :param V0: vector of specified voltages :param S0: vector of specified power :param Ysh0: vector of shunt admittances (including the shunts of the branches) :param pq: list of pq nodes :param pv: list of pv nodes :param sl: list of slack nodes :param pqpv: sorted list of pq and pv nodes :param tolerance: target error (or tolerance) :param max_coeff: maximum number of coefficients :param use_pade: Use the Padè approximation? otherwise a simple summation is done :param verbose: Print intermediate objects and calculations? :return: V, converged, norm_f, Scalc, iter_, elapsed """ start_time = time.time() npqpv = len(pqpv) npv = len(pv) nsl = len(sl) n = Yseries.shape[0] if n < 2: # V, converged, norm_f, Scalc, iter_, elapsed return V0, True, 0.0, S0, 0, 0.0 # --------------------------- PREPARING IMPLEMENTATION-------------------------------------------------------------- U = np.zeros((max_coeff, npqpv), dtype=complex) # voltages U_re = np.zeros((max_coeff, npqpv), dtype=float) # real part of voltages U_im = np.zeros((max_coeff, npqpv), dtype=float) # imaginary part of voltages X = np.zeros((max_coeff, npqpv), dtype=complex) # compute X=1/conj(U) X_re = np.zeros((max_coeff, npqpv), dtype=float) # real part of X X_im = np.zeros((max_coeff, npqpv), dtype=float) # imaginary part of X Q = np.zeros((max_coeff, npqpv), dtype=complex) # unknown reactive powers Vm0 = np.abs(V0) vec_W = Vm0 * Vm0 if verbose: print('Yseries') print(Yseries.toarray()) df = pd.DataFrame(data=np.c_[Ysh0.imag, S0.real, S0.imag, Vm0], columns=['Ysh', 'P0', 'Q0', 'V0']) print(df) Yred = Yseries[np.ix_(pqpv, pqpv)] # admittance matrix without slack buses Yslack = -Yseries[np.ix_(pqpv, sl)] # yes, it is the negative of this G = np.real(Yred) # real parts of Yij B = np.imag(Yred) # imaginary parts of Yij vec_P = S0.real[pqpv] vec_Q = S0.imag[pqpv] Vslack = V0[sl] # indices 0 based in the internal scheme nsl_counted = np.zeros(n, dtype=int) compt = 0 for i in range(n): if i in sl: compt += 1 nsl_counted[i] = compt pq_ = pq - nsl_counted[pq] pv_ = pv - nsl_counted[pv] pqpv_ = np.sort(np.r_[pq_, pv_]) # .......................CALCULATION OF TERMS [0] ------------------------------------------------------------------ if nsl > 1: U[0, :] = spsolve(Yred, Yslack.sum(axis=1)) else: U[0, :] = spsolve(Yred, Yslack) X[0, :] = 1 / np.conj(U[0, :]) U_re[0, :] = U[0, :].real U_im[0, :] = U[0, :].imag X_re[0, :] = X[0, :].real X_im[0, :] = X[0, :].imag # .......................CALCULATION OF TERMS [1] ------------------------------------------------------------------ valor = np.zeros(npqpv, dtype=complex) # get the current injections that appear due to the slack buses reduction I_inj_slack = Yslack[pqpv_, :] * Vslack valor[pq_] = I_inj_slack[pq_] - Yslack[pq_].sum(axis=1).A1 + (vec_P[pq_] - vec_Q[pq_] * 1j) * X[0, pq_] + U[0, pq_] * Ysh0[pq_] valor[pv_] = I_inj_slack[pv_] - Yslack[pv_].sum(axis=1).A1 + (vec_P[pv_]) * X[0, pv_] + U[0, pv_] * Ysh0[pv_] # compose the right-hand side vector RHS = np.r_[valor.real, valor.imag, vec_W[pv] - 1.0] # Form the system matrix (MAT) Upv = U[0, pv_] Xpv = X[0, pv_] VRE = coo_matrix((2 * Upv.real, (np.arange(npv), pv_)), shape=(npv, npqpv)).tocsc() VIM = coo_matrix((2 * Upv.imag, (np.arange(npv), pv_)), shape=(npv, npqpv)).tocsc() XIM = coo_matrix((-Xpv.imag, (pv_, np.arange(npv))), shape=(npqpv, npv)).tocsc() XRE = coo_matrix((Xpv.real, (pv_, np.arange(npv))), shape=(npqpv, npv)).tocsc() EMPTY = csc_matrix((npv, npv)) MAT = vs((hs((G, -B, XIM)), hs((B, G, XRE)), hs((VRE, VIM, EMPTY))), format='csc') if verbose: print('MAT') print(MAT.toarray()) # factorize (only once) MAT_LU = factorized(MAT.tocsc()) # solve LHS = MAT_LU(RHS) # update coefficients Q[0, pv_] = LHS[2 * npqpv:] U[1, :] = LHS[:npqpv] + 1j * LHS[npqpv:2 * npqpv] X[1, :] = -X[0, :] * np.conj(U[1, :]) / np.conj(U[0, :]) # .......................CALCULATION OF TERMS [>=2] ---------------------------------------------------------------- iter_ = 1 range_pqpv = np.arange(npqpv, dtype=np.int64) for c in range(2, max_coeff): # c defines the current depth valor[pq_] = (vec_P[pq_] - vec_Q[pq_] * 1j) * X[c - 1, pq_] + U[c - 1, pq_] * Ysh0[pq_] valor[pv_] = conv2(X, Q, c, pv_) * -1j + U[c - 1, pv_] * Ysh0[pv_] + X[c - 1, pv_] * vec_P[pv_] RHS = np.r_[valor.real, valor.imag, -conv3(U, U, c, pv_).real] LHS = MAT_LU(RHS) # update voltage coefficients U[c, :] = LHS[:npqpv] + 1j * LHS[npqpv:2 * npqpv] # update reactive power Q[c - 1, pv_] = LHS[2 * npqpv:] # update voltage inverse coefficients X[c, range_pqpv] = -conv1(U, X, c, range_pqpv) / np.conj(U[0, range_pqpv]) iter_ += 1 # --------------------------- RESULTS COMPOSITION ------------------------------------------------------------------ if verbose: print('V coefficients') print(U) # compute the final voltage V = np.zeros(n, dtype=complex) if use_pade: V[pqpv] = pade4all(max_coeff - 1, U, 1) else: V[pqpv] = U.sum(axis=0) V[sl] = Vslack # copy the slack buses # compute power mismatch Scalc = V * np.conj(Ybus * V) dP = np.abs(S0[pqpv].real - Scalc[pqpv].real) dQ = np.abs(S0[pq].imag - Scalc[pq].imag) norm_f = np.linalg.norm(np.r_[dP, dQ], np.inf) # same as max(abs()) # check convergence converged = norm_f < tolerance elapsed = time.time() - start_time if return_structs: return V, converged, norm_f, Scalc, iter_, elapsed, U, X else: return V, converged, norm_f, Scalc, iter_, elapsed
def helm_coefficients_josep(Yseries, V0, S0, Ysh0, pq, pv, sl, pqpv, tolerance=1e-6, max_coeff=30, verbose=False): """ Holomorphic Embedding LoadFlow Method as formulated by Josep Fanals Batllori in 2020 THis function just returns the coefficients for further usage in other routines :param Yseries: Admittance matrix of the series elements :param V0: vector of specified voltages :param S0: vector of specified power :param Ysh0: vector of shunt admittances (including the shunts of the branches) :param pq: list of pq nodes :param pv: list of pv nodes :param sl: list of slack nodes :param pqpv: sorted list of pq and pv nodes :param tolerance: target error (or tolerance) :param max_coeff: maximum number of coefficients :param verbose: print intermediate information :return: U, X, Q, iterations """ npqpv = len(pqpv) npv = len(pv) nsl = len(sl) n = Yseries.shape[0] # --------------------------- PREPARING IMPLEMENTATION ------------------------------------------------------------- U = np.zeros((max_coeff, npqpv), dtype=complex) # voltages X = np.zeros((max_coeff, npqpv), dtype=complex) # compute X=1/conj(U) Q = np.zeros((max_coeff, npqpv), dtype=complex) # unknown reactive powers if n < 2: return U, X, Q, 0 if verbose: print('Yseries') print(Yseries.toarray()) df = pd.DataFrame(data=np.c_[Ysh0.imag, S0.real, S0.imag, np.abs(V0)], columns=['Ysh', 'P0', 'Q0', 'V0']) print(df) # build the reduced system Yred = Yseries[np.ix_(pqpv, pqpv)] # admittance matrix without slack buses Yslack = -Yseries[np.ix_(pqpv, sl)] # yes, it is the negative of this G = Yred.real.copy() # real parts of Yij B = Yred.imag.copy() # imaginary parts of Yij vec_P = S0.real[pqpv] vec_Q = S0.imag[pqpv] Vslack = V0[sl] Ysh = Ysh0[pqpv] Vm0 = np.abs(V0[pqpv]) vec_W = Vm0 * Vm0 # indices 0 based in the internal scheme nsl_counted = np.zeros(n, dtype=int) compt = 0 for i in range(n): if i in sl: compt += 1 nsl_counted[i] = compt pq_ = pq - nsl_counted[pq] pv_ = pv - nsl_counted[pv] pqpv_ = np.sort(np.r_[pq_, pv_]) # .......................CALCULATION OF TERMS [0] ------------------------------------------------------------------ if nsl > 1: U[0, :] = spsolve(Yred, Yslack.sum(axis=1)) else: U[0, :] = spsolve(Yred, Yslack) X[0, :] = 1 / np.conj(U[0, :]) # .......................CALCULATION OF TERMS [1] ------------------------------------------------------------------ valor = np.zeros(npqpv, dtype=complex) # get the current injections that appear due to the slack buses reduction I_inj_slack = Yslack[pqpv_, :] * Vslack valor[pq_] = I_inj_slack[pq_] - Yslack[pq_].sum(axis=1).A1 + (vec_P[pq_] - vec_Q[pq_] * 1j) * X[0, pq_] - U[0, pq_] * Ysh[pq_] valor[pv_] = I_inj_slack[pv_] - Yslack[pv_].sum(axis=1).A1 + (vec_P[pv_]) * X[0, pv_] - U[0, pv_] * Ysh[pv_] # compose the right-hand side vector RHS = np.r_[valor.real, valor.imag, vec_W[pv_] - (U[0, pv_] * U[0, pv_]).real # vec_W[pv_] - 1.0 ] # Form the system matrix (MAT) Upv = U[0, pv_] Xpv = X[0, pv_] VRE = coo_matrix((2 * Upv.real, (np.arange(npv), pv_)), shape=(npv, npqpv)).tocsc() VIM = coo_matrix((2 * Upv.imag, (np.arange(npv), pv_)), shape=(npv, npqpv)).tocsc() XIM = coo_matrix((-Xpv.imag, (pv_, np.arange(npv))), shape=(npqpv, npv)).tocsc() XRE = coo_matrix((Xpv.real, (pv_, np.arange(npv))), shape=(npqpv, npv)).tocsc() EMPTY = csc_matrix((npv, npv)) MAT = vs((hs((G, -B, XIM)), hs((B, G, XRE)), hs((VRE, VIM, EMPTY))), format='csc') if verbose: print('MAT') print(MAT.toarray()) # factorize (only once) # MAT_LU = factorized(MAT.tocsc()) # solve LHS = spsolve(MAT, RHS) # update coefficients U[1, :] = LHS[:npqpv] + 1j * LHS[npqpv:2 * npqpv] Q[0, pv_] = LHS[2 * npqpv:] X[1, :] = -X[0, :] * np.conj(U[1, :]) / np.conj(U[0, :]) # .......................CALCULATION OF TERMS [>=2] ---------------------------------------------------------------- iter_ = 1 for c in range(2, max_coeff): # c defines the current depth valor[pq_] = (vec_P[pq_] - vec_Q[pq_] * 1j) * X[c - 1, pq_] - U[c - 1, pq_] * Ysh[pq_] valor[pv_] = -1j * conv2(X, Q, c, pv_) - U[c - 1, pv_] * Ysh[pv_] + X[c - 1, pv_] * vec_P[pv_] RHS = np.r_[valor.real, valor.imag, -conv3(U, U, c, pv_).real] LHS = spsolve(MAT, RHS) # update voltage coefficients U[c, :] = LHS[:npqpv] + 1j * LHS[npqpv:2 * npqpv] # update reactive power Q[c - 1, pv_] = LHS[2 * npqpv:] # update voltage inverse coefficients X[c, :] = -conv1(U, X, c) / np.conj(U[0, :]) iter_ += 1 return U, X, Q, iter_
def helm_coefficients_andre(Yseries, V0, S0, Ysh0, pq, pv, sl, pqpv, tolerance=1e-6, max_coeff=30, verbose=False): """ Holomorphic Embedding LoadFlow Method as formulated by Josep Fanals Batllori in 2020 (----Reformulation to avoid Qk[n], Wk[n] calculation----) This function just returns the coefficients for further usage in other routines :param Yseries: Admittance matrix of the series elements :param V0: vector of specified voltages (complex) :param S0: vector of specified power (complex) :param Ysh0: vector of shunt admittances (including the shunts of the branches) :param pq: list of pq nodes :param pv: list of pv nodes :param sl: list of slack nodes :param pqpv: sorted list of pq and pv nodes :param tolerance: target error (or tolerance) :param max_coeff: maximum number of coefficients :param verbose: print intermediate information :return: U, "iterations" """ npqpv = len(pqpv) # number of non-slack buses npq = len(pq) # number of pq buses npv = len(pv) # number of pv buses nsl = len(sl) # number of slack buses n = Yseries.shape[ 0] # number of buses; size(Yseries,n) = Yseries.shape[n-1]; size(Yseries) = Yseries.shape; # --------------------------- PREPARING IMPLEMENTATION ------------------------------------------------------------- U = np.zeros((max_coeff, npqpv), dtype=complex, order='C') # voltage series coefficients conjU = np.zeros((max_coeff, npqpv), dtype=complex, order='C') # conjugate voltage series coefficients Vsp = np.abs(V0[pqpv]) # Specified voltage magnitude for PV-Buses Vsp_2 = Vsp * Vsp # Specified voltage magnitude for PV-Buses Squared if n < 2: return U, 0 if verbose: print('Yseries') print(Yseries.toarray()) df = pd.DataFrame(data=np.c_[Ysh0.imag, S0.real, S0.imag, np.abs(V0)], columns=['Ysh', 'P0', 'Q0', 'V0']) print(df) Yrr = Yseries[np.ix_(pqpv, pqpv)] # Y_RED-RED; Yseries(pqpv,pqpv) Yrs = Yseries[np.ix_(pqpv, sl)] # Y_RED-SLK Vs = V0[sl] # V_SLK Isl = Yrs * Vs # I_SLK Ssp = S0[ pqpv] # Specified apparent power (Active for PQ and PV; Reactive for PQ) Ysh = Ysh0[pqpv] # Y_RED # indices 0 based in the internal scheme nsl_counted = np.zeros(n, dtype=int) compt = 0 for i in range(n): if i in sl: compt += 1 nsl_counted[i] = compt pq_ = pq - nsl_counted[pq] pv_ = pv - nsl_counted[pv] pqpv_ = np.arange(0, npqpv, dtype=np.int64) # .......................CALCULATION OF TERMS [0] ------------------------------------------------------------------ U[0, :] = spsolve(Yrr, -Isl) conjU[0, :] = np.conj(U[0, :]) # .......................LINEAR SYSTEM MATRIX (MAT) ---------------------------------------------------------------- YV = Yrr.multiply(conjU[0, :].reshape(npqpv, 1)) YV = YV.tocsc() # | Re{YxconjV} -Im{YxconjV} | * |LHS_ReU [PQ,PV]| = |RHS_P [PQ,PV]| # | Im{YxconjV} Re{YxconjV} | |LHS_ImU [PQ,PV]| |RHS_Q [PQ] | # | Re{V} Im{V} | |RHS_V [PV] | ReYVp = YV.real ImYVp = YV.imag ReYVq = ReYVp[np.ix_(pq_, pqpv_)] ImYVq = ImYVp[np.ix_(pq_, pqpv_)] ReV = coo_matrix( (U[0, pv_].real, (np.arange(0, npv, dtype=np.int64), pv_)), shape=(npv, npqpv)).tocsc() ImV = coo_matrix( (U[0, pv_].imag, (np.arange(0, npv, dtype=np.int64), pv_)), shape=(npv, npqpv)).tocsc() # sparse matrix concatenation MAT = vs((hs( (ReYVp, -ImYVp), format='csr'), hs( (ImYVq, ReYVq), format='csr'), hs((ReV, ImV), format='csr')), format='csc') if verbose: print('MAT') print(MAT.toarray()) # factorize (only once) MAT_LU = factorized(MAT) # .......................CALCULATION OF TERMS [>=1] ---------------------------------------------------------------- for c in range(1, max_coeff): # max_coeff_order if c == 1: UconjU = U[0, :] * conjU[0, :] UconjU = UconjU.real valor_v = 0.5 * (Vsp_2[pv_] - UconjU[pv_]) valor_pq = np.conj(Ssp) - Ysh * UconjU else: conjU[c - 1, :] = np.conj(U[c - 1, :]) valor_v = -conv4(U, conjU, c, pv_) valor_pq = -Ysh * conv5(U, conjU, c, pqpv_) - conv6( U, conjU, Yrr, npqpv, c) # compose the right-hand side vector RHS = np.r_[valor_pq.real, valor_pq[pq_].imag, valor_v] # solve LHS = MAT_LU(RHS) # update voltage coefficients U[c, :] = LHS[0:npqpv] + 1j * LHS[npqpv:2 * npqpv] return U, max_coeff - 1