Source code for VeraGridEngine.Templates.Emt.converter_switched_emt_template

# 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 typing import Dict, Tuple

import numpy as np

import VeraGridEngine.Utils.Symbolic.symbolic as sym
from VeraGridEngine.Devices.Dynamic.emt_template import EmtModelTemplate
from VeraGridEngine.Devices.Dynamic.var_factory import VarFactory
from VeraGridEngine.Templates.Emt.bridge_filter_control_2level_3ph_emt_template import get_bridge_filter_control_2level_3ph_emt_template
from VeraGridEngine.Utils.Symbolic.block import Block, VarPowerFlowReferenceType, build_name_to_var_lookup
from VeraGridEngine.Utils.Symbolic.symbolic import BinOp, Const, Expr, Var
from VeraGridEngine.Utils.procedural_logic import startup_handover
from VeraGridEngine.enumerations import ConverterControlType, DeviceType, ParamPowerFlowReferenceType


def _converter_control_type_code(control: ConverterControlType | None) -> int:
    """
    Return the integer code used by EMT converter control-mode parameters.

    The switched converter intentionally keeps local copies of the converter helper
    routines it reuses, so this template does not depend on another converter
    template module.

    :param control: Converter control enum.
    :return: Encoded integer value.
    """
    code_dict: Dict[ConverterControlType | None, int] = {
        None: 0,
        ConverterControlType.Vm_dc: 1,
        ConverterControlType.Vm_ac: 2,
        ConverterControlType.Va_ac: 3,
        ConverterControlType.Qac: 4,
        ConverterControlType.Pdc: 5,
        ConverterControlType.Pac: 6,
        ConverterControlType.Pdc_angle_droop: 7,
        ConverterControlType.Imax: 8,
    }
    return code_dict[control]


def _converter_control_match_expr(control_var: Var, control_type: ConverterControlType | None) -> Expr:
    """
    Build one symbolic match expression for a converter control mode.

    :param control_var: Runtime control-mode variable.
    :param control_type: Requested control enum.
    :return: Boolean-like symbolic expression.
    """
    return (control_var == _converter_control_type_code(control_type)).to_expression()


def _resolve_converter_control_reference_exprs(
        control1: Var,
        control2: Var,
        control1_val: Var,
        control2_val: Var,
        p0: Var,
) -> Tuple[Expr, Expr, Expr, Expr, Expr]:
    """
    Resolve the active-power, reactive-power and DC-voltage references from control modes.

    :param control1: First control-mode code.
    :param control2: Second control-mode code.
    :param control1_val: First control target.
    :param control2_val: Second control target.
    :param p0: Scheduled active-power fallback.
    :return: Tuple ``(p_ref, q_ref, vdc_ref, regulate_vdc, regulate_q)``.
    """
    control1_is_vm_dc: Expr = _converter_control_match_expr(control1, ConverterControlType.Vm_dc)
    control2_is_vm_dc: Expr = _converter_control_match_expr(control2, ConverterControlType.Vm_dc)
    control1_is_qac: Expr = _converter_control_match_expr(control1, ConverterControlType.Qac)
    control2_is_qac: Expr = _converter_control_match_expr(control2, ConverterControlType.Qac)
    control1_is_pac: Expr = _converter_control_match_expr(control1, ConverterControlType.Pac)
    control2_is_pac: Expr = _converter_control_match_expr(control2, ConverterControlType.Pac)
    control1_is_pdc: Expr = _converter_control_match_expr(control1, ConverterControlType.Pdc)
    control2_is_pdc: Expr = _converter_control_match_expr(control2, ConverterControlType.Pdc)

    regulate_vdc: Expr = sym.max(control1_is_vm_dc, control2_is_vm_dc)
    regulate_q: Expr = sym.max(control1_is_qac, control2_is_qac)
    vdc_ref: Expr = (
        control1_is_vm_dc * control1_val
        + (Const(1.0) - control1_is_vm_dc)
        * (control2_is_vm_dc * control2_val + (Const(1.0) - control2_is_vm_dc) * Const(1.0))
    )
    q_ref: Expr = (
        control1_is_qac * control1_val
        + (Const(1.0) - control1_is_qac)
        * (control2_is_qac * control2_val + (Const(1.0) - control2_is_qac) * Const(0.0))
    )
    p_ref: Expr = (
        (control1_is_pac + control1_is_pdc) * control1_val
        + (Const(1.0) - (control1_is_pac + control1_is_pdc))
        * ((control2_is_pac + control2_is_pdc) * control2_val + (Const(1.0) - (control2_is_pac + control2_is_pdc)) * p0)
    )

    return p_ref, q_ref, vdc_ref, regulate_vdc, regulate_q


def _build_pseudo_emt_converter_vsc_block(vf: VarFactory, name: str) -> Block:
    """
    Build the averaged converter electrical/DC block reused by the switched template.

    This is a local copy on purpose: the switched converter may reuse the validated
    DC-side model, but it should not import private helpers from another converter
    template module.

    :param vf: Shared EMT variable factory.
    :param name: Symbolic model name suffix.
    :return: VSC/DC block.
    """
    v_d: Var = vf.add_var(name=f"v_d_in_{name}")
    v_q: Var = vf.add_var(name=f"v_q_in_{name}")
    v_0: Var = vf.add_var(name=f"v_0_in_{name}")
    i_d: Var = vf.add_var(name=f"i_d_in_{name}")
    i_q: Var = vf.add_var(name=f"i_q_in_{name}")
    i_0: Var = vf.add_var(name=f"i_0_in_{name}")
    v_dc_bus: Var = vf.add_var(name=f"v_dc_bus_in_{name}")

    v_dc: Var = vf.add_var(name=f"v_dc_{name}")
    d_v_dc: Var = vf.add_diff_var(name=f"d_v_dc_{name}", base_var=v_dc)

    i_dc: Var = vf.add_var(name=f"i_dc_{name}", reference=VarPowerFlowReferenceType.Idc)
    P: Var = vf.add_var(name=f"P_{name}", reference=VarPowerFlowReferenceType.P)
    Q: Var = vf.add_var(name=f"Q_{name}", reference=VarPowerFlowReferenceType.Q)
    i_mag: Var = vf.add_var(name=f"i_mag_{name}")
    P_loss: Var = vf.add_var(name=f"P_loss_{name}")
    i_dc_conv: Var = vf.add_var(name=f"i_dc_conv_{name}")

    sbase: Var = vf.add_var(name=f"sbase_{name}")
    P_ref: Var = vf.add_var(name=f"P_ref_{name}")
    Q_ref: Var = vf.add_var(name=f"Q_ref_{name}")
    Vdc_ref: Var = vf.add_var(name=f"Vdc_ref_{name}")
    P0_sched: Var = vf.add_var(name=f"P0_{name}")
    control1: Var = vf.add_var(name=f"control1_{name}")
    control2: Var = vf.add_var(name=f"control2_{name}")
    control1_val: Var = vf.add_var(name=f"control1_val_{name}")
    control2_val: Var = vf.add_var(name=f"control2_val_{name}")
    omega_base: Var = vf.add_var(name=f"omega_base_{name}")
    phi_v: Var = vf.add_var(name=f"phi_v_{name}")
    Vpk: Var = vf.add_var(name=f"Vpk_{name}")
    R_eq: Var = vf.add_var(name=f"R_eq_{name}")
    L_eq: Var = vf.add_var(name=f"L_eq_{name}")
    C_dc: Var = vf.add_var(name=f"C_dc_{name}")
    R_dc: Var = vf.add_var(name=f"R_dc_{name}")
    R_dc_term: Var = vf.add_var(name=f"R_dc_term_{name}")
    pll_kp: Var = vf.add_var(name=f"pll_kp_{name}")
    pll_ki: Var = vf.add_var(name=f"pll_ki_{name}")
    i_kp: Var = vf.add_var(name=f"i_kp_{name}")
    i_ki: Var = vf.add_var(name=f"i_ki_{name}")
    vdc_kp: Var = vf.add_var(name=f"vdc_kp_{name}")
    vdc_ki: Var = vf.add_var(name=f"vdc_ki_{name}")
    q_kp: Var = vf.add_var(name=f"q_kp_{name}")
    q_ki: Var = vf.add_var(name=f"q_ki_{name}")
    i_max: Var = vf.add_var(name=f"i_max_{name}")
    m_max: Var = vf.add_var(name=f"m_max_{name}")
    P_loss0: Var = vf.add_var(name=f"P_loss0_{name}")
    P_loss_i1: Var = vf.add_var(name=f"P_loss_i1_{name}")
    P_loss_i2: Var = vf.add_var(name=f"P_loss_i2_{name}")
    tau_meas: Var = vf.add_var(name=f"tau_meas_{name}")
    aw_gain: Var = vf.add_var(name=f"aw_gain_{name}")
    vdc_floor: Var = vf.add_var(name=f"vdc_floor_{name}")

    p_ref_expr: Expr
    q_ref_expr: Expr
    vdc_ref_expr: Expr
    _unused_regulate_vdc: Expr
    _unused_regulate_q: Expr
    p_ref_expr, q_ref_expr, vdc_ref_expr, _unused_regulate_vdc, _unused_regulate_q = _resolve_converter_control_reference_exprs(
        control1=control1,
        control2=control2,
        control1_val=control1_val,
        control2_val=control2_val,
        p0=P0_sched,
    )

    eps: Const = vf.add_const(1e-10)
    c0: Const = vf.add_const(0.0)
    c1: Const = vf.add_const(1.0)
    c3: Const = vf.add_const(3.0)
    c32: Const = vf.add_const(1.5)

    P_loss0_pu: Expr = P_loss0 / sbase
    P_loss_i1_pu: Expr = P_loss_i1 / sbase
    P_loss_i2_pu: Expr = P_loss_i2 / sbase
    i_leak: Expr = v_dc / R_dc
    v_dc_eff: Expr = sym.max(v_dc, vdc_floor)

    i_d0: Expr = (Const(2.0 / 3.0) * ((P_ref / sbase) + (P_loss0 / sbase))) / (Vpk + eps)
    i_q0: Expr = (Const(2.0 / 3.0) * (Q_ref / sbase)) / (Vpk + eps)
    i_mag0: Expr = sym.sqrt(i_d0 * i_d0 + i_q0 * i_q0 + eps)
    P0: Expr = c32 * Vpk * i_d0
    Q0: Expr = c32 * Vpk * i_q0
    P_loss0_expr: Expr = P_loss0_pu + P_loss_i1_pu * i_mag0 + P_loss_i2_pu * i_mag0 * i_mag0
    i_dc_conv0: Expr = -(P0 - P_loss0_expr) / (Vdc_ref + eps)
    i_dc0: Expr = (i_dc_conv0 + v_dc_bus / R_dc) / (c1 + R_dc_term / R_dc)
    v_dc0: Expr = v_dc_bus - R_dc_term * i_dc0

    block: Block = Block(
        state_eqs=list([
            (i_dc - i_dc_conv - i_leak) / C_dc,
        ]),
        state_vars=list([v_dc]),
        diff_vars=list([d_v_dc]),
        algebraic_eqs=list([
            P_ref - p_ref_expr,
            Q_ref - q_ref_expr,
            Vdc_ref - vdc_ref_expr,
            P - (c32 * (v_d * i_d + v_q * i_q) + c3 * v_0 * i_0),
            Q - c32 * (v_d * i_q - v_q * i_d),
            i_mag - sym.sqrt(i_d * i_d + i_q * i_q + c3 * i_0 * i_0 + eps),
            P_loss - (P_loss0_pu + P_loss_i1_pu * i_mag + P_loss_i2_pu * i_mag * i_mag),
            i_dc_conv + (P - P_loss) / v_dc_eff,
            i_dc - (v_dc_bus - v_dc) / R_dc_term,
        ]),
        algebraic_vars=list([P_ref, Q_ref, Vdc_ref, i_dc, P, Q, i_mag, P_loss, i_dc_conv]),
        event_dict=dict([
            (sbase, vf.add_const(1.0)),
            (P0_sched, vf.add_const(0.0)),
            (control1, vf.add_const(float(_converter_control_type_code(ConverterControlType.Vm_dc)))),
            (control2, vf.add_const(float(_converter_control_type_code(ConverterControlType.Qac)))),
            (control1_val, vf.add_const(1.0)),
            (control2_val, vf.add_const(0.0)),
            (omega_base, vf.add_const(2.0 * np.pi * 50.0)),
            (phi_v, vf.add_const(0.0)),
            (Vpk, vf.add_const(np.sqrt(2.0))),
            (R_eq, vf.add_const(max(0.02, 1e-9))),
            (L_eq, vf.add_const(max(0.08, 1e-9))),
            (C_dc, vf.add_const(max(0.05, 1e-9))),
            (R_dc, vf.add_const(max(1e6, 1e-9))),
            (R_dc_term, vf.add_const(max(0.0001, 1e-9))),
            (pll_kp, vf.add_const(40.0)),
            (pll_ki, vf.add_const(400.0)),
            (i_kp, vf.add_const(0.5)),
            (i_ki, vf.add_const(40.0)),
            (vdc_kp, vf.add_const(1.5)),
            (vdc_ki, vf.add_const(30.0)),
            (q_kp, vf.add_const(0.6)),
            (q_ki, vf.add_const(25.0)),
            (i_max, vf.add_const(max(1.2, 1e-6))),
            (m_max, vf.add_const(max(0.95, 1e-6))),
            (P_loss0, vf.add_const(0.0)),
            (P_loss_i1, vf.add_const(0.01)),
            (P_loss_i2, vf.add_const(0.01)),
            (tau_meas, vf.add_const(max(0.01, 1e-6))),
            (aw_gain, vf.add_const(1.0)),
            (vdc_floor, vf.add_const(max(0.05, 1e-6))),
        ]),
        init_eqs=dict([
            (v_dc, v_dc0),
            (P_ref, p_ref_expr),
            (Q_ref, q_ref_expr),
            (Vdc_ref, vdc_ref_expr),
            (i_dc, i_dc0),
            (P, P0),
            (Q, Q0),
            (i_mag, i_mag0),
            (P_loss, P_loss0_expr),
            (i_dc_conv, i_dc_conv0),
        ]),
        diff_init_eqs=dict([
            (d_v_dc, c0),
        ]),
        in_vars=list([v_d, v_q, v_0, i_d, i_q, i_0, v_dc_bus]),
        out_vars=list([
            v_dc, i_dc, P, Q,
            sbase, P_ref, Q_ref, Vdc_ref, omega_base, phi_v, Vpk,
            R_eq, L_eq, C_dc, R_dc, R_dc_term,
            pll_kp, pll_ki, i_kp, i_ki,
            vdc_kp, vdc_ki, q_kp, q_ki,
            i_max, m_max, P_loss0, P_loss_i1, P_loss_i2, tau_meas, aw_gain, vdc_floor,
        ]),
        name=f"{name}_vsc",
    )
    block.api_obj_mapping = dict([
        (ParamPowerFlowReferenceType.Sbase, sbase),
        (ParamPowerFlowReferenceType.P0, P0_sched),
        (ParamPowerFlowReferenceType.converter_loss_power_0, P_loss0),
        (ParamPowerFlowReferenceType.omega_base, omega_base),
        (ParamPowerFlowReferenceType.converter_control_mode_1, control1),
        (ParamPowerFlowReferenceType.converter_control_mode_2, control2),
        (ParamPowerFlowReferenceType.converter_control_target_1, control1_val),
        (ParamPowerFlowReferenceType.converter_control_target_2, control2_val),
    ])
    return block


def _build_pseudo_emt_converter_outer_loop_block(vf: VarFactory, name: str) -> Block:
    """
    Build the averaged converter outer-loop block reused by the switched template.

    This local copy is kept in sync intentionally so the switched converter does not
    import private helpers from another converter template module.

    :param vf: Shared EMT variable factory.
    :param name: Symbolic model name suffix.
    :return: Outer-loop block.
    """
    v_d: Var = vf.add_var(name=f"v_d_outer_in_{name}")
    v_q: Var = vf.add_var(name=f"v_q_outer_in_{name}")
    v_0: Var = vf.add_var(name=f"v_0_outer_in_{name}")
    i_d: Var = vf.add_var(name=f"i_d_outer_in_{name}")
    i_q: Var = vf.add_var(name=f"i_q_outer_in_{name}")
    i_0: Var = vf.add_var(name=f"i_0_outer_in_{name}")
    v_dc: Var = vf.add_var(name=f"v_dc_outer_in_{name}")
    P: Var = vf.add_var(name=f"P_outer_in_{name}")
    Q: Var = vf.add_var(name=f"Q_outer_in_{name}")
    sbase: Var = vf.add_var(name=f"sbase_outer_in_{name}")
    P_ref: Var = vf.add_var(name=f"P_ref_outer_in_{name}")
    Q_ref: Var = vf.add_var(name=f"Q_ref_outer_in_{name}")
    Vdc_ref: Var = vf.add_var(name=f"Vdc_ref_outer_in_{name}")
    Vpk: Var = vf.add_var(name=f"Vpk_outer_in_{name}")
    P_loss0: Var = vf.add_var(name=f"P_loss0_outer_in_{name}")
    vdc_kp: Var = vf.add_var(name=f"vdc_kp_outer_in_{name}")
    vdc_ki: Var = vf.add_var(name=f"vdc_ki_outer_in_{name}")
    q_kp: Var = vf.add_var(name=f"q_kp_outer_in_{name}")
    q_ki: Var = vf.add_var(name=f"q_ki_outer_in_{name}")
    i_max: Var = vf.add_var(name=f"i_max_outer_in_{name}")
    tau_meas: Var = vf.add_var(name=f"tau_meas_outer_in_{name}")
    aw_gain: Var = vf.add_var(name=f"aw_gain_outer_in_{name}")

    xi_vdc: Var = vf.add_var(name=f"xi_vdc_{name}")
    xi_q: Var = vf.add_var(name=f"xi_q_{name}")
    P_f: Var = vf.add_var(name=f"P_f_{name}")
    Q_f: Var = vf.add_var(name=f"Q_f_{name}")

    d_xi_vdc: Var = vf.add_diff_var(name=f"d_xi_vdc_{name}", base_var=xi_vdc)
    d_xi_q: Var = vf.add_diff_var(name=f"d_xi_q_{name}", base_var=xi_q)
    d_P_f: Var = vf.add_diff_var(name=f"d_P_f_{name}", base_var=P_f)
    d_Q_f: Var = vf.add_diff_var(name=f"d_Q_f_{name}", base_var=Q_f)

    v_mag: Var = vf.add_var(name=f"v_mag_{name}")
    i_d_ff: Var = vf.add_var(name=f"i_d_ff_{name}")
    i_q_ff: Var = vf.add_var(name=f"i_q_ff_{name}")
    i_0_ref_u: Var = vf.add_var(name=f"i_0_ref_u_{name}")
    i_d_ref_u: Var = vf.add_var(name=f"i_d_ref_u_{name}")
    i_q_ref_u: Var = vf.add_var(name=f"i_q_ref_u_{name}")
    i_0_ref: Var = vf.add_var(name=f"i_0_ref_{name}")
    i_d_ref: Var = vf.add_var(name=f"i_d_ref_{name}")
    i_q_ref: Var = vf.add_var(name=f"i_q_ref_{name}")

    eps: Const = vf.add_const(1e-10)
    c0: Const = vf.add_const(0.0)
    c23: Const = vf.add_const(2.0 / 3.0)
    c3: Const = vf.add_const(3.0)
    c32: Const = vf.add_const(1.5)

    P_ref_pu: Expr = P_ref / sbase
    Q_ref_pu: Expr = Q_ref / sbase
    P_ac_ff_pu: Expr = P_ref_pu + P_loss0 / sbase

    i_d0: Expr = c23 * P_ac_ff_pu / (Vpk + eps)
    i_q0: Expr = c23 * Q_ref_pu / (Vpk + eps)
    Q0: Expr = c32 * Vpk * i_q0

    i_d_cap: Expr = sym.hard_sat(i_d_ref_u, -i_max, i_max)
    i_q_cap: Expr = sym.sqrt(sym.max(i_max * i_max - i_d_ref * i_d_ref, eps))
    i_0_cap: Expr = sym.sqrt(sym.max((i_max * i_max - i_d_ref * i_d_ref - i_q_ref * i_q_ref) / c3, eps / c3))

    return Block(
        state_eqs=list([
            vdc_ki * ((Vdc_ref - v_dc) + aw_gain * (i_d_ref - i_d_ref_u)),
            q_ki * ((Q_ref_pu - Q_f) + aw_gain * (i_q_ref - i_q_ref_u)),
            (P - P_f) / tau_meas,
            (Q - Q_f) / tau_meas,
        ]),
        state_vars=list([xi_vdc, xi_q, P_f, Q_f]),
        diff_vars=list([d_xi_vdc, d_xi_q, d_P_f, d_Q_f]),
        algebraic_eqs=list([
            v_mag - sym.sqrt(v_d * v_d + v_q * v_q + eps),
            i_d_ff - c23 * P_ac_ff_pu / (v_mag + eps),
            i_q_ff - c23 * Q_ref_pu / (v_mag + eps),
            i_0_ref_u,
            i_d_ref_u - (i_d_ff + vdc_kp * (Vdc_ref - v_dc) + xi_vdc),
            i_d_ref - i_d_cap,
            i_q_ref_u - (i_q_ff + q_kp * (Q_ref_pu - Q_f) + xi_q),
            i_q_ref - sym.hard_sat(i_q_ref_u, -i_q_cap, i_q_cap),
            i_0_ref - sym.hard_sat(i_0_ref_u, -i_0_cap, i_0_cap),
        ]),
        algebraic_vars=list([v_mag, i_d_ff, i_q_ff, i_0_ref_u, i_d_ref_u, i_q_ref_u, i_0_ref, i_d_ref, i_q_ref]),
        init_eqs=dict([
            (xi_vdc, i_d0 - (c23 * P_ac_ff_pu / (Vpk + eps)) - vdc_kp * (Vdc_ref - v_dc)),
            (xi_q, i_q0 - (c23 * Q_ref_pu / (Vpk + eps)) - q_kp * (Q_ref_pu - Q0)),
            (P_f, P),
            (Q_f, Q0),
            (v_mag, Vpk),
            (i_d_ff, c23 * P_ac_ff_pu / (Vpk + eps)),
            (i_q_ff, c23 * Q_ref_pu / (Vpk + eps)),
            (i_0_ref_u, c0),
            (i_d_ref_u, i_d0),
            (i_q_ref_u, i_q0),
            (i_0_ref, c0),
            (i_d_ref, i_d0),
            (i_q_ref, i_q0),
        ]),
        diff_init_eqs=dict([
            (d_xi_vdc, c0),
            (d_xi_q, c0),
            (d_P_f, c0),
            (d_Q_f, c0),
        ]),
        in_vars=list([
            v_d, v_q, v_0, i_d, i_q, i_0, v_dc, P, Q,
            sbase, P_ref, Q_ref, Vdc_ref, Vpk, P_loss0,
            vdc_kp, vdc_ki, q_kp, q_ki, i_max, tau_meas, aw_gain,
        ]),
        out_vars=list([P_f, Q_f, i_0_ref, i_d_ref, i_q_ref]),
        name=f"{name}_outer_loop",
    )


def _build_switched_converter_data_block(vf: VarFactory, name: str) -> Block:
    """
    Build the parameter and DC-link block of the switched EMT converter.

    The switched converter must preserve the validated pseudo-EMT DC-side model.
    The hybrid handover is introduced only in the converter-current forcing term:
    before the exact handover time the DC-link uses the averaged converter current,
    and after the handover time it uses the switched bridge current.

    :param vf: Shared EMT variable factory.
    :param name: Symbolic model name suffix.
    :return: Data and DC-link block.
    """
    block: Block = _build_pseudo_emt_converter_vsc_block(vf=vf, name=name)
    v_d: Var = _find_var_anywhere_in_block(block=block, variable_name=f"v_d_in_{name}")
    v_q: Var = _find_var_anywhere_in_block(block=block, variable_name=f"v_q_in_{name}")
    v_0: Var = _find_var_anywhere_in_block(block=block, variable_name=f"v_0_in_{name}")
    i_d: Var = _find_var_anywhere_in_block(block=block, variable_name=f"i_d_in_{name}")
    i_q: Var = _find_var_anywhere_in_block(block=block, variable_name=f"i_q_in_{name}")
    i_0: Var = _find_var_anywhere_in_block(block=block, variable_name=f"i_0_in_{name}")
    i_A: Var = vf.add_var(name=f"i_A_in_{name}")
    i_B: Var = vf.add_var(name=f"i_B_in_{name}")
    i_C: Var = vf.add_var(name=f"i_C_in_{name}")
    gate_a: Var = vf.add_var(name=f"gate_a_in_{name}")
    gate_b: Var = vf.add_var(name=f"gate_b_in_{name}")
    gate_c: Var = vf.add_var(name=f"gate_c_in_{name}")
    v_dc_bus: Var = _find_var_anywhere_in_block(block=block, variable_name=f"v_dc_bus_in_{name}")
    switching_enabled: Var = vf.add_var(name=f"switching_enabled_in_{name}")

    v_dc: Var = _find_var_anywhere_in_block(block=block, variable_name=f"v_dc_{name}")
    i_dc: Var = _find_var_anywhere_in_block(block=block, variable_name=f"i_dc_{name}")
    P: Var = _find_var_anywhere_in_block(block=block, variable_name=f"P_{name}")
    Q: Var = _find_var_anywhere_in_block(block=block, variable_name=f"Q_{name}")
    i_mag: Var = _find_var_anywhere_in_block(block=block, variable_name=f"i_mag_{name}")
    P_loss: Var = _find_var_anywhere_in_block(block=block, variable_name=f"P_loss_{name}")
    i_dc_conv: Var = _find_var_anywhere_in_block(block=block, variable_name=f"i_dc_conv_{name}")
    k_v_conv_nom: Var = vf.add_var(name=f"k_v_conv_nom_{name}")
    P_ref: Var = _find_var_anywhere_in_block(block=block, variable_name=f"P_ref_{name}")
    Q_ref: Var = _find_var_anywhere_in_block(block=block, variable_name=f"Q_ref_{name}")
    Vdc_ref: Var = _find_var_anywhere_in_block(block=block, variable_name=f"Vdc_ref_{name}")

    sbase: Var = _find_var_anywhere_in_block(block=block, variable_name=f"sbase_{name}")
    P0_sched: Var = _find_var_anywhere_in_block(block=block, variable_name=f"P0_{name}")
    control1: Var = _find_var_anywhere_in_block(block=block, variable_name=f"control1_{name}")
    control2: Var = _find_var_anywhere_in_block(block=block, variable_name=f"control2_{name}")
    control1_val: Var = _find_var_anywhere_in_block(block=block, variable_name=f"control1_val_{name}")
    control2_val: Var = _find_var_anywhere_in_block(block=block, variable_name=f"control2_val_{name}")
    omega_base: Var = _find_var_anywhere_in_block(block=block, variable_name=f"omega_base_{name}")
    phi_v: Var = _find_var_anywhere_in_block(block=block, variable_name=f"phi_v_{name}")
    Vpk: Var = _find_var_anywhere_in_block(block=block, variable_name=f"Vpk_{name}")
    R_eq: Var = _find_var_anywhere_in_block(block=block, variable_name=f"R_eq_{name}")
    L_eq: Var = _find_var_anywhere_in_block(block=block, variable_name=f"L_eq_{name}")
    C_dc: Var = _find_var_anywhere_in_block(block=block, variable_name=f"C_dc_{name}")
    R_dc: Var = _find_var_anywhere_in_block(block=block, variable_name=f"R_dc_{name}")
    R_dc_term: Var = _find_var_anywhere_in_block(block=block, variable_name=f"R_dc_term_{name}")
    pll_kp: Var = _find_var_anywhere_in_block(block=block, variable_name=f"pll_kp_{name}")
    pll_ki: Var = _find_var_anywhere_in_block(block=block, variable_name=f"pll_ki_{name}")
    i_kp: Var = _find_var_anywhere_in_block(block=block, variable_name=f"i_kp_{name}")
    i_ki: Var = _find_var_anywhere_in_block(block=block, variable_name=f"i_ki_{name}")
    vdc_kp: Var = _find_var_anywhere_in_block(block=block, variable_name=f"vdc_kp_{name}")
    vdc_ki: Var = _find_var_anywhere_in_block(block=block, variable_name=f"vdc_ki_{name}")
    q_kp: Var = _find_var_anywhere_in_block(block=block, variable_name=f"q_kp_{name}")
    q_ki: Var = _find_var_anywhere_in_block(block=block, variable_name=f"q_ki_{name}")
    i_max: Var = _find_var_anywhere_in_block(block=block, variable_name=f"i_max_{name}")
    m_max: Var = _find_var_anywhere_in_block(block=block, variable_name=f"m_max_{name}")
    P_loss0: Var = _find_var_anywhere_in_block(block=block, variable_name=f"P_loss0_{name}")
    P_loss_i1: Var = _find_var_anywhere_in_block(block=block, variable_name=f"P_loss_i1_{name}")
    P_loss_i2: Var = _find_var_anywhere_in_block(block=block, variable_name=f"P_loss_i2_{name}")
    tau_meas: Var = _find_var_anywhere_in_block(block=block, variable_name=f"tau_meas_{name}")
    aw_gain: Var = _find_var_anywhere_in_block(block=block, variable_name=f"aw_gain_{name}")
    vdc_floor: Var = _find_var_anywhere_in_block(block=block, variable_name=f"vdc_floor_{name}")
    omega_sw: Var = vf.add_var(name=f"omega_sw_{name}")
    carrier_phase: Var = vf.add_var(name=f"carrier_phase_{name}")

    if v_d is None or v_q is None or v_0 is None or i_d is None or i_q is None or i_0 is None or v_dc_bus is None or v_dc is None or i_dc is None or P is None or Q is None or i_mag is None or P_loss is None or i_dc_conv is None or P_ref is None or Q_ref is None or Vdc_ref is None or sbase is None or P0_sched is None or control1 is None or control2 is None or control1_val is None or control2_val is None or omega_base is None or phi_v is None or Vpk is None or R_eq is None or L_eq is None or C_dc is None or R_dc is None or R_dc_term is None or pll_kp is None or pll_ki is None or i_kp is None or i_ki is None or vdc_kp is None or vdc_ki is None or q_kp is None or q_ki is None or i_max is None or m_max is None or P_loss0 is None or P_loss_i1 is None or P_loss_i2 is None or tau_meas is None or aw_gain is None or vdc_floor is None:
        raise KeyError(f"The switched EMT converter data block '{name}' could not resolve one or more pseudo-EMT variables")
    else:
        pass

    p_ref_expr: Expr
    q_ref_expr: Expr
    vdc_ref_expr: Expr
    _unused_regulate_vdc: Expr
    _unused_regulate_q: Expr
    p_ref_expr, q_ref_expr, vdc_ref_expr, _unused_regulate_vdc, _unused_regulate_q = _resolve_converter_control_reference_exprs(
        control1=control1,
        control2=control2,
        control1_val=control1_val,
        control2_val=control2_val,
        p0=P0_sched,
    )

    eps: Const = Const(1.0e-10)
    c23: Const = Const(2.0 / 3.0)
    c_one: Const = Const(1.0)
    c_two: Const = Const(2.0)
    c_three: Const = Const(3.0)
    sbase_eff: Expr = sym.max(sbase, eps)
    v_dc_eff: Expr = sym.max(v_dc, vdc_floor)
    gate_current_sum_expr: Expr = gate_a * i_A + gate_b * i_B + gate_c * i_C
    gate_sum_expr: Expr = gate_a + gate_b + gate_c
    phase_current_sum_expr: Expr = i_A + i_B + i_C
    # The switched bridge does not use physical pole voltages of ``+-v_dc/2``. Instead, each retained
    # leg is scaled as ``(2 * gate - 1) * k_v_conv * v_dc`` so the bridge reproduces the averaged
    # converter fundamental. The DC current reconstruction must therefore follow the instantaneous
    # bridge power balance rather than the unscaled gated-current sum.
    i_dc_sw_expr: Expr = -c_two * k_v_conv_nom * (
        gate_current_sum_expr - (gate_sum_expr * phase_current_sum_expr) / c_three
    )
    p_loss0_pu_expr: Expr = P_loss0 / sbase_eff
    i_dc_avg_expr: Expr = -(P - P_loss) / v_dc_eff
    i_dc_conv_eff_expr: Expr = (c_one - switching_enabled) * i_dc_avg_expr + switching_enabled * i_dc_sw_expr

    # The hybrid switched path must preserve the same nominal voltage ceiling used by the
    # validated pseudo-EMT inner loop. Rebuilding that operating-point command here keeps the
    # current controller limit aligned across the averaged and switched templates.
    i_d_nom_expr: Expr = c23 * ((P_ref / sbase_eff) + p_loss0_pu_expr) / (Vpk + eps)
    i_q_nom_expr: Expr = c23 * (Q_ref / sbase_eff) / (Vpk + eps)
    v_cmd_d_nom_expr: Expr = Vpk - R_eq * i_d_nom_expr + L_eq * i_q_nom_expr
    v_cmd_q_nom_expr: Expr = -R_eq * i_q_nom_expr - L_eq * i_d_nom_expr
    k_v_conv_nom_expr: Expr = (
        sym.sqrt(v_cmd_d_nom_expr * v_cmd_d_nom_expr + v_cmd_q_nom_expr * v_cmd_q_nom_expr + eps)
        / (m_max * sym.max(Vdc_ref, vdc_floor) + eps)
    )

    i_dc_conv_eq_index: int = _find_algebraic_equation_index_for_var(block=block, target_var=i_dc_conv)

    # The validated pseudo-EMT DC-link dynamics are preserved. Only the effective converter current
    # forcing term becomes hybrid, while the pure switched bridge current can be reconstructed outside.
    block.algebraic_eqs[i_dc_conv_eq_index] = i_dc_conv - i_dc_conv_eff_expr
    block.init_eqs[i_dc_conv] = i_dc_conv_eff_expr

    # The plant controller still needs the nominal voltage ceiling used to scale modulation commands.
    block.algebraic_eqs.append(k_v_conv_nom - k_v_conv_nom_expr)
    block.algebraic_vars.append(k_v_conv_nom)
    block.init_eqs[k_v_conv_nom] = k_v_conv_nom_expr

    # PWM timing remains a runtime parameter owned by the converter data block.
    block.event_dict[omega_sw] = Const(2.0 * np.pi * 1000.0)
    block.event_dict[carrier_phase] = Const(0.0)

    # The parent block connects dq measurements first, then the bridge currents and retained gates.
    block.in_vars = list([
        v_d,
        v_q,
        v_0,
        i_d,
        i_q,
        i_0,
        i_A,
        i_B,
        i_C,
        gate_a,
        gate_b,
        gate_c,
        v_dc_bus,
        switching_enabled,
    ])
    block.out_vars = list([
        v_dc,
        i_dc,
        P,
        Q,
        k_v_conv_nom,
        sbase,
        P_ref,
        Q_ref,
        Vdc_ref,
        omega_base,
        phi_v,
        Vpk,
        R_eq,
        L_eq,
        C_dc,
        R_dc,
        R_dc_term,
        pll_kp,
        pll_ki,
        i_kp,
        i_ki,
        vdc_kp,
        vdc_ki,
        q_kp,
        q_ki,
        i_max,
        m_max,
        P_loss0,
        P_loss_i1,
        P_loss_i2,
        tau_meas,
        aw_gain,
        vdc_floor,
        omega_sw,
        carrier_phase,
    ])
    block.name = f"{name}_switched_data"

    return block


def _find_child_block_by_name(root_block: Block, block_name: str) -> Block | None:
    """
    Return one nested child block by its symbolic name.

    :param root_block: Root block to inspect.
    :param block_name: Requested block name.
    :return: Matching child block or ``None``.
    """
    child_block: Block
    nested_block: Block | None

    for child_block in root_block.children:
        if child_block.name == block_name:
            return child_block
        else:
            pass

        nested_block = _find_child_block_by_name(root_block=child_block, block_name=block_name)
        if nested_block is not None:
            return nested_block
        else:
            pass

    return None


def _find_var_anywhere_in_block(block: Block, variable_name: str) -> Var | None:
    """
    Return one symbolic variable from a block, including runtime and constant parameter stores.

    The generic ``find_name_in_block()`` helper only scans state/algebraic/input/output variables.
    The switched converter data-block reuse also needs access to event, mode and parameter symbols
    owned by the pseudo-EMT DC-side block.

    :param block: Block to inspect.
    :param variable_name: Requested symbolic-variable name.
    :return: Matching variable or ``None``.
    """
    variable_groups: list[list[Var]] = list([
        block.in_vars,
        block.out_vars,
        block.algebraic_vars,
        block.state_vars,
        block.diff_vars,
        list(block.event_dict.keys()),
        list(block.mode_dict.keys()),
        list(block.parameters.keys()),
    ])
    variable_group: list[Var]
    variable: Var
    child_block: Block
    nested_result: Var | None

    # The switched template must access pseudo-EMT runtime and parameter symbols in addition to state variables.
    for variable_group in variable_groups:
        for variable in variable_group:
            if variable.name == variable_name:
                return variable
            else:
                pass

    for child_block in block.children:
        nested_result = _find_var_anywhere_in_block(block=child_block, variable_name=variable_name)
        if nested_result is not None:
            return nested_result
        else:
            pass

    return None


def _find_algebraic_equation_index_for_var(block: Block, target_var: Var) -> int:
    """
    Return the algebraic-equation index whose left operand is the requested variable.

    Some legacy symbolic blocks do not keep ``algebraic_vars`` and ``algebraic_eqs`` in a perfectly
    aligned semantic order. When one equation must be replaced surgically, the robust way is to scan
    the residual expressions and locate the equation that is actually written for the requested symbol.

    :param block: Block to inspect.
    :param target_var: Variable whose residual equation must be located.
    :return: Matching algebraic-equation index.
    """
    equation_index: int = 0
    equation: Expr

    while equation_index < len(block.algebraic_eqs):
        equation = block.algebraic_eqs[equation_index]

        if isinstance(equation, BinOp):
            if isinstance(equation.left, Var):
                if equation.left.uid == target_var.uid:
                    return equation_index
                else:
                    pass
            else:
                pass
        else:
            pass

        equation_index += 1

    raise KeyError(f"The block '{block.name}' could not locate an algebraic equation for '{target_var.name}'")


[docs] def get_switched_emt_converter(vf: VarFactory, name: str = "switched_converter_emt") -> EmtModelTemplate: """ Assemble a hybrid switched EMT converter with the same external interface as the averaged converter. The switched converter is rebuilt incrementally around the validated ``bridge + filter + control`` plant. The startup remains averaged until the exact ``t_enable_sw`` handover time, and from that point on the bridge/filter plant is seen electrically by the rest of the converter. :param vf: Shared EMT variable factory. :param name: Symbolic model name. :return: Switched EMT converter template. """ templ: EmtModelTemplate = EmtModelTemplate() templ.tpe = DeviceType.VscDevice templ.name = name templ.block.name = name v_A: Var = vf.add_var(name=f"v_A_{name}", reference=VarPowerFlowReferenceType.v_A) v_B: Var = vf.add_var(name=f"v_B_{name}", reference=VarPowerFlowReferenceType.v_B) v_C: Var = vf.add_var(name=f"v_C_{name}", reference=VarPowerFlowReferenceType.v_C) v_dc_bus: Var = vf.add_var(name=f"v_dc_bus_{name}", reference=VarPowerFlowReferenceType.Vdc) switching_enabled_mode: Var = vf.add_var(name=f"switching_enabled_mode_{name}") t_enable_sw: Var = vf.add_var(name=f"t_enable_sw_{name}") i_A: Var = vf.add_var(name=f"i_A_{name}", reference=VarPowerFlowReferenceType.i_A) i_B: Var = vf.add_var(name=f"i_B_{name}", reference=VarPowerFlowReferenceType.i_B) i_C: Var = vf.add_var(name=f"i_C_{name}", reference=VarPowerFlowReferenceType.i_C) gate_a: Var = vf.add_var(name=f"gate_a_{name}") gate_b: Var = vf.add_var(name=f"gate_b_{name}") gate_c: Var = vf.add_var(name=f"gate_c_{name}") v_conv_a: Var = vf.add_var(name=f"v_conv_a_{name}") v_conv_b: Var = vf.add_var(name=f"v_conv_b_{name}") v_conv_c: Var = vf.add_var(name=f"v_conv_c_{name}") v_conv_d: Var = vf.add_var(name=f"v_conv_d_{name}") v_conv_q: Var = vf.add_var(name=f"v_conv_q_{name}") v_conv_0: Var = vf.add_var(name=f"v_conv_0_{name}") omega_sw_eff: Var = vf.add_var(name=f"omega_sw_eff_{name}") data_block: Block = _build_switched_converter_data_block(vf=vf, name=name) outer_loop_block: Block = _build_pseudo_emt_converter_outer_loop_block(vf=vf, name=name) plant_block: Block = get_bridge_filter_control_2level_3ph_emt_template(vf=vf, name=name).block filter_stage_block: Block = plant_block data_lookup: Dict[str, Var] = build_name_to_var_lookup(data_block) outer_loop_lookup: Dict[str, Var] = build_name_to_var_lookup(outer_loop_block) plant_lookup: Dict[str, Var] = build_name_to_var_lookup(plant_block) data_vdc: Var | None = data_lookup.get(f"v_dc_{name}", None) data_idc: Var | None = data_lookup.get(f"i_dc_{name}", None) data_p: Var | None = data_lookup.get(f"P_{name}", None) data_q: Var | None = data_lookup.get(f"Q_{name}", None) data_sbase: Var | None = data_lookup.get(f"sbase_{name}", None) data_p_ref: Var | None = data_lookup.get(f"P_ref_{name}", None) data_q_ref: Var | None = data_lookup.get(f"Q_ref_{name}", None) data_vdc_ref: Var | None = data_lookup.get(f"Vdc_ref_{name}", None) data_omega_base: Var | None = data_lookup.get(f"omega_base_{name}", None) data_phi_v: Var | None = data_lookup.get(f"phi_v_{name}", None) data_vpk: Var | None = data_lookup.get(f"Vpk_{name}", None) data_r_eq: Var | None = data_lookup.get(f"R_eq_{name}", None) data_l_eq: Var | None = data_lookup.get(f"L_eq_{name}", None) data_pll_kp: Var | None = data_lookup.get(f"pll_kp_{name}", None) data_pll_ki: Var | None = data_lookup.get(f"pll_ki_{name}", None) data_i_kp: Var | None = data_lookup.get(f"i_kp_{name}", None) data_i_ki: Var | None = data_lookup.get(f"i_ki_{name}", None) data_vdc_kp: Var | None = data_lookup.get(f"vdc_kp_{name}", None) data_vdc_ki: Var | None = data_lookup.get(f"vdc_ki_{name}", None) data_q_kp: Var | None = data_lookup.get(f"q_kp_{name}", None) data_q_ki: Var | None = data_lookup.get(f"q_ki_{name}", None) data_i_max: Var | None = data_lookup.get(f"i_max_{name}", None) data_m_max: Var | None = data_lookup.get(f"m_max_{name}", None) data_p_loss0: Var | None = data_lookup.get(f"P_loss0_{name}", None) data_tau_meas: Var | None = data_lookup.get(f"tau_meas_{name}", None) data_aw_gain: Var | None = data_lookup.get(f"aw_gain_{name}", None) data_vdc_floor: Var | None = data_lookup.get(f"vdc_floor_{name}", None) data_omega_sw: Var | None = data_lookup.get(f"omega_sw_{name}", None) data_carrier_phase: Var | None = data_lookup.get(f"carrier_phase_{name}", None) plant_i_A: Var | None = plant_lookup.get(f"i_A_{name}_plant", None) plant_i_B: Var | None = plant_lookup.get(f"i_B_{name}_plant", None) plant_i_C: Var | None = plant_lookup.get(f"i_C_{name}_plant", None) plant_i_d: Var | None = plant_lookup.get(f"i_d_{name}_plant", None) plant_i_q: Var | None = plant_lookup.get(f"i_q_{name}_plant", None) plant_i_0: Var | None = plant_lookup.get(f"i_0_{name}_plant", None) plant_v_d: Var | None = plant_lookup.get(f"v_d_{name}_plant", None) plant_v_q: Var | None = plant_lookup.get(f"v_q_{name}_plant", None) plant_v_0: Var | None = plant_lookup.get(f"v_0_{name}_plant", None) plant_gate_a: Var | None = plant_lookup.get(f"gate_a_{name}_plant_bridge", None) plant_gate_b: Var | None = plant_lookup.get(f"gate_b_{name}_plant_bridge", None) plant_gate_c: Var | None = plant_lookup.get(f"gate_c_{name}_plant_bridge", None) plant_v_ref_a: Var | None = plant_lookup.get(f"v_ref_a_{name}_plant_bridge", None) plant_v_ref_b: Var | None = plant_lookup.get(f"v_ref_b_{name}_plant_bridge", None) plant_v_ref_c: Var | None = plant_lookup.get(f"v_ref_c_{name}_plant_bridge", None) plant_v_conv_a: Var | None = plant_lookup.get(f"v_conv_a_{name}_plant_bridge", None) plant_v_conv_b: Var | None = plant_lookup.get(f"v_conv_b_{name}_plant_bridge", None) plant_v_conv_c: Var | None = plant_lookup.get(f"v_conv_c_{name}_plant_bridge", None) plant_v_conv_d: Var | None = plant_lookup.get(f"v_conv_d_{name}_plant_bridge", None) plant_v_conv_q: Var | None = plant_lookup.get(f"v_conv_q_{name}_plant_bridge", None) plant_v_conv_0: Var | None = plant_lookup.get(f"v_conv_0_{name}_plant_bridge", None) plant_v_d_meas_f: Var | None = plant_lookup.get(f"v_d_meas_f_{name}", None) plant_v_q_meas_f: Var | None = plant_lookup.get(f"v_q_meas_f_{name}", None) plant_i_d_meas_f: Var | None = plant_lookup.get(f"i_d_meas_f_{name}", None) plant_i_q_meas_f: Var | None = plant_lookup.get(f"i_q_meas_f_{name}", None) plant_i_0_meas_f: Var | None = plant_lookup.get(f"i_0_meas_f_{name}", None) plant_v_0_meas_f: Var | None = plant_lookup.get(f"v_0_meas_f_{name}", None) plant_v_cmd_d: Var | None = plant_lookup.get(f"v_cmd_d_{name}", None) plant_v_cmd_q: Var | None = plant_lookup.get(f"v_cmd_q_{name}", None) plant_v_cmd_0: Var | None = plant_lookup.get(f"v_cmd_0_{name}", None) outer_i_0_ref: Var | None = outer_loop_lookup.get(f"i_0_ref_{name}", None) outer_i_d_ref: Var | None = outer_loop_lookup.get(f"i_d_ref_{name}", None) outer_i_q_ref: Var | None = outer_loop_lookup.get(f"i_q_ref_{name}", None) if data_vdc is None or data_idc is None or data_p is None or data_q is None or data_sbase is None or data_p_ref is None or data_q_ref is None or data_vdc_ref is None or data_omega_base is None or data_phi_v is None or data_vpk is None or data_r_eq is None or data_l_eq is None or data_pll_kp is None or data_pll_ki is None or data_i_kp is None or data_i_ki is None or data_vdc_kp is None or data_vdc_ki is None or data_q_kp is None or data_q_ki is None or data_i_max is None or data_m_max is None or data_p_loss0 is None or data_tau_meas is None or data_aw_gain is None or data_vdc_floor is None or data_omega_sw is None or data_carrier_phase is None or plant_i_A is None or plant_i_B is None or plant_i_C is None or plant_i_d is None or plant_i_q is None or plant_i_0 is None or plant_v_d is None or plant_v_q is None or plant_v_0 is None or plant_gate_a is None or plant_gate_b is None or plant_gate_c is None or plant_v_ref_a is None or plant_v_ref_b is None or plant_v_ref_c is None or plant_v_conv_a is None or plant_v_conv_b is None or plant_v_conv_c is None or plant_v_conv_d is None or plant_v_conv_q is None or plant_v_conv_0 is None or plant_v_d_meas_f is None or plant_v_q_meas_f is None or plant_v_0_meas_f is None or plant_i_d_meas_f is None or plant_i_q_meas_f is None or plant_i_0_meas_f is None or plant_v_cmd_d is None or plant_v_cmd_q is None or plant_v_cmd_0 is None or outer_i_0_ref is None or outer_i_d_ref is None or outer_i_q_ref is None: raise KeyError(f"The switched EMT converter '{name}' could not resolve one or more internal variables") else: pass one: Const = Const(1.0) averaged_mode: Expr = one - switching_enabled_mode # The bridge keeps switching procedurally from t = 0, but the RL filter only sees the discrete # bridge pole voltages after the exact startup handover time. filter_stage_block.state_eqs = list([ equation.subs({ plant_v_conv_a: v_conv_a, plant_v_conv_b: v_conv_b, plant_v_conv_c: v_conv_c, }) for equation in filter_stage_block.state_eqs ]) templ.block.algebraic_eqs.extend(list([ i_A - plant_i_A, i_B - plant_i_B, i_C - plant_i_C, gate_a - plant_gate_a, gate_b - plant_gate_b, gate_c - plant_gate_c, v_conv_a - (averaged_mode * plant_v_ref_a + switching_enabled_mode * plant_v_conv_a), v_conv_b - (averaged_mode * plant_v_ref_b + switching_enabled_mode * plant_v_conv_b), v_conv_c - (averaged_mode * plant_v_ref_c + switching_enabled_mode * plant_v_conv_c), v_conv_d - (averaged_mode * plant_v_cmd_d + switching_enabled_mode * plant_v_conv_d), v_conv_q - (averaged_mode * plant_v_cmd_q + switching_enabled_mode * plant_v_conv_q), v_conv_0 - (averaged_mode * plant_v_cmd_0 + switching_enabled_mode * plant_v_conv_0), ])) templ.block.algebraic_vars.extend(list([ i_A, i_B, i_C, gate_a, gate_b, gate_c, v_conv_a, v_conv_b, v_conv_c, v_conv_d, v_conv_q, v_conv_0, ])) templ.block.init_eqs.update(dict([ (i_A, plant_i_A), (i_B, plant_i_B), (i_C, plant_i_C), (gate_a, plant_gate_a), (gate_b, plant_gate_b), (gate_c, plant_gate_c), (v_conv_a, averaged_mode * plant_v_ref_a + switching_enabled_mode * plant_v_conv_a), (v_conv_b, averaged_mode * plant_v_ref_b + switching_enabled_mode * plant_v_conv_b), (v_conv_c, averaged_mode * plant_v_ref_c + switching_enabled_mode * plant_v_conv_c), (v_conv_d, averaged_mode * plant_v_cmd_d + switching_enabled_mode * plant_v_conv_d), (v_conv_q, averaged_mode * plant_v_cmd_q + switching_enabled_mode * plant_v_conv_q), (v_conv_0, averaged_mode * plant_v_cmd_0 + switching_enabled_mode * plant_v_conv_0), ])) templ.block.event_dict.update(dict([ (t_enable_sw, Const(1.0e-4)), # Keeping the PWM carrier frequency at zero before the handover prevents the bridge procedural # logic from introducing discrete switching activity while the converter is still in averaged mode. (omega_sw_eff, switching_enabled_mode * data_omega_sw), ])) templ.block.mode_dict.update(dict([ (switching_enabled_mode, Const(0.0)), ])) templ.block.procedural_logic.append( startup_handover( mode=switching_enabled_mode, t_enable=t_enable_sw, name=f"startup_handover_{name}", ) ) # The data block should compute instantaneous power from the raw plant dq0 quantities. Filtering # ``v_dq0`` and ``i_dq0`` before forming ``P`` and ``Q`` introduces an avoidable phase bias, while # the outer-loop ``P_f`` / ``Q_f`` states already provide the intended low-frequency power filtering. data_block.connect(data_block.in_vars[0:14], list([ plant_v_d, plant_v_q, plant_v_0, plant_i_d, plant_i_q, plant_i_0, plant_i_A, plant_i_B, plant_i_C, plant_gate_a, plant_gate_b, plant_gate_c, v_dc_bus, switching_enabled_mode, ])) # The outer loop must see the same filtered plant measurements as the PLL and current loop so the # switched converter keeps one coherent low-frequency control view across all control layers. outer_loop_block.connect(outer_loop_block.in_vars[0:22], list([ plant_v_d_meas_f, plant_v_q_meas_f, plant_v_0_meas_f, plant_i_d_meas_f, plant_i_q_meas_f, plant_i_0_meas_f, data_vdc, data_p, data_q, data_sbase, data_p_ref, data_q_ref, data_vdc_ref, data_vpk, data_p_loss0, data_vdc_kp, data_vdc_ki, data_q_kp, data_q_ki, data_i_max, data_tau_meas, data_aw_gain, ])) # The bridge + filter + control plant closes its own PLL. The parent provides the electrical # references, the effective bridge PWM frequency, and the real carrier frequency used to shape # the measurement filters seen by the control hierarchy during and after the handover. plant_block.connect(plant_block.in_vars, list([ data_omega_base, v_A, v_B, v_C, data_vdc, outer_i_d_ref, outer_i_q_ref, outer_i_0_ref, data_m_max, data_vdc_floor, omega_sw_eff, data_omega_sw, data_carrier_phase, data_r_eq, data_l_eq, data_pll_kp, data_pll_ki, data_i_kp, data_i_ki, data_aw_gain, data_phi_v, data_vdc_ref, data_sbase, data_p_ref, data_q_ref, data_p_loss0, data_vpk, ])) templ.block.children.extend(list([data_block, outer_loop_block, plant_block])) templ.block.unify_blocks() templ.block.in_vars = list([v_A, v_B, v_C, v_dc_bus]) templ.block.out_vars = list([ i_A, i_B, i_C, data_idc, gate_a, gate_b, gate_c, v_conv_a, v_conv_b, v_conv_c, v_conv_d, v_conv_q, v_conv_0, ]) templ.block.external_mapping = dict([ (VarPowerFlowReferenceType.v_A, v_A), (VarPowerFlowReferenceType.v_B, v_B), (VarPowerFlowReferenceType.v_C, v_C), (VarPowerFlowReferenceType.Vdc, v_dc_bus), (VarPowerFlowReferenceType.i_A, i_A), (VarPowerFlowReferenceType.i_B, i_B), (VarPowerFlowReferenceType.i_C, i_C), (VarPowerFlowReferenceType.Idc, data_idc), (VarPowerFlowReferenceType.P, data_p), (VarPowerFlowReferenceType.Q, data_q), (VarPowerFlowReferenceType.phi_v, data_phi_v), (VarPowerFlowReferenceType.Vpk, data_vpk), ]) templ.block.api_obj_mapping = dict(data_block.api_obj_mapping) return templ