Source code for VeraGridEngine.Simulations.PowerFlow.power_flow_worker

# This Source Code Form is subject to the terms of the Mozilla Public
# License, v. 2.0. If a copy of the MPL was not distributed with this
# file, You can obtain one at https://mozilla.org/MPL/2.0/.
# SPDX-License-Identifier: MPL-2.0
from __future__ import annotations
import numpy as np
from typing import Union, Dict, Tuple, TYPE_CHECKING

import VeraGridEngine.Simulations.PowerFlow as pflw
from VeraGridEngine.enumerations import SolverType, GeneratorControlMode, BusMode
from VeraGridEngine.basic_structures import Logger, ConvergenceReport
from VeraGridEngine.Simulations.PowerFlow.power_flow_results import PowerFlowResults
from VeraGridEngine.Simulations.PowerFlow.power_flow_options import PowerFlowOptions
from VeraGridEngine.Simulations.PowerFlow.power_flow_results import NumericPowerFlowResults
from VeraGridEngine.Simulations.PowerFlow.Formulations.pf_basic_formulation import PfBasicFormulation
from VeraGridEngine.Simulations.PowerFlow.Formulations.pf_full_acdc_with_negative_poles import PfAcDcWithNegativePoles
from VeraGridEngine.Simulations.PowerFlow.NumericalMethods.newton_raphson_fx import newton_raphson_fx
from VeraGridEngine.Simulations.PowerFlow.NumericalMethods.common_functions import (
    split_bus_quantity,
    split_reactive_power_between_generators_and_batteries,
    split_slack_bus_quantity_between_generators_and_batteries,
)
from VeraGridEngine.Simulations.PowerFlow.NumericalMethods.powell_fx import powell_fx
from VeraGridEngine.Simulations.PowerFlow.NumericalMethods.levenberg_marquadt_fx import levenberg_marquardt_fx
from VeraGridEngine.Topology.simulation_indices import SimulationIndices
from VeraGridEngine.DataStructures.numerical_circuit import NumericalCircuit
from VeraGridEngine.Devices.multi_circuit import MultiCircuit
from VeraGridEngine.Compilers.circuit_to_data import compile_numerical_circuit_at
from VeraGridEngine.Simulations.PowerFlow.NumericalMethods.discrete_controls import compute_slack_distribution
from VeraGridEngine.Devices.Substation.bus import Bus
from VeraGridEngine.Devices.Aggregation.area import Area
from VeraGridEngine.basic_structures import CxVec, Vec

if TYPE_CHECKING:  # Only imports the below statements during type checking
    from VeraGridEngine.Compilers.circuit_to_data import VALID_OPF_RESULTS


def __solve_island_complete_support(nc: NumericalCircuit,
                                    indices: SimulationIndices,
                                    options: PowerFlowOptions,
                                    V0: CxVec,
                                    S0: CxVec,
                                    logger=Logger()) -> Tuple[NumericPowerFlowResults, ConvergenceReport]:
    """
    Run a power flow simulation using the selected method (no outer loop controls).
    This routine supports all controls, VSC's and Hvdc links
    Does not require grids to be split by HvdcLines
    :param nc: SnapshotData circuit, this ensures on-demand admittances computation
    :param indices: SimulationIndices
    :param options: PowerFlow options
    :param V0: Array of initial voltages
    :param S0: Array of power Injections
    :param logger: Logger
    :return: NumericPowerFlowResults
    """

    logger.add_info('Using the complete support power flow method')

    report = ConvergenceReport()
    if options.retry_with_other_methods:
        solver_list = [SolverType.NR,
                       SolverType.PowellDogLeg,
                       SolverType.LM]

        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 = false to enter the loop
    solver_idx = 0

    # set the initial value
    Qmax, Qmin = nc.get_reactive_power_limits()
    I0 = nc.get_current_injections_pu()
    Y0 = nc.get_admittance_injections_pu()

    if len(indices.vd) == 0 or len(indices.no_slack) == 0:
        solution = NumericPowerFlowResults(V=np.zeros(len(S0), dtype=complex),
                                           Scalc=S0,
                                           m=nc.active_branch_data.tap_module,
                                           tau=nc.active_branch_data.tap_angle,
                                           Sf=np.zeros(nc.nbr, dtype=complex),
                                           St=np.zeros(nc.nbr, dtype=complex),
                                           If=np.zeros(nc.nbr, dtype=complex),
                                           It=np.zeros(nc.nbr, dtype=complex),
                                           loading=np.zeros(nc.nbr, dtype=complex),
                                           losses=np.zeros(nc.nbr, dtype=complex),
                                           Pfp_vsc=np.zeros(nc.nvsc, dtype=float),
                                           Pfn_vsc=np.zeros(nc.nvsc, dtype=float),
                                           St_vsc=np.zeros(nc.nvsc, dtype=complex),
                                           If_vsc=np.zeros(nc.nvsc, dtype=float),
                                           It_vsc=np.zeros(nc.nvsc, dtype=complex),
                                           losses_vsc=np.zeros(nc.nvsc, dtype=float),
                                           loading_vsc=np.zeros(nc.nvsc, dtype=float),
                                           Sf_hvdc=np.zeros(nc.nhvdc, dtype=complex),
                                           St_hvdc=np.zeros(nc.nhvdc, dtype=complex),
                                           losses_hvdc=np.zeros(nc.nhvdc, dtype=complex),
                                           loading_hvdc=np.zeros(nc.nhvdc, dtype=complex),
                                           converged=False,
                                           norm_f=1e200,
                                           iterations=0,
                                           elapsed=0)

        # method, converged: bool, error: float, elapsed: float, iterations: int
        report.add(method=SolverType.NoSolver, converged=True, error=0.0, elapsed=0.0, iterations=0)
        logger.add_error('Not solving power flow because there is no slack bus')
        return solution, report

    else:
        final_solution = NumericPowerFlowResults(V=V0,
                                                 converged=False,
                                                 norm_f=1e200,
                                                 Scalc=S0,
                                                 m=nc.active_branch_data.tap_module,
                                                 tau=nc.active_branch_data.tap_angle,
                                                 Sf=np.zeros(nc.nbr, dtype=complex),
                                                 St=np.zeros(nc.nbr, dtype=complex),
                                                 If=np.zeros(nc.nbr, dtype=complex),
                                                 It=np.zeros(nc.nbr, dtype=complex),
                                                 loading=np.zeros(nc.nbr, dtype=complex),
                                                 losses=np.zeros(nc.nbr, dtype=complex),
                                                 Pfp_vsc=np.zeros(nc.nvsc, dtype=float),
                                                 Pfn_vsc=np.zeros(nc.nvsc, dtype=float),
                                                 St_vsc=np.zeros(nc.nvsc, dtype=complex),
                                                 If_vsc=np.zeros(nc.nvsc, dtype=float),
                                                 It_vsc=np.zeros(nc.nvsc, dtype=complex),
                                                 losses_vsc=np.zeros(nc.nvsc, dtype=float),
                                                 loading_vsc=np.zeros(nc.nvsc, dtype=float),
                                                 Sf_hvdc=np.zeros(nc.nhvdc, dtype=complex),
                                                 St_hvdc=np.zeros(nc.nhvdc, dtype=complex),
                                                 losses_hvdc=np.zeros(nc.nhvdc, dtype=complex),
                                                 loading_hvdc=np.zeros(nc.nhvdc, dtype=complex),
                                                 iterations=0,
                                                 elapsed=0)

        while solver_idx < len(solvers) and not final_solution.converged:
            # get the solver
            solver_type = solvers[solver_idx]

            if solver_type == SolverType.LM:

                problem = PfAcDcWithNegativePoles(V0=final_solution.V,
                                                  S0=S0,
                                                  I0=I0,
                                                  Y0=-Y0,
                                                  Qmin=Qmin,
                                                  Qmax=Qmax,
                                                  nc=nc,
                                                  options=options,
                                                  logger=logger)

                solution = levenberg_marquardt_fx(problem=problem,
                                                  tol=options.tolerance,
                                                  max_iter=options.max_iter,
                                                  verbose=options.verbose,
                                                  logger=logger)

            elif solver_type == SolverType.NR:

                problem = PfAcDcWithNegativePoles(V0=final_solution.V,
                                                  S0=S0,
                                                  I0=I0,
                                                  Y0=-Y0,
                                                  Qmin=Qmin,
                                                  Qmax=Qmax,
                                                  nc=nc,
                                                  options=options,
                                                  logger=logger)

                solution = newton_raphson_fx(problem=problem,
                                             tol=options.tolerance,
                                             max_iter=options.max_iter,
                                             trust=options.trust_radius,
                                             verbose=options.verbose,
                                             logger=logger)

            elif solver_type == SolverType.PowellDogLeg:

                problem = PfAcDcWithNegativePoles(V0=final_solution.V,
                                                  S0=S0,
                                                  I0=I0,
                                                  Y0=-Y0,
                                                  Qmin=Qmin,
                                                  Qmax=Qmax,
                                                  nc=nc,
                                                  options=options,
                                                  logger=logger)

                solution = powell_fx(problem=problem,
                                     tol=options.tolerance,
                                     max_iter=options.max_iter,
                                     trust=options.trust_radius,
                                     verbose=options.verbose,
                                     logger=logger)

            elif solver_type == SolverType.Linear:

                indices = nc.get_simulation_indices(Sbus=S0)
                lin_adm = nc.get_linear_admittance_matrices(indices=indices)

                solution = pflw.acdc_lin_pf(
                    nc=nc,
                    Bbus=lin_adm.Bbus,
                    Bf=lin_adm.Bf,
                    Gbus=lin_adm.Gbus,
                    Gf=lin_adm.Gf,
                    ac=indices.ac,
                    dc=indices.dc,
                    vd=indices.vd,
                    pv=indices.pv,
                    S0=S0,
                    I0=I0,
                    Y0=Y0,
                    V0=V0,
                    tau=nc.active_branch_data.tap_angle
                )

            else:
                # for any other method, raise exception
                logger.add_error('Solver not supported in power flow mode', value=solver_type.value)
                problem = PfAcDcWithNegativePoles(V0=final_solution.V,
                                                  S0=S0,
                                                  I0=I0,
                                                  Y0=-Y0,
                                                  Qmin=Qmin,
                                                  Qmax=Qmax,
                                                  nc=nc,
                                                  options=options,
                                                  logger=logger)

                solution = newton_raphson_fx(problem=problem,
                                             tol=options.tolerance,
                                             max_iter=options.max_iter,
                                             trust=options.trust_radius,
                                             verbose=options.verbose,
                                             logger=logger)

            # record the solution type
            solution.method = solver_type
            if np.isfinite(solution.norm_f) and solution.norm_f <= options.tolerance:
                solution.converged = True
            else:
                pass

            # record the method used, if it improved the solution
            if abs(solution.norm_f) < abs(final_solution.norm_f):
                report.add(method=solver_type,
                           converged=solution.converged,
                           error=solution.norm_f,
                           elapsed=solution.elapsed,
                           iterations=solution.iterations)

                if solution.method in [SolverType.Linear, SolverType.LACPF]:
                    # if the method is linear, we do not check the solution quality
                    final_solution = solution
                else:
                    # if the method is supposed to be exact, we check the solution quality
                    if abs(solution.norm_f) < 0.1:
                        final_solution = solution
                    else:
                        logger.add_info('Tried solution is garbage',
                                        solver_type.value,
                                        value="{:.4e}".format(solution.norm_f),
                                        expected_value=0.1)
            else:
                logger.add_info('Tried solver but it did not improve the solution',
                                solver_type.value,
                                value="{:.4e}".format(solution.norm_f),
                                expected_value=final_solution.norm_f)

            # next solver
            solver_idx += 1

        if not final_solution.converged:
            logger.add_error('Did not converge, even after retry!',
                             device='Error',
                             value="{:.4e}".format(final_solution.norm_f),
                             expected_value=f"<{options.tolerance}")

        if final_solution.tap_module is None:
            final_solution.tap_module = nc.active_branch_data.tap_module

        if final_solution.tap_angle is None:
            final_solution.tap_angle = nc.active_branch_data.tap_angle

        return final_solution, report


def __solve_island_limited_support(island: NumericalCircuit,
                                   indices: SimulationIndices,
                                   options: PowerFlowOptions,
                                   V0: CxVec,
                                   S_base: CxVec,
                                   Shvdc: Vec,
                                   logger=Logger()) -> Tuple[NumericPowerFlowResults, ConvergenceReport]:
    """
    Run a power flow simulation using the selected method (no outer loop controls).
    This routine supports delete voltage controls,and Hvdc links through external injections (Shvdc)
    Also requires grids to be split by HvdcLines
    :param island: SnapshotData circuit, this ensures on-demand admittances computation
    :param indices: SimulationIndices
    :param options: PowerFlow options
    :param V0: Array of initial voltages
    :param S_base: Array of power Injections
    :param Shvdc: Array of power injections due t the HVDC lines (only used in some algorithms)
    :param logger: Logger
    :return: NumericPowerFlowResults 
    """

    logger.add_info('Using the limited support power flow method')

    report = ConvergenceReport()
    if options.retry_with_other_methods:
        solver_list = [SolverType.NR,
                       SolverType.PowellDogLeg,
                       SolverType.HELM,
                       SolverType.IWAMOTO,
                       SolverType.LM,
                       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 = false to enter the loop
    solver_idx = 0

    # set the initial value
    Qmax, Qmin = island.get_reactive_power_limits()
    I0 = island.get_current_injections_pu()
    Y0 = island.get_admittance_injections_pu()

    Sbase_plus_hvdc: CxVec = S_base + Shvdc

    if len(indices.vd) == 0 or len(indices.no_slack) == 0:
        solution = NumericPowerFlowResults(V=np.zeros(len(S_base), dtype=complex),
                                           Scalc=Sbase_plus_hvdc,
                                           m=island.active_branch_data.tap_module,
                                           tau=island.active_branch_data.tap_angle,
                                           Sf=np.zeros(island.nbr, dtype=complex),
                                           St=np.zeros(island.nbr, dtype=complex),
                                           If=np.zeros(island.nbr, dtype=complex),
                                           It=np.zeros(island.nbr, dtype=complex),
                                           loading=np.zeros(island.nbr, dtype=complex),
                                           losses=np.zeros(island.nbr, dtype=complex),
                                           Pfp_vsc=np.zeros(island.nvsc, dtype=float),
                                           Pfn_vsc=np.zeros(island.nvsc, dtype=float),
                                           St_vsc=np.zeros(island.nvsc, dtype=complex),
                                           If_vsc=np.zeros(island.nvsc, dtype=float),
                                           It_vsc=np.zeros(island.nvsc, dtype=complex),
                                           losses_vsc=np.zeros(island.nvsc, dtype=float),
                                           loading_vsc=np.zeros(island.nvsc, dtype=float),
                                           Sf_hvdc=np.zeros(island.nhvdc, dtype=complex),
                                           St_hvdc=np.zeros(island.nhvdc, dtype=complex),
                                           losses_hvdc=np.zeros(island.nhvdc, dtype=complex),
                                           loading_hvdc=np.zeros(island.nhvdc, dtype=complex),
                                           converged=False,
                                           norm_f=1e200,
                                           iterations=0,
                                           elapsed=0)

        # method, converged: bool, error: float, elapsed: float, iterations: int
        report.add(method=SolverType.NoSolver, converged=True, error=0.0, elapsed=0.0, iterations=0)
        logger.add_error('Not solving power flow because there is no slack bus')
        return solution, report

    else:

        adm = island.get_admittance_matrices()

        final_solution = NumericPowerFlowResults(V=V0,
                                                 converged=False,
                                                 norm_f=1e200,
                                                 Scalc=Sbase_plus_hvdc,
                                                 m=island.active_branch_data.tap_module,
                                                 tau=island.active_branch_data.tap_angle,
                                                 Sf=np.zeros(island.nbr, dtype=complex),
                                                 St=np.zeros(island.nbr, dtype=complex),
                                                 If=np.zeros(island.nbr, dtype=complex),
                                                 It=np.zeros(island.nbr, dtype=complex),
                                                 loading=np.zeros(island.nbr, dtype=complex),
                                                 losses=np.zeros(island.nbr, dtype=complex),
                                                 Pfp_vsc=np.zeros(island.nvsc, dtype=float),
                                                 Pfn_vsc=np.zeros(island.nvsc, dtype=float),
                                                 St_vsc=np.zeros(island.nvsc, dtype=complex),
                                                 If_vsc=np.zeros(island.nvsc, dtype=float),
                                                 It_vsc=np.zeros(island.nvsc, dtype=complex),
                                                 losses_vsc=np.zeros(island.nvsc, dtype=float),
                                                 loading_vsc=np.zeros(island.nvsc, dtype=float),
                                                 Sf_hvdc=np.zeros(island.nhvdc, dtype=complex),
                                                 St_hvdc=np.zeros(island.nhvdc, dtype=complex),
                                                 losses_hvdc=np.zeros(island.nhvdc, dtype=complex),
                                                 loading_hvdc=np.zeros(island.nhvdc, dtype=complex),
                                                 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 == SolverType.HELM:
                adms = island.get_series_admittance_matrices()

                solution = pflw.helm_josep(nc=island,
                                           Ybus=adm.Ybus,
                                           Yf=adm.Yf,
                                           Yt=adm.Yt,
                                           Yshunt_bus=adm.Yshunt_bus,
                                           Yseries=adms.Yseries,
                                           V0=V0,  # take V0 instead of V
                                           S0=Sbase_plus_hvdc,
                                           Ysh0=adms.Yshunt,
                                           pq=indices.pq,
                                           pv=indices.pv,
                                           vd=indices.vd,
                                           no_slack=indices.no_slack,
                                           tolerance=options.tolerance,
                                           max_coefficients=options.max_iter,
                                           use_pade=False,
                                           verbose=options.verbose,
                                           logger=logger)

                if options.distributed_slack:
                    ok, delta = compute_slack_distribution(Scalc=solution.Scalc,
                                                           vd=indices.vd,
                                                           bus_installed_power=island.bus_data.installed_power)
                    if ok:
                        solution = pflw.helm_josep(nc=island,
                                                   Ybus=adm.Ybus,
                                                   Yf=adm.Yf,
                                                   Yt=adm.Yt,
                                                   Yshunt_bus=adm.Yshunt_bus,
                                                   Yseries=adms.Yseries,
                                                   V0=V0,  # take V0 instead of V
                                                   S0=Sbase_plus_hvdc + delta,
                                                   Ysh0=adms.Yshunt,
                                                   pq=indices.pq,
                                                   pv=indices.pv,
                                                   vd=indices.vd,
                                                   no_slack=indices.no_slack,
                                                   tolerance=options.tolerance,
                                                   max_coefficients=options.max_iter,
                                                   use_pade=False,
                                                   verbose=options.verbose,
                                                   logger=logger)

            # type DC
            elif solver_type == SolverType.Linear:

                lin_adm = island.get_linear_admittance_matrices(indices=indices)
                Bpqpv = lin_adm.get_Bred(pqpv=indices.no_slack)
                Bref = lin_adm.get_Bslack(pqpv=indices.no_slack, vd=indices.vd)

                solution = pflw.linear_pf(nc=island,
                                          Ybus=adm.Ybus,
                                          Bpqpv=Bpqpv,
                                          Bref=Bref,
                                          Bf=lin_adm.Bf,
                                          S0=Sbase_plus_hvdc,
                                          I0=I0,
                                          Y0=Y0,
                                          V0=V0,
                                          tau=island.active_branch_data.tap_angle,
                                          vd=indices.vd,
                                          no_slack=indices.no_slack,
                                          pq=indices.pq,
                                          pv=indices.pv)

                if options.distributed_slack:
                    ok, delta = compute_slack_distribution(Scalc=solution.Scalc,
                                                           vd=indices.vd,
                                                           bus_installed_power=island.bus_data.installed_power)
                    if ok:
                        solution = pflw.linear_pf(nc=island,
                                                  Ybus=adm.Ybus,
                                                  Bpqpv=Bpqpv,
                                                  Bref=Bref,
                                                  Bf=lin_adm.Bf,
                                                  S0=Sbase_plus_hvdc + delta,
                                                  I0=I0,
                                                  Y0=Y0,
                                                  V0=V0,
                                                  tau=island.active_branch_data.tap_angle,
                                                  vd=indices.vd,
                                                  no_slack=indices.no_slack,
                                                  pq=indices.pq,
                                                  pv=indices.pv)

                if solver_idx > 0:
                    # if we get to this solver, the converged tag should not tell me it worked
                    solution.converged = solution.norm_f <= options.tolerance

            # LAC PF
            elif solver_type == SolverType.LACPF:
                adms = island.get_series_admittance_matrices()
                solution = pflw.lacpf(nc=island,
                                      Ybus=adm.Ybus,
                                      Yf=adm.Yf,
                                      Yt=adm.Yt,
                                      Yshunt_bus=adm.Yshunt_bus,
                                      Ys=adms.Yseries,
                                      S0=Sbase_plus_hvdc,
                                      V0=V0,
                                      pq=indices.pq,
                                      pv=indices.pv,
                                      vd=indices.vd,
                                      logger=logger)
                if options.distributed_slack:
                    ok, delta = compute_slack_distribution(Scalc=solution.Scalc,
                                                           vd=indices.vd,
                                                           bus_installed_power=island.bus_data.installed_power)
                    if ok:
                        solution = pflw.lacpf(nc=island,
                                              Ybus=adm.Ybus,
                                              Yf=adm.Yf,
                                              Yt=adm.Yt,
                                              Ys=adms.Yseries,
                                              Yshunt_bus=adm.Yshunt_bus,
                                              S0=Sbase_plus_hvdc + delta,
                                              V0=V0,
                                              pq=indices.pq,
                                              pv=indices.pv,
                                              vd=indices.vd,
                                              logger=logger)
                if solver_idx > 0:
                    # if we get to this solver, the converged tag should not tell me it worked
                    solution.converged = solution.norm_f <= options.tolerance

            # Gauss-Seidel
            elif solver_type == SolverType.GAUSS:
                solution = pflw.gausspf(nc=island,
                                        Ybus=adm.Ybus,
                                        Yf=adm.Yf,
                                        Yt=adm.Yt,
                                        Yshunt_bus=adm.Yshunt_bus,
                                        S0=Sbase_plus_hvdc,
                                        I0=I0,
                                        Y0=Y0,
                                        V0=V0,
                                        pv=indices.pv,
                                        pq=indices.pq,
                                        p=indices.p,
                                        pqv=indices.pqv,
                                        vd=indices.vd,
                                        bus_installed_power=island.bus_data.installed_power,
                                        Qmin=Qmin,
                                        Qmax=Qmax,
                                        tol=options.tolerance,
                                        max_it=options.max_iter,
                                        control_q=options.control_Q,
                                        distribute_slack=options.distributed_slack,
                                        verbose=options.verbose,
                                        logger=logger)

            # Levenberg-Marquardt
            elif solver_type == SolverType.LM:
                problem = PfBasicFormulation(V0=final_solution.V,
                                             S0=Sbase_plus_hvdc,
                                             I0=I0,
                                             Y0=Y0,
                                             Qmin=Qmin,
                                             Qmax=Qmax,
                                             nc=island,
                                             options=options)

                solution = levenberg_marquardt_fx(problem=problem,
                                                  tol=options.tolerance,
                                                  max_iter=options.max_iter,
                                                  verbose=options.verbose,
                                                  logger=logger)

            # Fast decoupled
            elif solver_type == SolverType.FASTDECOUPLED:
                fd_adm = island.get_fast_decoupled_amittances()

                solution = pflw.FDPF(nc=island,
                                     Vbus=V0,
                                     S0=Sbase_plus_hvdc,
                                     I0=I0,
                                     Y0=Y0,
                                     Ybus=adm.Ybus,
                                     Yf=adm.Yf,
                                     Yt=adm.Yt,
                                     Yshunt_bus=adm.Yshunt_bus,
                                     B1=fd_adm.B1,
                                     B2=fd_adm.B2,
                                     pv_=indices.pv,
                                     pq_=indices.pq,
                                     pqv_=indices.pqv,
                                     p_=indices.p,
                                     vd_=indices.vd,
                                     Qmin=Qmin,
                                     Qmax=Qmax,
                                     bus_installed_power=island.bus_data.installed_power,
                                     tol=options.tolerance,
                                     max_it=options.max_iter,
                                     control_q=options.control_Q,
                                     distribute_slack=options.distributed_slack)

            # Newton-Raphson (full, but non-generalized)
            elif solver_type == SolverType.NR:
                problem = PfBasicFormulation(V0=final_solution.V,
                                             S0=Sbase_plus_hvdc,
                                             I0=I0,
                                             Y0=Y0,
                                             Qmin=Qmin,
                                             Qmax=Qmax,
                                             nc=island,
                                             options=options)

                solution = newton_raphson_fx(problem=problem,
                                             tol=options.tolerance,
                                             max_iter=options.max_iter,
                                             trust=options.trust_radius,
                                             verbose=options.verbose,
                                             logger=logger)

            # Powell's Dog Leg (full)
            elif solver_type == SolverType.PowellDogLeg:
                problem = PfBasicFormulation(V0=final_solution.V,
                                             S0=S_base,
                                             I0=I0,
                                             Y0=Y0,
                                             Qmin=Qmin,
                                             Qmax=Qmax,
                                             nc=island,
                                             options=options)

                solution = powell_fx(problem=problem,
                                     tol=options.tolerance,
                                     max_iter=options.max_iter,
                                     trust=options.trust_radius,
                                     verbose=options.verbose,
                                     logger=logger)

            # Newton-Raphson-Iwamoto
            elif solver_type == SolverType.IWAMOTO:
                solution = pflw.IwamotoNR(nc=island,
                                          Ybus=adm.Ybus,
                                          Yf=adm.Yf,
                                          Yt=adm.Yt,
                                          Yshunt_bus=adm.Yshunt_bus,
                                          S0=Sbase_plus_hvdc,
                                          V0=final_solution.V,
                                          I0=I0,
                                          Y0=Y0,
                                          pv_=indices.pv,
                                          pq_=indices.pq,
                                          pqv_=indices.pqv,
                                          p_=indices.p,
                                          vd_=indices.vd,
                                          Qmin=Qmin,
                                          Qmax=Qmax,
                                          tol=options.tolerance,
                                          max_it=options.max_iter,
                                          control_q=options.control_Q,
                                          robust=True,
                                          logger=logger)

            else:
                # for any other method, raise exception
                raise Exception(solver_type.value + ' Not supported in power flow mode')

            # record the solution type
            solution.method = solver_type
            if np.isfinite(solution.norm_f) and solution.norm_f <= options.tolerance:
                solution.converged = True
            else:
                pass

            # record the method used, if it improved the solution
            if abs(solution.norm_f) < abs(final_solution.norm_f):
                report.add(method=solver_type,
                           converged=solution.converged,
                           error=solution.norm_f,
                           elapsed=solution.elapsed,
                           iterations=solution.iterations)

                if solution.method in [SolverType.Linear, SolverType.LACPF]:
                    # if the method is linear, we do not check the solution quality
                    final_solution = solution
                else:
                    # if the method is supposed to be exact, we check the solution quality
                    if abs(solution.norm_f) < 0.1 or (options.retry_with_other_methods == False):
                        final_solution = solution
                    else:
                        logger.add_info('Tried solution is garbage',
                                        solver_type.value,
                                        value="{:.4e}".format(solution.norm_f),
                                        expected_value=0.1)
            else:
                logger.add_info('Tried solver but it did not improve the solution',
                                solver_type.value,
                                value="{:.4e}".format(solution.norm_f),
                                expected_value=final_solution.norm_f)

            # next solver
            solver_idx += 1

        if not final_solution.converged:
            logger.add_error('Did not converge, even after retry!',
                             device='Error',
                             value="{:.4e}".format(final_solution.norm_f),
                             expected_value=f"<{options.tolerance}")

        if final_solution.tap_module is None:
            final_solution.tap_module = island.active_branch_data.tap_module

        if final_solution.tap_angle is None:
            final_solution.tap_angle = island.active_branch_data.tap_angle

        return final_solution, report


def __multi_island_pf_nc_complete_support(nc: NumericalCircuit,
                                          options: PowerFlowOptions,
                                          logger: Logger | None = None,
                                          V_guess: Union[CxVec, None] = None,
                                          Sbus_input: Union[CxVec, None] = None) -> PowerFlowResults:
    """
    Multiple islands power flow (this is the most generic power flow function)

    multi_island_pf
      |-> multi_island_pf_nc
                |-> split_into_islands
                        |-> for each island:
                                |-> __solve_island_complete_support
                                        |-> solve

    :param nc: SnapshotData instance
    :param options: PowerFlowOptions instance
    :param logger: logger
    :param V_guess: voltage guess
    :param Sbus_input: Use this power injections if provided
    :return: PowerFlowResults instance
    """
    if logger is None:
        logger = Logger()

    # declare results
    results = PowerFlowResults(
        n=nc.nbus,
        m=nc.nbr,
        n_hvdc=nc.nhvdc,
        n_vsc=nc.nvsc,
        n_gen=nc.ngen,
        n_batt=nc.nbatt,
        n_sh=nc.nshunt,
        bus_names=nc.bus_data.names,
        branch_names=nc.passive_branch_data.names,
        hvdc_names=nc.hvdc_data.names,
        vsc_names=nc.vsc_data.names,
        gen_names=nc.generator_data.names,
        batt_names=nc.battery_data.names,
        sh_names=nc.shunt_data.names,
        bus_types=nc.bus_data.bus_types,
    )

    # compute islands
    islands = nc.split_into_islands(ignore_single_node_islands=options.ignore_single_node_islands,
                                    consider_hvdc_as_island_links=True,
                                    logger=logger)

    for i, island in enumerate(islands):

        indices = island.get_simulation_indices()
        Sbus_base = island.get_power_injections_pu()

        if len(indices.vd) > 0:

            # call the numerical methods
            solution, report = __solve_island_complete_support(
                nc=island,
                indices=indices,
                options=options,
                V0=island.bus_data.Vbus if V_guess is None else V_guess[island.bus_data.original_idx],
                S0=Sbus_base if Sbus_input is None else Sbus_input[island.bus_data.original_idx],
                logger=logger
            )

            # merge the results from this island
            results.apply_from_island(
                results=solution,
                b_idx=island.bus_data.original_idx,
                br_idx=island.passive_branch_data.original_idx,
                hvdc_idx=island.hvdc_data.original_idx,
                vsc_idx=island.vsc_data.original_idx
            )
            results.convergence_reports.append(report)

        else:
            logger.add_info('No slack nodes in the island', str(i))

    return results


def __multi_island_pf_nc_limited_support(nc: NumericalCircuit,
                                         options: PowerFlowOptions,
                                         logger: Logger | None = None,
                                         V_guess: Union[CxVec, None] = None,
                                         Sbus_input: Union[CxVec, None] = None) -> PowerFlowResults:
    """
    Multiple islands power flow (this is the most generic power flow function)

    multi_island_pf
      |-> multi_island_pf_nc
                |-> split_into_islands  (Deals with HvdcLine injections)
                        |-> for each island:
                                |-> single_island_pf
                                        |-> solve

    :param nc: SnapshotData instance
    :param options: PowerFlowOptions instance
    :param logger: logger
    :param V_guess: voltage guess
    :param Sbus_input: Use this power injections if provided
    :return: PowerFlowResults instance
    """
    if logger is None:
        logger = Logger()

    # declare results
    results = PowerFlowResults(
        n=nc.nbus,
        m=nc.nbr,
        n_hvdc=nc.nhvdc,
        n_vsc=nc.nvsc,
        n_gen=nc.ngen,
        n_batt=nc.nbatt,
        n_sh=nc.nshunt,
        bus_names=nc.bus_data.names,
        branch_names=nc.passive_branch_data.names,
        hvdc_names=nc.hvdc_data.names,
        vsc_names=nc.vsc_data.names,
        gen_names=nc.generator_data.names,
        batt_names=nc.battery_data.names,
        sh_names=nc.shunt_data.names,
        bus_types=nc.bus_data.bus_types,
    )

    # compose the HVDC power Injections
    # since the power flow methods don't support HVDC directly, we need this step
    Shvdc, Losses_hvdc, Pf_hvdc, Pt_hvdc, loading_hvdc, n_free = nc.hvdc_data.get_power(
        Sbase=nc.Sbase,
        theta=np.zeros(nc.nbus),
    )

    # compute islands
    islands = nc.split_into_islands(ignore_single_node_islands=options.ignore_single_node_islands,
                                    consider_hvdc_as_island_links=False,
                                    logger=logger)

    for i, island in enumerate(islands):

        Sbus_base = island.get_power_injections_pu()
        indices = island.get_simulation_indices(Sbus=Sbus_base)

        if len(indices.vd) > 0:

            # call the numerical methods
            solution, report = __solve_island_limited_support(
                island=island,
                indices=indices,
                options=options,
                V0=island.bus_data.Vbus if V_guess is None else V_guess[island.bus_data.original_idx],
                S_base=Sbus_base if Sbus_input is None else Sbus_input[island.bus_data.original_idx],
                Shvdc=Shvdc[island.bus_data.original_idx],
                logger=logger
            )

            # merge the results from this island
            results.apply_from_island(
                results=solution,
                b_idx=island.bus_data.original_idx,
                br_idx=island.passive_branch_data.original_idx,
                hvdc_idx=island.hvdc_data.original_idx,
                vsc_idx=island.vsc_data.original_idx
            )
            results.convergence_reports.append(report)

        else:
            logger.add_info('No slack nodes in the island', str(i))

    # Compile HVDC results (available for the complete grid since HVDC line as
    # formulated are split objects
    # Pt is the "generation" at the sending point
    results.Pf_hvdc = - Pf_hvdc * nc.Sbase  # we change the sign to keep the sign convention with AC lines
    results.Pt_hvdc = - Pt_hvdc * nc.Sbase  # we change the sign to keep the sign convention with AC lines
    results.loading_hvdc = loading_hvdc
    results.losses_hvdc = Losses_hvdc * nc.Sbase

    return results


[docs] def multi_island_pf_nc(nc: NumericalCircuit, options: PowerFlowOptions, logger: Logger | None = None, V_guess: Union[CxVec, None] = None, Sbus_input: Union[CxVec, None] = None) -> PowerFlowResults: """ Multiple islands power flow (this is the most generic power flow function) :param nc: SnapshotData instance :param options: PowerFlowOptions instance :param logger: logger :param V_guess: voltage guess :param Sbus_input: Use this power injections if provided (in p.u.) :return: PowerFlowResults instance """ if logger is None: logger = Logger() if options.initialize_angles and options.solver_type not in [SolverType.Linear, SolverType.LACPF, SolverType.HELM]: # NOTE: This is to initialize power flows with very different angles # that may happen if the transformer phase shifts are applied in the power flow results_0 = __multi_island_pf_nc_limited_support( nc=nc, options=PowerFlowOptions(solver_type=SolverType.Linear), logger=logger, V_guess=V_guess, Sbus_input=Sbus_input, ) V0 = results_0.voltage else: V0 = nc.bus_data.Vbus if V_guess is None else V_guess[nc.bus_data.original_idx] if nc.active_branch_data.any_pf_control: results = __multi_island_pf_nc_complete_support( nc=nc, options=options, logger=logger, V_guess=V0, Sbus_input=Sbus_input, ) if not results.converged: logger.add_warning( msg="Control-aware power flow did not converge; falling back to the " "limited-support solver, which does NOT enforce branch/VSC/HVDC " "controls (tap module, phase shift, converter set-points). " "The returned solution may not honour those control targets.", device="PowerFlow", value="control-aware solver failed", expected_value="converged solution with controls enforced" ) results = __multi_island_pf_nc_limited_support( nc=nc, options=options, logger=logger, V_guess=V0, Sbus_input=Sbus_input, ) # expand voltages if there was a bus topology reduction if nc.topology_performed: results.voltage = nc.propagate_bus_result(results.voltage) vm_abs: Vec = np.abs(results.voltage) slack_bus_mask: np.ndarray = (nc.bus_data.bus_types == BusMode.Slack_tpe.value) fixed_load_bus: CxVec = ( nc.load_data.get_injections_per_bus() + results.voltage * np.conj( nc.load_data.get_current_injections_per_bus() + nc.load_data.get_admittance_injections_per_bus() * results.voltage ) ) fixed_shunt_bus: CxVec = results.voltage * np.conj( nc.shunt_data.get_injections_per_bus() * results.voltage ) fixed_non_generator_bus: CxVec = fixed_load_bus + fixed_shunt_bus qfixed_bus: Vec = ( nc.bus_data.q_fixed - (nc.bus_data.ii_fixed + nc.bus_data.b_fixed * vm_abs) * vm_abs ) # Reconstruct the fixed shunt-like device reactive power from the final # solved voltage so the reported device values remain in nodal balance. results.shunt_q = -(nc.shunt_data.Y.imag * np.power(vm_abs[nc.shunt_data.bus_idx], 2.0)) * nc.shunt_data.active # Split only the remaining generator-like reactive power after removing # the fixed compiled bus contribution. results.gen_q, results.battery_q = split_reactive_power_between_generators_and_batteries( Qbus=results.Sbus.imag, Qfixed_bus=qfixed_bus, gen_bus_idx=nc.generator_data.bus_idx, Qmin_gen=nc.generator_data.qmin, Qmax_gen=nc.generator_data.qmax, gen_status=nc.generator_data.active, control_mode_int_gen=nc.generator_data.control_mode_int, Q0_gen=nc.generator_data.q, Vset_gen=nc.generator_data.v, k_droop_gen=nc.generator_data.k_droop, dead_band_gen=nc.generator_data.dead_band, batt_bus_idx=nc.battery_data.bus_idx, Qmin_batt=nc.battery_data.qmin, Qmax_batt=nc.battery_data.qmax, batt_status=nc.battery_data.active, control_mode_int_batt=nc.battery_data.control_mode_int, Q0_batt=nc.battery_data.q, v_ctrl_val_gen=GeneratorControlMode.V.idx(), qv_droop_val_gen=GeneratorControlMode.QVDroop.idx(), Vm=vm_abs, atol=1e-12, ) results.gen_q, results.battery_q = split_slack_bus_quantity_between_generators_and_batteries( Qbus=results.Sbus.imag, Qfixed_bus=fixed_non_generator_bus.imag, slack_bus_mask=slack_bus_mask, gen_bus_idx=nc.generator_data.bus_idx, Qmin_gen=nc.generator_data.qmin, Qmax_gen=nc.generator_data.qmax, gen_status=nc.generator_data.active, Q0_gen=results.gen_q, batt_bus_idx=nc.battery_data.bus_idx, Qmin_batt=nc.battery_data.qmin, Qmax_batt=nc.battery_data.qmax, batt_status=nc.battery_data.active, Q0_batt=results.battery_q, atol=1e-12, ) results.gen_p, results.battery_p = split_slack_bus_quantity_between_generators_and_batteries( Qbus=results.Sbus.real, Qfixed_bus=fixed_non_generator_bus.real, slack_bus_mask=slack_bus_mask, gen_bus_idx=nc.generator_data.bus_idx, Qmin_gen=nc.generator_data.pmin, Qmax_gen=nc.generator_data.pmax, gen_status=nc.generator_data.active, Q0_gen=nc.generator_data.p, batt_bus_idx=nc.battery_data.bus_idx, Qmin_batt=nc.battery_data.pmin, Qmax_batt=nc.battery_data.pmax, batt_status=nc.battery_data.active, Q0_batt=nc.battery_data.p, atol=1e-12, ) return results else: results = __multi_island_pf_nc_limited_support( nc=nc, options=options, logger=logger, V_guess=V0, Sbus_input=Sbus_input, ) # expand voltages if there was a bus topology reduction if nc.topology_performed: results.voltage = nc.propagate_bus_result(results.voltage) vm_abs: Vec = np.abs(results.voltage) slack_bus_mask: np.ndarray = (nc.bus_data.bus_types == BusMode.Slack_tpe.value) fixed_load_bus: CxVec = ( nc.load_data.get_injections_per_bus() + results.voltage * np.conj( nc.load_data.get_current_injections_per_bus() + nc.load_data.get_admittance_injections_per_bus() * results.voltage ) ) fixed_shunt_bus: CxVec = results.voltage * np.conj( nc.shunt_data.get_injections_per_bus() * results.voltage ) fixed_non_generator_bus: CxVec = fixed_load_bus + fixed_shunt_bus qfixed_bus: Vec = ( nc.bus_data.q_fixed - (nc.bus_data.ii_fixed + nc.bus_data.b_fixed * vm_abs) * vm_abs ) results.shunt_q = -(nc.shunt_data.Y.imag * np.power(vm_abs[nc.shunt_data.bus_idx], 2.0)) * nc.shunt_data.active # Remove the compiled fixed bus-side Q before allocating the solved # residual to generator-like voltage controllers. results.gen_q, results.battery_q = split_reactive_power_between_generators_and_batteries( Qbus=results.Sbus.imag, Qfixed_bus=qfixed_bus, gen_bus_idx=nc.generator_data.bus_idx, Qmin_gen=nc.generator_data.qmin, Qmax_gen=nc.generator_data.qmax, gen_status=nc.generator_data.active, control_mode_int_gen=nc.generator_data.control_mode_int, Q0_gen=nc.generator_data.q, Vset_gen=nc.generator_data.v, k_droop_gen=nc.generator_data.k_droop, dead_band_gen=nc.generator_data.dead_band, batt_bus_idx=nc.battery_data.bus_idx, Qmin_batt=nc.battery_data.qmin, Qmax_batt=nc.battery_data.qmax, batt_status=nc.battery_data.active, control_mode_int_batt=nc.battery_data.control_mode_int, Q0_batt=nc.battery_data.q, v_ctrl_val_gen=GeneratorControlMode.V.idx(), qv_droop_val_gen=GeneratorControlMode.QVDroop.idx(), Vm=vm_abs, atol=1e-12, ) results.gen_q, results.battery_q = split_slack_bus_quantity_between_generators_and_batteries( Qbus=results.Sbus.imag, Qfixed_bus=fixed_non_generator_bus.imag, slack_bus_mask=slack_bus_mask, gen_bus_idx=nc.generator_data.bus_idx, Qmin_gen=nc.generator_data.qmin, Qmax_gen=nc.generator_data.qmax, gen_status=nc.generator_data.active, Q0_gen=results.gen_q, batt_bus_idx=nc.battery_data.bus_idx, Qmin_batt=nc.battery_data.qmin, Qmax_batt=nc.battery_data.qmax, batt_status=nc.battery_data.active, Q0_batt=results.battery_q, atol=1e-12, ) if options.distributed_slack: results.gen_p, results.battery_p = split_bus_quantity( Qbus=results.Sbus.real, gen_bus_idx=nc.generator_data.bus_idx, Qmin_gen=nc.generator_data.pmin, Qmax_gen=nc.generator_data.pmax, gen_status=nc.generator_data.active, control_mode_int_gen=nc.generator_data.control_mode_int, Q0_gen=nc.generator_data.p, batt_bus_idx=nc.battery_data.bus_idx, Qmin_batt=nc.battery_data.pmin, Qmax_batt=nc.battery_data.pmax, batt_status=nc.battery_data.active, control_mode_int_batt=nc.battery_data.control_mode_int, Q0_batt=nc.battery_data.p, v_ctrl_val_gen=GeneratorControlMode.V.idx(), atol=1e-12, ) else: results.gen_p = nc.generator_data.p results.battery_p = nc.battery_data.p results.gen_p, results.battery_p = split_slack_bus_quantity_between_generators_and_batteries( Qbus=results.Sbus.real, Qfixed_bus=fixed_non_generator_bus.real, slack_bus_mask=slack_bus_mask, gen_bus_idx=nc.generator_data.bus_idx, Qmin_gen=nc.generator_data.pmin, Qmax_gen=nc.generator_data.pmax, gen_status=nc.generator_data.active, Q0_gen=results.gen_p, batt_bus_idx=nc.battery_data.bus_idx, Qmin_batt=nc.battery_data.pmin, Qmax_batt=nc.battery_data.pmax, batt_status=nc.battery_data.active, Q0_batt=results.battery_p, atol=1e-12, ) return results
[docs] def multi_island_pf(multi_circuit: MultiCircuit, options: PowerFlowOptions, opf_results: VALID_OPF_RESULTS | None = None, t: Union[int, None] = None, logger: Logger = Logger(), bus_dict: Union[Dict[Bus, int], None] = None, areas_dict: Union[Dict[Area, int], None] = None) -> PowerFlowResults: """ Multiple islands power flow (this is the most generic power flow function) :param multi_circuit: MultiCircuit instance :param options: PowerFlowOptions instance :param opf_results: OPF results, to be used if not None :param t: time step, if None, the snapshot is compiled :param logger: list of events to add to :param bus_dict: Dus object to index dictionary :param areas_dict: Area to area index dictionary :return: PowerFlowResults instance """ nc = compile_numerical_circuit_at( circuit=multi_circuit, t_idx=t, apply_temperature=options.apply_temperature_correction, branch_tolerance_mode=options.branch_impedance_tolerance_mode, opf_results=opf_results, use_stored_guess=options.use_stored_guess, bus_dict=bus_dict, areas_dict=areas_dict, control_taps_modules=options.control_taps_modules, control_taps_phase=options.control_taps_phase, control_remote_voltage=options.control_remote_voltage, logger=logger, fill_three_phase=False, # This worker is only for positive sequence consider_grounded_buses=len(multi_circuit.vsc_devices) > 0 ) res = multi_island_pf_nc(nc=nc, options=options, logger=logger) return res