def helm_josep(Ybus, Yseries, V0, S0, Ysh0, pq, pv, sl, pqpv, tolerance=1e-6, max_coeff=30, use_pade=True, verbose=False) -> NumericPowerFlowResults: """ 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 information :return: V, converged, norm_f, Scalc, iter_, elapsed """ start_time = time.time() n = Yseries.shape[0] if n < 2: return NumericPowerFlowResults(V0, True, 0.0, S0, None, None, None, None, None, None, 0, 0.0) # compute the series of coefficients U, X, Q, iter_ = helm_coefficients_josep(Yseries, V0, S0, Ysh0, pq, pv, sl, pqpv, tolerance=tolerance, max_coeff=max_coeff, verbose=verbose) # --------------------------- RESULTS COMPOSITION ------------------------------------------------------------------ if verbose: print('V coefficients') print(U) # compute the final voltage vector V = V0.copy() if use_pade: try: V[pqpv] = pade4all(max_coeff - 1, U, 1) except: warn('Padè failed :(, using coefficients summation') V[pqpv] = U.sum(axis=0) else: V[pqpv] = U.sum(axis=0) # 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 return NumericPowerFlowResults(V, converged, norm_f, Scalc, None, None, None, None, None, None, iter_, elapsed)
def dcpf(Ybus, Bpqpv, Bref, Sbus, Ibus, V0, ref, pvpq, pq, pv) -> NumericPowerFlowResults: """ Solves a DC power flow. :param Ybus: Normal circuit admittance matrix :param Sbus: Complex power injections at all the nodes :param Ibus: Complex current injections at all the nodes :param V0: Array of complex seed voltage (it contains the ref voltages) :param ref: array of the indices of the slack nodes :param pvpq: array of the indices of the non-slack nodes :param pq: array of the indices of the pq nodes :param pv: array of the indices of the pv nodes :return: Complex voltage solution Converged: Always true Solution error Computed power injections given the found solution """ start = time.time() npq = len(pq) npv = len(pv) if (npq + npv) > 0: # Decompose the voltage in angle and magnitude Va_ref = np.angle(V0[ref]) # we only need the angles at the slack nodes Vm = np.abs(V0) # initialize result vector Va = np.empty(len(V0)) # compose the reduced power injections # Since we have removed the slack nodes, we must account their influence as injections Bref * Va_ref Pinj = Sbus[pvpq].real + (- Bref * Va_ref + Ibus[pvpq].real) * Vm[pvpq] # update angles for non-reference buses Va[pvpq] = linear_solver(Bpqpv, Pinj) Va[ref] = Va_ref # re assemble the voltage V = Vm * np.exp(1j * Va) # compute the calculated power injection and the error of the voltage solution Scalc = V * np.conj(Ybus * V - Ibus) # compute the power mismatch between the specified power Sbus and the calculated power Scalc mis = Scalc - Sbus # complex power mismatch mismatch = np.r_[mis[pv].real, mis[pq].real, mis[pq].imag] # concatenate again # check for convergence norm_f = np.linalg.norm(mismatch, np.Inf) else: norm_f = 0.0 V = V0 Scalc = V * np.conj(Ybus * V - Ibus) end = time.time() elapsed = end - start return NumericPowerFlowResults(V, True, norm_f, Scalc, None, None, None, 1, elapsed)
def lacpf(Y, Ys, S, I, Vset, pq, pv) -> NumericPowerFlowResults: """ Linearized AC Load Flow form the article: Linearized AC Load Flow Applied to Analysis in Electric Power Systems by: P. Rossoni, W. M da Rosa and E. A. Belati Args: Y: Admittance matrix Ys: Admittance matrix of the series elements S: Power injections vector of all the nodes Vset: Set voltages of all the nodes (used for the slack and PV nodes) pq: list of indices of the pq nodes pv: list of indices of the pv nodes Returns: Voltage vector, converged?, error, calculated power and elapsed time """ start = time.time() pvpq = np.r_[pv, pq] npq = len(pq) npv = len(pv) if (npq + npv) > 0: # compose the system matrix # G = Y.real # B = Y.imag # Gp = Ys.real # Bp = Ys.imag A11 = -Ys.imag[np.ix_(pvpq, pvpq)] A12 = Y.real[np.ix_(pvpq, pq)] A21 = -Ys.real[np.ix_(pq, pvpq)] A22 = -Y.imag[np.ix_(pq, pq)] Asys = sp.vstack([sp.hstack([A11, A12]), sp.hstack([A21, A22])], format="csc") # compose the right hand side (power vectors) rhs = np.r_[S.real[pvpq], S.imag[pq]] # solve the linear system try: x = linear_solver(Asys, rhs) except Exception as e: voltages_vector = Vset # Calculate the error and check the convergence s_calc = voltages_vector * np.conj(Y * voltages_vector) # complex power mismatch power_mismatch = s_calc - S # concatenate error by type mismatch = np.r_[power_mismatch[pv].real, power_mismatch[pq].real, power_mismatch[pq].imag] # check for convergence norm_f = np.linalg.norm(mismatch, np.Inf) end = time.time() elapsed = end - start return voltages_vector, False, norm_f, s_calc, 1, elapsed # compose the results vector voltages_vector = Vset.copy() # set the pv voltages va_pv = x[0:npv] vm_pv = np.abs(Vset[pv]) voltages_vector[pv] = vm_pv * np.exp(1.0j * va_pv) # set the PQ voltages va_pq = x[npv:npv + npq] vm_pq = np.ones(npq) - x[npv + npq::] voltages_vector[pq] = vm_pq * np.exp(1.0j * va_pq) # Calculate the error and check the convergence s_calc = voltages_vector * np.conj(Y * voltages_vector) # complex power mismatch power_mismatch = s_calc - S # concatenate error by type mismatch = np.r_[power_mismatch[pv].real, power_mismatch[pq].real, power_mismatch[pq].imag] # check for convergence norm_f = np.linalg.norm(mismatch, np.Inf) else: norm_f = 0.0 voltages_vector = Vset s_calc = voltages_vector * np.conj(Y * voltages_vector) end = time.time() elapsed = end - start return NumericPowerFlowResults(voltages_vector, True, norm_f, s_calc, None, None, None, None, None, None, 1, elapsed)
def solve(circuit: SnapshotData, options: PowerFlowOptions, report: ConvergenceReport, V0, Sbus, Ibus, pq, pv, ref, pqpv, logger=bs.Logger()) -> NumericPowerFlowResults: """ Run a power flow simulation using the selected method (no outer loop controls). :param circuit: SnapshotData circuit, this ensures on-demand admittances computation :param options: PowerFlow options :param report: Convergence report to fill in :param V0: Array of initial voltages :param Sbus: Array of power injections :param Ibus: Array of current injections :param pq: Array of pq nodes :param pv: Array of pv nodes :param ref: Array of slack nodes :param pqpv: Array of (sorted) pq and pv nodes :param logger: Logger :return: NumericPowerFlowResults """ if options.retry_with_other_methods: if circuit.any_control: solver_list = [bs.SolverType.NR, bs.SolverType.LM, bs.SolverType.HELM, bs.SolverType.IWAMOTO, bs.SolverType.LACPF] else: solver_list = [bs.SolverType.NR, bs.SolverType.HELM, bs.SolverType.IWAMOTO, bs.SolverType.LM, bs.SolverType.LACPF] if options.solver_type in solver_list: solver_list.remove(options.solver_type) solvers = [options.solver_type] + solver_list else: # No retry selected solvers = [options.solver_type] # set worked to false to enter in the loop solver_idx = 0 # set the initial value final_solution = NumericPowerFlowResults(V=V0, converged=False, norm_f=1e200, Scalc=Sbus, ma=circuit.branch_data.m[:, 0], theta=circuit.branch_data.theta[:, 0], Beq=circuit.branch_data.Beq[:, 0], iterations=0, elapsed=0) while solver_idx < len(solvers) and not final_solution.converged: # get the solver solver_type = solvers[solver_idx] # type HELM if solver_type == bs.SolverType.HELM: solution = hl.helm_josep(Ybus=circuit.Ybus, Yseries=circuit.Yseries, V0=V0, # take V0 instead of V S0=Sbus, Ysh0=circuit.Yshunt, pq=pq, pv=pv, sl=ref, pqpv=pqpv, tolerance=options.tolerance, max_coeff=options.max_iter, use_pade=True, verbose=False) # type DC elif solver_type == bs.SolverType.DC: solution = aclin.dcpf(Ybus=circuit.Ybus, Bpqpv=circuit.Bpqpv, Bref=circuit.Bref, Sbus=Sbus, Ibus=Ibus, V0=V0, ref=ref, pvpq=pqpv, pq=pq, pv=pv) # LAC PF elif solver_type == bs.SolverType.LACPF: solution = aclin.lacpf(Y=circuit.Ybus, Ys=circuit.Yseries, S=Sbus, I=Ibus, Vset=V0, pq=pq, pv=pv) # Levenberg-Marquardt elif solver_type == bs.SolverType.LM: if circuit.any_control: solution = acdcjb.LM_ACDC(nc=circuit, Vbus=V0, Sbus=Sbus, tolerance=options.tolerance, max_iter=options.max_iter) else: solution = acjb.levenberg_marquardt_pf(Ybus=circuit.Ybus, Sbus_=Sbus, V0=final_solution.V, Ibus=Ibus, pv_=pv, pq_=pq, Qmin=circuit.Qmin_bus[0, :], Qmax=circuit.Qmax_bus[0, :], tol=options.tolerance, max_it=options.max_iter, control_q=options.control_Q) # Fast decoupled elif solver_type == bs.SolverType.FASTDECOUPLED: solution = acfd.FDPF(Vbus=V0, Sbus=Sbus, Ibus=Ibus, Ybus=circuit.Ybus, B1=circuit.B1, B2=circuit.B2, pq=pq, pv=pv, pqpv=pqpv, tol=options.tolerance, max_it=options.max_iter) # Newton-Raphson (full) elif solver_type == bs.SolverType.NR: if circuit.any_control: # Solve NR with the AC/DC algorithm solution = acdcjb.NR_LS_ACDC(nc=circuit, Vbus=V0, Sbus=Sbus, tolerance=options.tolerance, max_iter=options.max_iter, acceleration_parameter=options.backtracking_parameter, mu_0=options.mu, control_q=options.control_Q) else: # Solve NR with the AC algorithm solution = acjb.NR_LS(Ybus=circuit.Ybus, Sbus_=Sbus, V0=final_solution.V, Ibus=Ibus, pv_=pv, pq_=pq, Qmin=circuit.Qmin_bus[0, :], Qmax=circuit.Qmax_bus[0, :], tol=options.tolerance, max_it=options.max_iter, mu_0=options.mu, acceleration_parameter=options.backtracking_parameter, control_q=options.control_Q) # Newton-Raphson-Decpupled elif solver_type == bs.SolverType.NRD: # Solve NR with the linear AC solution solution = acjb.NRD_LS(Ybus=circuit.Ybus, Sbus=Sbus, V0=final_solution.V, Ibus=Ibus, pv=pv, pq=pq, tol=options.tolerance, max_it=options.max_iter, acceleration_parameter=options.backtracking_parameter) # Newton-Raphson-Iwamoto elif solver_type == bs.SolverType.IWAMOTO: solution = acjb.IwamotoNR(Ybus=circuit.Ybus, Sbus_=Sbus, V0=final_solution.V, Ibus=Ibus, pv_=pv, pq_=pq, Qmin=circuit.Qmin_bus[0, :], Qmax=circuit.Qmax_bus[0, :], tol=options.tolerance, max_it=options.max_iter, control_q=options.control_Q, robust=True) # Newton-Raphson in current equations elif solver_type == bs.SolverType.NRI: solution = acjb.NR_I_LS(Ybus=circuit.Ybus, Sbus_sp=Sbus, V0=final_solution.V, Ibus_sp=Ibus, pv=pv, pq=pq, tol=options.tolerance, max_it=options.max_iter) else: # for any other method, raise exception raise Exception(solver_type + ' Not supported in power flow mode') # record the method used, if it improved the solution if solution.norm_f < final_solution.norm_f: report.add(method=solver_type, converged=solution.converged, error=solution.norm_f, elapsed=solution.elapsed, iterations=solution.iterations) final_solution = solution # record the solver steps solver_idx += 1 if not final_solution.converged: logger.add_error('Did not converge, even after retry!', 'Error', str(final_solution.norm_f), options.tolerance) if final_solution.ma is None: final_solution.ma = circuit.branch_data.m[:, 0] if final_solution.theta is None: final_solution.theta = circuit.branch_data.theta[:, 0] if final_solution.Beq is None: final_solution.Beq = circuit.branch_data.Beq[:, 0] return final_solution
def outer_loop_power_flow(circuit: SnapshotData, options: PowerFlowOptions, voltage_solution, Sbus, Ibus, branch_rates, t=0, logger=bs.Logger()) -> "PowerFlowResults": """ Run a power flow simulation for a single circuit using the selected outer loop controls. This method shouldn't be called directly. :param circuit: CalculationInputs instance :param options: :param voltage_solution: vector of initial voltages :param Sbus: vector of power injections :param Ibus: vector of current injections :param branch_rates: :param t: time step :param logger: :return: PowerFlowResults instance """ # get the original types and compile this class' own lists of node types for thread independence bus_types = circuit.bus_types.copy() # vd = circuit.vd.copy() # pq = circuit.pq.copy() # pv = circuit.pv.copy() # pqpv = circuit.pqpv.copy() report = ConvergenceReport() solution = NumericPowerFlowResults(V=voltage_solution, converged=False, norm_f=1e200, Scalc=Sbus, ma=circuit.branch_data.m[:, 0], theta=circuit.branch_data.theta[:, 0], Beq=circuit.branch_data.Beq[:, 0], iterations=0, elapsed=0) # this the "outer-loop" if len(circuit.vd) == 0: voltage_solution = np.zeros(len(Sbus), dtype=complex) normF = 0 Scalc = Sbus.copy() any_q_control_issue = False converged = True logger.add_error('Not solving power flow because there is no slack bus') else: # run the power flow method that shall be run solution = solve(circuit=circuit, options=options, report=report, # is modified here V0=voltage_solution, Sbus=Sbus, Ibus=Ibus, pq=circuit.pq, pv=circuit.pv, ref=circuit.vd, pqpv=circuit.pqpv, logger=logger) if options.distributed_slack: # Distribute the slack power slack_power = Sbus[circuit.vd].real.sum() total_installed_power = circuit.bus_installed_power.sum() if total_installed_power > 0.0: delta = slack_power * circuit.bus_installed_power / total_installed_power # repeat power flow with the redistributed power solution = solve(circuit=circuit, options=options, report=report, # is modified here V0=solution.V, Sbus=Sbus + delta, Ibus=Ibus, pq=circuit.pq, pv=circuit.pv, ref=circuit.vd, pqpv=circuit.pqpv, logger=logger) # Compute the branches power and the slack buses power Sfb, Stb, If, It, Vbranch, loading, losses, \ flow_direction, Sbus = power_flow_post_process(calculation_inputs=circuit, Sbus=solution.Scalc, V=solution.V, branch_rates=branch_rates) # voltage, Sf, loading, losses, error, converged, Qpv results = PowerFlowResults(n=circuit.nbus, m=circuit.nbr, n_tr=circuit.ntr, n_hvdc=circuit.nhvdc, bus_names=circuit.bus_names, branch_names=circuit.branch_names, transformer_names=circuit.tr_names, hvdc_names=circuit.hvdc_names, bus_types=bus_types) results.Sbus = solution.Scalc * circuit.Sbase # MVA results.voltage = solution.V results.Sf = Sfb # in MVA already results.St = Stb # in MVA already results.If = If # in p.u. results.It = It # in p.u. results.ma = solution.ma results.theta = solution.theta results.Beq = solution.Beq results.Vbranch = Vbranch results.loading = loading results.losses = losses results.flow_direction = flow_direction results.transformer_tap_module = solution.ma[circuit.transformer_idx] results.convergence_reports.append(report) results.Qpv = Sbus.imag[circuit.pv] # HVDC results are gathered in the multi island power flow function due to their nature return results
def NR_I_LS(Ybus, Sbus_sp, V0, Ibus_sp, pv, pq, tol, max_it=15, acceleration_parameter=0.5) -> NumericPowerFlowResults: """ Solves the power flow using a full Newton's method in current equations with current mismatch with line search Args: Ybus: Admittance matrix Sbus_sp: Array of nodal specified power injections V0: Array of nodal voltages (initial solution) Ibus_sp: Array of nodal specified 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 acceleration_parameter: value used to correct bad iterations Returns: Voltage solution, converged?, error, calculated power injections @Author: Santiago Penate Vera """ start = time.time() # initialize back_track_counter = 0 back_track_iterations = 0 alpha = 1e-4 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 # j3:j4 - V angle of pq buses j3 = j2 j4 = j2 + npq # j5:j6 - V mag of pq buses j5 = j4 j6 = j4 + npq # evaluate F(x0) Icalc = Ybus * V - Ibus_sp dI = np.conj(Sbus_sp / V) - Icalc # compute the mismatch F = np.r_[dI[pvpq].real, dI[pq].imag] normF = np.linalg.norm(F, np.Inf) # check tolerance if normF < tol: converged = 1 # do Newton iterations while not converged and iter_ < max_it: # update iteration counter iter_ += 1 # evaluate Jacobian J = Jacobian_I(Ybus, V, pq, pvpq) # compute update step dx = linear_solver(J, F) # reassign the solution vector dVa[pvpq] = dx[j1:j4] dVm[pq] = dx[j5:j6] # update voltage the Newton way (mu=1) mu_ = 1.0 Vm += mu_ * dVm Va += mu_ * dVa Vnew = Vm * np.exp(1j * Va) # compute the mismatch function f(x_new) Icalc = Ybus * Vnew - Ibus_sp dI = np.conj(Sbus_sp / Vnew) - Icalc Fnew = np.r_[dI[pvpq].real, dI[pq].imag] normFnew = np.linalg.norm(Fnew, np.Inf) cond = normF < normFnew # condition to back track (no improvement at all) if not cond: back_track_counter += 1 l_iter = 0 while cond and l_iter < max_it and mu_ > tol: # line search back # reset voltage Va = np.angle(V) Vm = np.abs(V) # 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(1j * Va) # compute the mismatch function f(x_new) Icalc = Ybus * Vnew - Ibus_sp dI = np.conj(Sbus_sp / Vnew) - Icalc Fnew = np.r_[dI[pvpq].real, dI[pq].imag] normFnew = np.linalg.norm(Fnew, np.Inf) cond = normF < normFnew l_iter += 1 back_track_iterations += 1 # update calculation variables V = Vnew F = Fnew # check for convergence normF = normFnew if normF < tol: converged = 1 end = time.time() elapsed = end - start Scalc = V * np.conj(Icalc) 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 = AC_jacobian(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 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 = AC_jacobian(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 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 = AC_jacobian(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)
def NRD_LS(Ybus, Sbus, V0, Ibus, pv, pq, tol, max_it=15, acceleration_parameter=0.5, error_registry=None) -> 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 tol: Tolerance :param max_it: Maximum number of iterations :param acceleration_parameter: parameter used to correct the "bad" iterations, typically 0.5 :param error_registry: list to store the error for plotting :return: Voltage solution, converged?, error, calculated power injections """ start = time.time() use_norm_error = True # 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) # 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 if use_norm_error: norm_f = np.linalg.norm(f, np.Inf) else: 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 J1, J4 = Jacobian_decoupled(Ybus, V, Ibus, pq, pvpq) # compute update step and reassign the solution vector dVa[pvpq] = linear_solver(J1, f[pvpq]) dVm[pq] = linear_solver(J4, f[pq]) # 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) dS = Vnew * np.conj(Ybus * Vnew - Ibus) - Sbus # complex power mismatch f_new = np.r_[dS[pvpq].real, dS[pq].imag] # concatenate to form the mismatch function if use_norm_error: norm_f_new = np.linalg.norm(f_new, np.Inf) else: 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) dS = Vnew * np.conj(Ybus * Vnew - Ibus) - Sbus # complex power mismatch f_new = np.r_[ dS[pvpq].real, dS[pq].imag] # concatenate to form the mismatch function if use_norm_error: norm_f_new = np.linalg.norm(f_new, np.Inf) else: 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 use_norm_error: norm_f = np.linalg.norm(f_new, np.Inf) else: norm_f = 0.5 * f_new.dot(f_new) if error_registry is not None: error_registry.append(norm_f) if norm_f < tol: converged = 1 end = time.time() elapsed = end - start return NumericPowerFlowResults(V, converged, norm_f, Scalc, None, None, None, iter_, elapsed)
def ContinuousNR(Ybus, Sbus, V0, Ibus, pv, pq, tol, max_it=15) -> NumericPowerFlowResults: """ Solves the power flow using a full Newton's method with the backtrack improvement algorithm 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.copy() # 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 # j3:j4 - V angle of pq buses j3 = j2 j4 = j2 + npq # j5:j6 - V mag of pq buses j5 = j4 j6 = j4 + npq # evaluate F(x0) Scalc = V * np.conj(Ybus * V - Ibus) mis = Scalc - Sbus # compute the mismatch F = np.r_[mis[pv].real, mis[pq].real, mis[pq].imag] # check tolerance normF = np.linalg.norm(F, np.Inf) converged = normF < tol dt = 1.0 # Compose x x = np.zeros(2 * npq + npv) Va = np.angle(V) Vm = np.abs(V) # do Newton iterations while not converged and iter_ < max_it: # update iteration counter iter_ += 1 x[j1:j4] = Va[pvpq] x[j5:j6] = Vm[pq] # Compute the Runge-Kutta steps k1 = compute_fx(x, Ybus, Sbus, Ibus, pq, pv, pvpq, j1, j2, j3, j4, j5, j6, Va, Vm) k2 = compute_fx(x + 0.5 * dt * k1, Ybus, Sbus, Ibus, pq, pv, pvpq, j1, j2, j3, j4, j5, j6, Va, Vm) k3 = compute_fx(x + 0.5 * dt * k2, Ybus, Sbus, Ibus, pq, pv, pvpq, j1, j2, j3, j4, j5, j6, Va, Vm) k4 = compute_fx(x + dt * k3, Ybus, Sbus, Ibus, pq, pv, pvpq, j1, j2, j3, j4, j5, j6, Va, Vm) x -= dt * (k1 + 2.0 * k2 + 2.0 * k3 + k4) / 6.0 # reassign the solution vector Va[pvpq] = x[j1:j4] Vm[pq] = x[j5:j6] V = Vm * np.exp(1j * Va) # voltage mismatch # evaluate F(x) Scalc = V * np.conj(Ybus * V - Ibus) mis = Scalc - Sbus # complex power mismatch F = np.r_[mis[pv].real, mis[pq].real, mis[pq].imag] # concatenate again # check for convergence normF = np.linalg.norm(F, np.Inf) if normF > 0.01: dt = max(dt * 0.985, 0.75) else: dt = min(dt * 1.015, 0.75) print(dt) converged = normF < tol end = time.time() elapsed = end - start return NumericPowerFlowResults(V, converged, normF, Scalc, None, None, None, iter_, elapsed)
def LM_ACDC(nc: "SnapshotData", Vbus, Sbus, tolerance=1e-6, max_iter=4, verbose=False) -> 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. :param nc: SnapshotData instance :param tolerance: maximum error allowed :param max_iter: maximum number of iterations :return: """ start = time.time() # initialize the variables nb = nc.nbus nl = nc.nbr V = Vbus S0 = Sbus Va = np.angle(V) Vm = np.abs(V) Vmfset = nc.branch_data.vf_set[:, 0] m = nc.branch_data.m[:, 0].copy() theta = nc.branch_data.theta[:, 0].copy() Beq = nc.branch_data.Beq[:, 0].copy() Gsw = nc.branch_data.G0[:, 0] Pfset = nc.branch_data.Pfset[:, 0] / nc.Sbase Qfset = nc.branch_data.Qfset[:, 0] / nc.Sbase Qtset = nc.branch_data.Qfset[:, 0] / nc.Sbase Kdp = nc.branch_data.Kdp k2 = nc.branch_data.k Cf = nc.Cf Ct = nc.Ct F = nc.F T = nc.T Ys = 1.0 / (nc.branch_data.R + 1j * nc.branch_data.X) Bc = nc.branch_data.B pq = nc.pq.copy().astype(int) pvpq_orig = np.r_[nc.pv, pq].astype(int) pvpq_orig.sort() # the elements of PQ that exist in the control indices Ivf and Ivt must be passed from the PQ to the PV list # otherwise those variables would be in two sets of equations i_ctrl_v = np.unique(np.r_[nc.VfBeqbus, nc.Vtmabus]) for val in pq: if val in i_ctrl_v: pq = pq[pq != val] # compose the new pvpq indices à la NR pv = np.unique(np.r_[i_ctrl_v, nc.pv]).astype(int) pv.sort() pvpq = np.r_[pv, pq].astype(int) npv = len(pv) npq = len(pq) if (npq + npv) > 0: # -------------------------------------------------------------------------- # variables dimensions in Jacobian sol_slicer = SolSlicer(npq, npv, len(nc.VfBeqbus), len(nc.Vtmabus), len(nc.iPfsh), len(nc.iQfma), len(nc.iBeqz), len(nc.iQtma), len(nc.iPfdp)) # ------------------------------------------------------------------------- # compute initial admittances Ybus, Yf, Yt, tap = compile_y_acdc(Cf=Cf, Ct=Ct, C_bus_shunt=nc.shunt_data.C_bus_shunt, shunt_admittance=nc.shunt_data.shunt_admittance[:, 0], shunt_active=nc.shunt_data.shunt_active[:, 0], ys=Ys, B=Bc, Sbase=nc.Sbase, m=m, theta=theta, Beq=Beq, Gsw=Gsw, mf=nc.branch_data.tap_f, mt=nc.branch_data.tap_t) # compute branch power flows If = Yf * V # complex current injected at "from" bus, Yf(br, :) * V; For in-service branches It = Yt * V # complex current injected at "to" bus, Yt(br, :) * V; For in-service branches Sf = V[F] * np.conj(If) # complex power injected at "from" bus St = V[T] * np.conj(It) # complex power injected at "to" bus # compute converter losses Gsw = compute_converter_losses(V=V, It=It, F=F, alpha1=nc.branch_data.alpha1, alpha2=nc.branch_data.alpha2, alpha3=nc.branch_data.alpha3, iVscL=nc.iVscL) # compute total mismatch dz, Scalc = compute_fx(Ybus=Ybus, V=V, Vm=Vm, Sbus=S0, Sf=Sf, St=St, Pfset=Pfset, Qfset=Qfset, Qtset=Qtset, Vmfset=Vmfset, Kdp=Kdp, F=F, pvpq=pvpq, pq=pq, iPfsh=nc.iPfsh, iQfma=nc.iQfma, iBeqz=nc.iBeqz, iQtma=nc.iQtma, iPfdp=nc.iPfdp, VfBeqbus=nc.VfBeqbus, Vtmabus=nc.Vtmabus) norm_f = np.max(np.abs(dz)) update_jacobian = True converged = norm_f < tolerance iter_ = 0 nu = 2.0 lbmda = 0 f_prev = 1e9 # very large number # 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_iter: # evaluate Jacobian if update_jacobian: H = fubm_jacobian(nb, nl, nc.iPfsh, nc.iPfdp, nc.iQfma, nc.iQtma, nc.iVtma, nc.iBeqz, nc.iBeqv, nc.VfBeqbus, nc.Vtmabus, F, T, Ys, k2, tap, m, Bc, Beq, Kdp, V, Ybus, Yf, Yt, Cf, Ct, pvpq, pq) if iter_ == 0: # compute this identity only once Idn = sp.diags(np.ones(H.shape[0])) # csc_matrix identity # system matrix # H1 = H^t H1 = H.transpose() # 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 = spsolve(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 # split the solution dVa, dVm, dBeq_v, dma_Vt, dtheta_Pf, dma_Qf, dBeq_z, dma_Qt, dtheta_Pd = sol_slicer.split(dx) # assign the new values Va[pvpq] -= dVa Vm[pq] -= dVm theta[nc.iPfsh] -= dtheta_Pf theta[nc.iPfdp] -= dtheta_Pd m[nc.iQfma] -= dma_Qf m[nc.iQtma] -= dma_Qt m[nc.iVtma] -= dma_Vt Beq[nc.iBeqz] -= dBeq_z Beq[nc.iBeqv] -= dBeq_v V = Vm * np.exp(1.0j * Va) else: update_jacobian = False lbmda *= nu nu *= 2.0 # compute initial admittances Ybus, Yf, Yt, tap = compile_y_acdc(Cf=Cf, Ct=Ct, C_bus_shunt=nc.shunt_data.C_bus_shunt, shunt_admittance=nc.shunt_data.shunt_admittance[:, 0], shunt_active=nc.shunt_data.shunt_active[:, 0], ys=Ys, B=Bc, Sbase=nc.Sbase, m=m, theta=theta, Beq=Beq, Gsw=Gsw, mf=nc.branch_data.tap_f, mt=nc.branch_data.tap_t) # compute branch power flows If = Yf * V # complex current injected at "from" bus, Yf(br, :) * V; For in-service branches It = Yt * V # complex current injected at "to" bus, Yt(br, :) * V; For in-service branches Sf = V[F] * np.conj(If) # complex power injected at "from" bus St = V[T] * np.conj(It) # complex power injected at "to" bus # compute converter losses Gsw = compute_converter_losses(V=V, It=It, F=F, alpha1=nc.branch_data.alpha1, alpha2=nc.branch_data.alpha2, alpha3=nc.branch_data.alpha3, iVscL=nc.iVscL) # check convergence dz, Scalc = compute_fx(Ybus=Ybus, V=V, Vm=Vm, Sbus=S0, Sf=Sf, St=St, Pfset=Pfset, Qfset=Qfset, Qtset=Qtset, Vmfset=Vmfset, Kdp=Kdp, F=F, pvpq=pvpq, pq=pq, iPfsh=nc.iPfsh, iQfma=nc.iQfma, iBeqz=nc.iBeqz, iQtma=nc.iQtma, iPfdp=nc.iPfdp, VfBeqbus=nc.VfBeqbus, Vtmabus=nc.Vtmabus) norm_f = np.max(np.abs(dz)) converged = norm_f < tolerance f_prev = f if verbose: print('dx:', dx) print('Va:', Va) print('Vm:', Vm) print('theta:', theta) print('ma:', m) print('Beq:', Beq) print('norm_f:', norm_f) # update iteration counter iter_ += 1 else: norm_f = 0 converged = True Scalc = S0 # V * np.conj(Ybus * V - Ibus) iter_ = 0 end = time.time() elapsed = end - start return NumericPowerFlowResults(V, converged, norm_f, Scalc, m, theta, Beq, iter_, elapsed)
def NR_LS_ACDC(nc: "SnapshotData", Vbus, Sbus, tolerance=1e-6, max_iter=4, mu_0=1.0, acceleration_parameter=0.05, verbose=False, t=0, control_q=ReactivePowerControlMode.NoControl) -> NumericPowerFlowResults: """ Newton-Raphson Line search with the FUBM formulation :param nc: SnapshotData instance :param tolerance: maximum error allowed :param max_iter: maximum number of iterations :param mu_0: :param acceleration_parameter: :param verbose: :param t: :param control_q: :return: """ start = time.time() # initialize the variables nb = nc.nbus nl = nc.nbr V = Vbus S0 = Sbus Va = np.angle(V) Vm = np.abs(V) Vmfset = nc.branch_data.vf_set[:, t] m = nc.branch_data.m[:, t].copy() theta = nc.branch_data.theta[:, t].copy() Beq = nc.branch_data.Beq[:, t].copy() Gsw = nc.branch_data.G0[:, t] Pfset = nc.branch_data.Pfset[:, t] / nc.Sbase Qfset = nc.branch_data.Qfset[:, t] / nc.Sbase Qtset = nc.branch_data.Qfset[:, t] / nc.Sbase Qmin = nc.Qmin_bus[t, :] Qmax = nc.Qmax_bus[t, :] Kdp = nc.branch_data.Kdp k2 = nc.branch_data.k Cf = nc.Cf.tocsc() Ct = nc.Ct.tocsc() F = nc.F T = nc.T Ys = 1.0 / (nc.branch_data.R + 1j * nc.branch_data.X) Bc = nc.branch_data.B pq = nc.pq.copy().astype(int) pvpq_orig = np.r_[nc.pv, pq].astype(int) pvpq_orig.sort() # the elements of PQ that exist in the control indices Ivf and Ivt must be passed from the PQ to the PV list # otherwise those variables would be in two sets of equations i_ctrl_v = np.unique(np.r_[nc.VfBeqbus, nc.Vtmabus]) for val in pq: if val in i_ctrl_v: pq = pq[pq != val] # compose the new pvpq indices à la NR pv = np.unique(np.r_[i_ctrl_v, nc.pv]).astype(int) pv.sort() pvpq = np.r_[pv, pq].astype(int) npv = len(pv) npq = len(pq) # -------------------------------------------------------------------------- # variables dimensions in Jacobian sol_slicer = SolSlicer(npq, npv, len(nc.VfBeqbus), len(nc.Vtmabus), len(nc.iPfsh), len(nc.iQfma), len(nc.iBeqz), len(nc.iQtma), len(nc.iPfdp)) # ------------------------------------------------------------------------- # compute initial admittances Ybus, Yf, Yt, tap = compile_y_acdc(Cf=Cf, Ct=Ct, C_bus_shunt=nc.shunt_data.C_bus_shunt, shunt_admittance=nc.shunt_data.shunt_admittance[:, 0], shunt_active=nc.shunt_data.shunt_active[:, 0], ys=Ys, B=Bc, Sbase=nc.Sbase, m=m, theta=theta, Beq=Beq, Gsw=Gsw, mf=nc.branch_data.tap_f, mt=nc.branch_data.tap_t) # compute branch power flows If = Yf * V # complex current injected at "from" bus, Yf(br, :) * V; For in-service branches It = Yt * V # complex current injected at "to" bus, Yt(br, :) * V; For in-service branches Sf = V[F] * np.conj(If) # complex power injected at "from" bus St = V[T] * np.conj(It) # complex power injected at "to" bus # compute converter losses Gsw = compute_converter_losses(V=V, It=It, F=F, alpha1=nc.branch_data.alpha1, alpha2=nc.branch_data.alpha2, alpha3=nc.branch_data.alpha3, iVscL=nc.iVscL) # compute total mismatch fx, Scalc = compute_fx(Ybus=Ybus, V=V, Vm=Vm, Sbus=S0, Sf=Sf, St=St, Pfset=Pfset, Qfset=Qfset, Qtset=Qtset, Vmfset=Vmfset, Kdp=Kdp, F=F, pvpq=pvpq, pq=pq, iPfsh=nc.iPfsh, iQfma=nc.iQfma, iBeqz=nc.iBeqz, iQtma=nc.iQtma, iPfdp=nc.iPfdp, VfBeqbus=nc.VfBeqbus, Vtmabus=nc.Vtmabus) norm_f = np.max(np.abs(fx)) # ------------------------------------------------------------------------- converged = norm_f < tolerance iterations = 0 while not converged and iterations < max_iter: # compute the Jacobian J = fubm_jacobian(nb, nl, nc.iPfsh, nc.iPfdp, nc.iQfma, nc.iQtma, nc.iVtma, nc.iBeqz, nc.iBeqv, nc.VfBeqbus, nc.Vtmabus, F, T, Ys, k2, tap, m, Bc, Beq, Kdp, V, Ybus, Yf, Yt, Cf, Ct, pvpq, pq) # solve the linear system dx = sp.linalg.spsolve(J, -fx) # split the solution dVa, dVm, dBeq_v, dma_Vt, dtheta_Pf, dma_Qf, dBeq_z, dma_Qt, dtheta_Pd = sol_slicer.split(dx) # set the restoration values prev_Vm = Vm.copy() prev_Va = Va.copy() prev_m = m.copy() prev_theta = theta.copy() prev_Beq = Beq.copy() prev_Scalc = Scalc.copy() mu = mu_0 # ideally 1.0 cond = True l_iter = 0 norm_f_new = 0.0 while cond and l_iter < max_iter and mu > tolerance: # backtracking: if all goes well it is only done 1 time # 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() m = prev_m.copy() theta = prev_theta.copy() Beq = prev_Beq.copy() # assign the new values Va[pvpq] += dVa * mu Vm[pq] += dVm * mu theta[nc.iPfsh] += dtheta_Pf * mu theta[nc.iPfdp] += dtheta_Pd * mu m[nc.iQfma] += dma_Qf * mu m[nc.iQtma] += dma_Qt * mu m[nc.iVtma] += dma_Vt * mu Beq[nc.iBeqz] += dBeq_z * mu Beq[nc.iBeqv] += dBeq_v * mu V = Vm * np.exp(1j * Va) # compute admittances Ybus, Yf, Yt, tap = compile_y_acdc(Cf=Cf, Ct=Ct, C_bus_shunt=nc.shunt_data.C_bus_shunt, shunt_admittance=nc.shunt_data.shunt_admittance[:, 0], shunt_active=nc.shunt_data.shunt_active[:, 0], ys=Ys, B=Bc, Sbase=nc.Sbase, m=m, theta=theta, Beq=Beq, Gsw=Gsw, mf=nc.branch_data.tap_f, mt=nc.branch_data.tap_t) # compute branch power flows If = Yf * V # complex current injected at "from" bus It = Yt * V # complex current injected at "to" bus Sf = V[F] * np.conj(If) # complex power injected at "from" bus St = V[T] * np.conj(It) # complex power injected at "to" bus # compute converter losses Gsw = compute_converter_losses(V=V, It=It, F=F, alpha1=nc.branch_data.alpha1, alpha2=nc.branch_data.alpha2, alpha3=nc.branch_data.alpha3, iVscL=nc.iVscL) # compute total mismatch fx, Scalc = compute_fx(Ybus=Ybus, V=V, Vm=Vm, Sbus=S0, Sf=Sf, St=St, Pfset=Pfset, Qfset=Qfset, Qtset=Qtset, Vmfset=Vmfset, Kdp=Kdp, F=F, pvpq=pvpq, pq=pq, iPfsh=nc.iPfsh, iQfma=nc.iQfma, iBeqz=nc.iBeqz, iQtma=nc.iQtma, iPfdp=nc.iPfdp, VfBeqbus=nc.VfBeqbus, Vtmabus=nc.Vtmabus) norm_f_new = np.max(np.abs(fx)) cond = norm_f_new > norm_f # condition to back track (no improvement at all) mu *= acceleration_parameter l_iter += 1 if l_iter > 1 and norm_f_new > norm_f: # 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() m = prev_m.copy() theta = prev_theta.copy() Beq = prev_Beq.copy() V = Vm * np.exp(1j * Va) end = time.time() elapsed = end - start # set the state for the next solver nc.branch_data.m[:, 0] = m nc.branch_data.theta[:, 0] = theta nc.branch_data.Beq[:, 0] = Beq return NumericPowerFlowResults(V, converged, norm_f_new, prev_Scalc, m, theta, Beq, iterations, elapsed) else: # the iteration was ok, check the controls if the error is small enough if norm_f < 1e-2: for idx in nc.iVscL: # correct m (tap modules) if m[idx] < nc.branch_data.m_min[idx]: m[idx] = nc.branch_data.m_min[idx] elif m[idx] > nc.branch_data.m_max[idx]: m[idx] = nc.branch_data.m_max[idx] # correct theta (tap angles) if theta[idx] < nc.branch_data.theta_min[idx]: theta[idx] = nc.branch_data.theta_min[idx] elif theta[idx] > nc.branch_data.theta_max[idx]: theta[idx] = nc.branch_data.theta_max[idx] # 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 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, S0, pv, pq, pvpq = control_q_inside_method(Scalc, S0, pv, pq, pvpq, Qmin, Qmax) if n_changes > 0: # adjust internal variables to the new pq|pv values npv = len(pv) npq = len(pq) # re declare the slicer because the indices of pq and pv changed sol_slicer = SolSlicer(npq, npv, len(nc.VfBeqbus), len(nc.Vtmabus), len(nc.iPfsh), len(nc.iQfma), len(nc.iBeqz), len(nc.iQtma), len(nc.iPfdp)) # recompute the mismatch, based on the new S0 fx, Scalc = compute_fx(Ybus=Ybus, V=V, Vm=Vm, Sbus=S0, Sf=Sf, St=St, Pfset=Pfset, Qfset=Qfset, Qtset=Qtset, Vmfset=Vmfset, Kdp=Kdp, F=F, pvpq=pvpq, pq=pq, iPfsh=nc.iPfsh, iQfma=nc.iQfma, iBeqz=nc.iBeqz, iQtma=nc.iQtma, iPfdp=nc.iPfdp, VfBeqbus=nc.VfBeqbus, Vtmabus=nc.Vtmabus) norm_f_new = np.max(np.abs(fx)) # set the mismatch to the new mismatch norm_f = norm_f_new if verbose: print('dx:', dx) print('Va:', Va) print('Vm:', Vm) print('theta:', theta) print('ma:', m) print('Beq:', Beq) print('norm_f:', norm_f) iterations += 1 converged = norm_f <= tolerance end = time.time() elapsed = end - start return NumericPowerFlowResults(V, converged, norm_f, Scalc, m, theta, Beq, iterations, elapsed)
def FDPF(Vbus, Sbus, Ibus, Ybus, B1, B2, pq, pv, pqpv, tol=1e-9, max_it=100) -> NumericPowerFlowResults: """ Fast decoupled power flow :param Vbus: :param Sbus: :param Ibus: :param Ybus: :param B1: :param B2: :param pq: :param pv: :param pqpv: :param tol: :param max_it: :return: """ start = time.time() # pvpq = np.r_[pv, pq] # set voltage vector for the iterations voltage = Vbus.copy() Va = np.angle(voltage) Vm = np.abs(voltage) # Factorize B1 and B2 J1 = splu(B1[np.ix_(pqpv, pqpv)]) J2 = splu(B2[np.ix_(pq, pq)]) # evaluate initial mismatch Scalc = voltage * np.conj(Ybus * voltage - Ibus) mis = (Scalc - Sbus) / Vm # complex power mismatch dP = mis[pqpv].real dQ = mis[pq].imag if len(pqpv) > 0: normP = norm(dP, Inf) normQ = norm(dQ, Inf) converged = normP < tol and normQ < tol # iterate iter_ = 0 while not converged and iter_ < max_it: iter_ += 1 # ----------------------------- P iteration to update Va ---------------------- # solve voltage angles dVa = J1.solve(dP) # update voltage Va[pqpv] -= dVa voltage = Vm * exp(1j * Va) # evaluate mismatch Scalc = voltage * conj(Ybus * voltage - Ibus) mis = (Scalc - Sbus) / Vm # complex power mismatch dP = mis[pqpv].real dQ = mis[pq].imag normP = norm(dP, Inf) normQ = norm(dQ, Inf) if normP < tol and normQ < tol: converged = True else: # ----------------------------- Q iteration to update Vm ---------------------- # Solve voltage modules dVm = J2.solve(dQ) # update voltage Vm[pq] -= dVm voltage = Vm * exp(1j * Va) # evaluate mismatch Scalc = voltage * conj(Ybus * voltage - Ibus) mis = (Scalc - Sbus) / Vm # complex power mismatch dP = mis[pqpv].real dQ = mis[pq].imag normP = norm(dP, Inf) normQ = norm(dQ, Inf) if normP < tol and normQ < tol: converged = True F = r_[dP, dQ] # concatenate again normF = norm(F, Inf) else: converged = True iter_ = 0 normF = 0 end = time.time() elapsed = end - start return NumericPowerFlowResults(voltage, converged, normF, Scalc, None, None, None, iter_, elapsed)
def NR_LS_ACDC(nc: "SnapshotData", tolerance=1e-6, max_iter=4, mu_0=1.0, acceleration_parameter=0.05) -> NumericPowerFlowResults: """ Newton-Raphson Line search with the FUBM formulation :param nc: SnapshotData instance :param tolerance: maximum error allowed :param max_iter: maximum number of iterations :param mu_0: :param acceleration_parameter: :return: """ start = time.time() # compute the indices of the converter/transformer variables from their control strategies # iPfsh, iQfma, iBeqz, iBeqv, iVtma, iQtma, iPfdp, iVscL, VfBeqbus, Vtmabus = nc.determine_branch_indices() # initialize the variables nb = nc.nbus nl = nc.nbr V = nc.Vbus S0 = nc.Sbus Va = np.angle(V) Vm = np.abs(V) Vmfset = nc.branch_data.vf_set[:, 0] m = nc.branch_data.m[:, 0].copy() theta = nc.branch_data.theta[:, 0].copy() Beq = nc.branch_data.Beq[:, 0].copy() Gsw = nc.branch_data.G0[:, 0] Pfset = nc.branch_data.Pfset[:, 0] / nc.Sbase Qfset = nc.branch_data.Qfset[:, 0] / nc.Sbase Qtset = nc.branch_data.Qfset[:, 0] / nc.Sbase Kdp = nc.branch_data.Kdp k2 = nc.branch_data.k Cf = nc.Cf Ct = nc.Ct F = nc.F T = nc.T Ys = 1.0 / (nc.branch_data.R + 1j * nc.branch_data.X) Bc = nc.branch_data.B pq = nc.pq.copy().astype(int) pvpq_orig = np.r_[nc.pv, pq].astype(int) pvpq_orig.sort() # the elements of PQ that exist in the control indices Ivf and Ivt must be passed from the PQ to the PV list # otherwise those variables would be in two sets of equations i_ctrl_v = np.unique(np.r_[nc.VfBeqbus, nc.Vtmabus]) for val in pq: if val in i_ctrl_v: pq = pq[pq != val] # compose the new pvpq indices à la NR pv = np.unique(np.r_[i_ctrl_v, nc.pv]).astype(int) pv.sort() pvpq = np.r_[pv, pq].astype(int) npv = len(pv) npq = len(pq) # -------------------------------------------------------------------------- # variables dimensions in Jacobian a0 = 0 a1 = a0 + npq + npv a2 = a1 + npq a3 = a2 + len(nc.iPfsh) a4 = a3 + len(nc.iQfma) a5 = a4 + len(nc.iBeqz) a6 = a5 + len(nc.VfBeqbus) a7 = a6 + len(nc.Vtmabus) a8 = a7 + len(nc.iQtma) a9 = a8 + len(nc.iPfdp) # ------------------------------------------------------------------------- # compute initial admittances Ybus, Yf, Yt, tap = compile_y_acdc( Cf=Cf, Ct=Ct, C_bus_shunt=nc.shunt_data.C_bus_shunt, shunt_admittance=nc.shunt_data.shunt_admittance[:, 0], shunt_active=nc.shunt_data.shunt_active[:, 0], ys=Ys, B=Bc, Sbase=nc.Sbase, m=m, theta=theta, Beq=Beq, Gsw=Gsw, mf=nc.branch_data.tap_f, mt=nc.branch_data.tap_t) # compute branch power flows If = Yf * V # FUBM- complex current injected at "from" bus, Yf(br, :) * V; For in-service branches It = Yt * V # FUBM- complex current injected at "to" bus, Yt(br, :) * V; For in-service branches Sf = V[F] * np.conj(If) # FUBM- complex power injected at "from" bus St = V[T] * np.conj(It) # FUBM- complex power injected at "to" bus # compute converter losses Gsw = compute_converter_losses(V=V, It=It, F=F, alpha1=nc.branch_data.alpha1, alpha2=nc.branch_data.alpha2, alpha3=nc.branch_data.alpha3, iVscL=nc.iVscL) # compute total mismatch fx, Scalc = compute_fx(Ybus=Ybus, V=V, Vm=Vm, Sbus=S0, Sf=Sf, St=St, Pfset=Pfset, Qfset=Qfset, Qtset=Qtset, Vmfset=Vmfset, Kdp=Kdp, F=F, pvpq=pvpq, pq=pq, iPfsh=nc.iPfsh, iQfma=nc.iQfma, iBeqz=nc.iBeqz, iQtma=nc.iQtma, iPfdp=nc.iPfdp, VfBeqbus=nc.VfBeqbus, Vtmabus=nc.Vtmabus) norm_f = np.max(np.abs(fx)) # ------------------------------------------------------------------------- converged = norm_f < tolerance iterations = 0 back_track_counter = 0 while not converged and iterations < max_iter: # compute the Jacobian J = fubm_jacobian(nb, nl, nc.iPfsh, nc.iPfdp, nc.iQfma, nc.iQtma, nc.iVtma, nc.iBeqz, nc.iBeqv, nc.VfBeqbus, nc.Vtmabus, F, T, Ys, k2, tap, m, Bc, Beq, Kdp, V, Ybus, Yf, Yt, Cf, Ct, pvpq, pq) # solve the linear system dx = sp.linalg.spsolve(J, -fx) # split the solution dVa = dx[a0:a1] dVm = dx[a1:a2] dtheta_Pf = dx[a2:a3] dma_Qf = dx[a3:a4] dBeq_z = dx[a4:a5] dBeq_v = dx[a5:a6] dma_Vt = dx[a6:a7] dma_Qt = dx[a7:a8] dtheta_Pd = dx[a8:a9] # set the restoration values prev_Vm = Vm.copy() prev_Va = Va.copy() prev_m = m.copy() prev_theta = theta.copy() prev_Beq = Beq.copy() prev_Scalc = Scalc.copy() mu = mu_0 # ideally 1.0 cond = True l_iter = 0 norm_f_new = 0.0 while cond and l_iter < max_iter and mu > tolerance: # backtracking: if all goes well it is only done 1 time # 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() m = prev_m.copy() theta = prev_theta.copy() Beq = prev_Beq.copy() # assign the new values Va[pvpq] += dVa * mu Vm[pq] += dVm * mu theta[nc.iPfsh] += dtheta_Pf * mu theta[nc.iPfdp] += dtheta_Pd * mu m[nc.iQfma] += dma_Qf * mu m[nc.iQtma] += dma_Qt * mu m[nc.iVtma] += dma_Vt * mu Beq[nc.iBeqz] += dBeq_z * mu Beq[nc.iBeqv] += dBeq_v * mu V = Vm * np.exp(1j * Va) for idx in nc.iVscL: # correct m (tap modules) if m[idx] < nc.branch_data.m_min[idx]: m[idx] = nc.branch_data.m_min[idx] elif m[idx] > nc.branch_data.m_max[idx]: m[idx] = nc.branch_data.m_max[idx] # correct theta (tap angles) if theta[idx] < nc.branch_data.theta_min[idx]: theta[idx] = nc.branch_data.theta_min[idx] elif theta[idx] > nc.branch_data.theta_max[idx]: theta[idx] = nc.branch_data.theta_max[idx] # compute admittances Ybus, Yf, Yt, tap = compile_y_acdc( Cf=Cf, Ct=Ct, C_bus_shunt=nc.shunt_data.C_bus_shunt, shunt_admittance=nc.shunt_data.shunt_admittance[:, 0], shunt_active=nc.shunt_data.shunt_active[:, 0], ys=Ys, B=Bc, Sbase=nc.Sbase, m=m, theta=theta, Beq=Beq, Gsw=Gsw, mf=nc.branch_data.tap_f, mt=nc.branch_data.tap_t) # compute branch power flows If = Yf * V # complex current injected at "from" bus It = Yt * V # complex current injected at "to" bus Sf = V[F] * np.conj(If) # complex power injected at "from" bus St = V[T] * np.conj(It) # complex power injected at "to" bus # compute converter losses Gsw = compute_converter_losses(V=V, It=It, F=F, alpha1=nc.branch_data.alpha1, alpha2=nc.branch_data.alpha2, alpha3=nc.branch_data.alpha3, iVscL=nc.iVscL) # compute total mismatch fx, Scalc = compute_fx(Ybus=Ybus, V=V, Vm=Vm, Sbus=S0, Sf=Sf, St=St, Pfset=Pfset, Qfset=Qfset, Qtset=Qtset, Vmfset=Vmfset, Kdp=Kdp, F=F, pvpq=pvpq, pq=pq, iPfsh=nc.iPfsh, iQfma=nc.iQfma, iBeqz=nc.iBeqz, iQtma=nc.iQtma, iPfdp=nc.iPfdp, VfBeqbus=nc.VfBeqbus, Vtmabus=nc.Vtmabus) norm_f_new = np.max(np.abs(fx)) cond = norm_f_new > norm_f # condition to back track (no improvement at all) mu *= acceleration_parameter back_track_counter += 1 l_iter += 1 if l_iter > 1: # 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() m = prev_m.copy() theta = prev_theta.copy() Beq = prev_Beq.copy() V = Vm * np.exp(1j * Va) end = time.time() elapsed = end - start # set the state for the next solver nc.branch_data.m[:, 0] = m nc.branch_data.theta[:, 0] = theta nc.branch_data.Beq[:, 0] = Beq return NumericPowerFlowResults(V, converged, norm_f, prev_Scalc, m, theta, Beq, iterations, elapsed) else: norm_f = norm_f_new print('dx:', dx) print('Va:', Va) print('Vm:', Vm) print('theta:', theta) print('ma:', m) print('Beq:', Beq) print('norm_f:', norm_f) iterations += 1 converged = norm_f <= tolerance end = time.time() elapsed = end - start return NumericPowerFlowResults(V, converged, norm_f, Scalc, m, theta, Beq, iterations, elapsed)