Ejemplo n.º 1
0
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)
Ejemplo n.º 2
0
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)
Ejemplo n.º 3
0
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)
Ejemplo n.º 4
0
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)