Source code for VeraGridEngine.Templates.Emt.converter_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 see it at https://mozilla.org/MPL/2.0/.
# SPDX-License-Identifier: MPL-2.0

import numpy as np
from typing import List, Dict, Tuple
from VeraGridEngine.enumerations import DeviceType, ConverterControlType, ParamPowerFlowReferenceType
from VeraGridEngine.Devices.Dynamic.emt_template import EmtModelTemplate
from VeraGridEngine.Utils.Symbolic.block import Block, VarPowerFlowReferenceType
from VeraGridEngine.Utils.Symbolic.symbolic import Var, Const
from VeraGridEngine.Devices.Dynamic.var_factory import VarFactory
import VeraGridEngine.Utils.Symbolic.symbolic as sym

"""
Standard converter template for EMT simulation.

This is an ideal converter model that connects a DC bus with an AC bus,
maintaining power balance: P_ac = P_dc

The converter takes:
- DC side: V_dc (bus DC voltage) as input
- AC side: v_A, v_B, v_C (bus AC instantaneous voltages) as inputs

And provides:
- DC side: I_dc (current flowing into the DC bus)
- AC side: i_A, i_B, i_C (instantaneous three-phase currents)

External mapping for EmtProblemDae:
- v_A, v_B, v_C: AC bus voltages (from bus)
- Vdc: DC bus voltage (from bus)
- i_A, i_B, i_C: AC phase currents (to bus - entering the bus)
- Idc: DC current (to bus - entering the DC bus)

For KCL connection:
- If the DC bus is bus_from: If_dc is looked up
- If the DC bus is bus_to: It_dc is looked up
- AC currents i_A, i_B, i_C are added to the AC bus KCL
"""

def _converter_control_type_code(control: ConverterControlType | None) -> int:
    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) -> sym.Expr:
    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,
    vdc_nom: Var,
) -> Tuple[sym.Expr, sym.Expr, sym.Expr, sym.Expr, sym.Expr, sym.Expr]:
    control1_is_vm_dc = _converter_control_match_expr(control1, ConverterControlType.Vm_dc)
    control2_is_vm_dc = _converter_control_match_expr(control2, ConverterControlType.Vm_dc)
    control1_is_qac = _converter_control_match_expr(control1, ConverterControlType.Qac)
    control2_is_qac = _converter_control_match_expr(control2, ConverterControlType.Qac)
    control1_is_pac = _converter_control_match_expr(control1, ConverterControlType.Pac)
    control2_is_pac = _converter_control_match_expr(control2, ConverterControlType.Pac)
    control1_is_pdc = _converter_control_match_expr(control1, ConverterControlType.Pdc)
    control2_is_pdc = _converter_control_match_expr(control2, ConverterControlType.Pdc)

    regulate_vdc = sym.max(control1_is_vm_dc, control2_is_vm_dc)
    regulate_q = sym.max(control1_is_qac, control2_is_qac)
    regulate_active = sym.max(control1_is_pac + control1_is_pdc, control2_is_pac + control2_is_pdc)
    vdc_ref = (
        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) * vdc_nom)
    )
    q_ref = (
        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))
    )
    # ------------------------------------------------------------------
    # Preserve the power-flow operating point when neither controller is an
    # explicit active-power mode. This is required by the EMT scripting cases
    # that regulate DC voltage and reactive power around a non-zero scheduled
    # active transfer obtained from the solved power-flow state.
    # ------------------------------------------------------------------
    p_ref = (
        (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, regulate_active


[docs] def get_emt_ideal_converter( vf: VarFactory, name: str = "ideal_converter_emt", ) -> EmtModelTemplate: """ Build an idealized averaged EMT converter model current injection type with DC power balance. It does not represent: switching, PLL, inner current loop, L filter, DC capacitor, Current or voltage dependent losses Transformer Snubbers or filters The AC-side current reference is synthesized from fundamental-frequency P/Q commands in a synchronized rotating frame. This is a better physical approximation of a grid-following converter than an instantaneous constant-power current source. ``P_ref``, ``Q_ref`` and ``P_loss`` are interpreted in system-base units (MW / MVAr on ``sbase``), because that is the convention used by the EMT initialization layer when it maps power-flow values into external references. :param vf: Variable factory for creating symbolic variables :param name: Name for the converter model :return: EmtModelTemplate with the converter block """ templ = EmtModelTemplate() templ.tpe = DeviceType.VscDevice templ.name = name templ.block.name = name # ================================================================= # INPUT VARIABLES (from buses) # ================================================================= # AC side voltages from the AC bus v_A = vf.add_var(name=f"v_A_{name}", reference=VarPowerFlowReferenceType.v_A) v_B = vf.add_var(name=f"v_B_{name}", reference=VarPowerFlowReferenceType.v_B) v_C = vf.add_var(name=f"v_C_{name}", reference=VarPowerFlowReferenceType.v_C) # DC side voltage from the DC bus v_dc = vf.add_var(name=f"v_dc_{name}", reference=VarPowerFlowReferenceType.Vdc) inputs = [v_A, v_B, v_C, v_dc] # ================================================================= # OUTPUT VARIABLES (to buses) # ================================================================= i_A = vf.add_var(name=f"i_A_{name}", reference=VarPowerFlowReferenceType.i_A) i_B = vf.add_var(name=f"i_B_{name}", reference=VarPowerFlowReferenceType.i_B) i_C = vf.add_var(name=f"i_C_{name}", reference=VarPowerFlowReferenceType.i_C) theta = vf.add_var(name=f"theta_{name}") d_theta = vf.add_diff_var(name=f"d_theta_{name}", base_var=theta) i_dc = vf.add_var(name=f"i_dc_{name}", reference=VarPowerFlowReferenceType.Idc) P = vf.add_var(name=f"P_{name}", reference=VarPowerFlowReferenceType.P) Q = vf.add_var(name=f"Q_{name}", reference=VarPowerFlowReferenceType.Q) # ================================================================= # INTERNAL VARIABLES # ================================================================= P_ref = vf.add_var(name=f"P_ref_{name}") Q_ref = vf.add_var(name=f"Q_ref_{name}") Vdc_ref = vf.add_var(name=f"Vdc_ref_{name}") P_loss = vf.add_var(name=f"P_loss_{name}") P0 = vf.add_var(name=f"P0_{name}") omega_base = vf.add_var(name=f"omega_base_{name}") sbase = vf.add_var(name=f"sbase_{name}") control1 = vf.add_var(name=f"control1_{name}") control2 = vf.add_var(name=f"control2_{name}") control1_val = vf.add_var(name=f"control1_val_{name}") control2_val = vf.add_var(name=f"control2_val_{name}") phi_v = vf.add_var(name=f"phi_v_{name}") Vpk = vf.add_var(name=f"Vpk_{name}") Vdc_nom = vf.add_var(name=f"Vdc_nom_{name}") # ================================================================= # PARAMETERS # ================================================================= p_ref_expr, q_ref_expr, vdc_ref_expr, regulate_vdc, regulate_q, _ = _resolve_converter_control_reference_exprs( control1=control1, control2=control2, control1_val=control1_val, control2_val=control2_val, p0=P0, vdc_nom=Vdc_nom, ) eps = vf.add_const(1e-10) c_two_over_three = vf.add_const(2.0 / 3.0) c_sqrt3_over_two = vf.add_const(np.sqrt(3.0) / 2.0) c_half = vf.add_const(0.5) sbase_eff = sym.max(sbase, eps) event_dict = { P_loss: vf.add_const(0.0), P0: vf.add_const(0.0), omega_base: vf.add_const(2.0 * np.pi * 50.0), sbase: vf.add_const(1.0), control1: vf.add_const(float(_converter_control_type_code(None))), control2: vf.add_const(float(_converter_control_type_code(None))), control1_val: vf.add_const(0.0), control2_val: vf.add_const(0.0), phi_v: vf.add_const(0.0), Vpk: vf.add_const(np.sqrt(2.0)), Vdc_nom: vf.add_const(1.0), } # ================================================================= # CURRENT REFERENCE EQUATIONS # ================================================================= P_dc_cmd = P_ref + vf.add_const(2.0) * (v_dc - Vdc_ref) * regulate_vdc Q_cmd = Q_ref * regulate_q P_dc_cmd_pu = P_dc_cmd / sbase_eff Q_cmd_pu = Q_cmd / sbase_eff P_ac_cmd_pu = P_dc_cmd_pu + P_loss / sbase_eff i_d_cmd = c_two_over_three * Q_cmd_pu / (Vpk + eps) i_q_cmd = c_two_over_three * P_ac_cmd_pu / (Vpk + eps) i_A_cmd = i_d_cmd * sym.cos(theta) + i_q_cmd * sym.sin(theta) i_B_cmd = ((-c_half * i_d_cmd - c_sqrt3_over_two * i_q_cmd) * sym.cos(theta) + ((c_sqrt3_over_two * i_d_cmd) - c_half * i_q_cmd) * sym.sin(theta)) i_C_cmd = ((-c_half * i_d_cmd + c_sqrt3_over_two * i_q_cmd) * sym.cos(theta) + ((-c_sqrt3_over_two * i_d_cmd) - c_half * i_q_cmd) * sym.sin(theta)) Q_init_pu = Q_ref * regulate_q / sbase_eff P_dc_init_pu = P_ref / sbase_eff P_ac_init_pu = P_dc_init_pu + P_loss / sbase_eff i_d_init = c_two_over_three * Q_init_pu / (Vpk + eps) i_q_init = c_two_over_three * P_ac_init_pu / (Vpk + eps) i_A_init = i_d_init * sym.cos(phi_v) + i_q_init * sym.sin(phi_v) i_B_init = ((-c_half * i_d_init - c_sqrt3_over_two * i_q_init) * sym.cos(phi_v) + ((c_sqrt3_over_two * i_d_init) - c_half * i_q_init) * sym.sin(phi_v)) i_C_init = ((-c_half * i_d_init + c_sqrt3_over_two * i_q_init) * sym.cos(phi_v) + ((-c_sqrt3_over_two * i_d_init) - c_half * i_q_init) * sym.sin(phi_v)) state_eqs = [omega_base] algebraic_eqs = [ P_ref - p_ref_expr, Q_ref - q_ref_expr, Vdc_ref - vdc_ref_expr, i_A - i_A_cmd, i_B - i_B_cmd, i_C - i_C_cmd, v_dc * i_dc + P_dc_cmd_pu, P - (v_A * i_A + v_B * i_B + v_C * i_C), Q - (1 / np.sqrt(3.0)) * ((v_A - v_B) * i_C + (v_B - v_C) * i_A + (v_C - v_A) * i_B), ] # ================================================================= # ALGEBRAIC VARIABLES # ================================================================= algebraic_vars = [P_ref, Q_ref, Vdc_ref, i_A, i_B, i_C, i_dc, P, Q] # ================================================================= # INITIALIZATION EQUATIONS # ================================================================= init_eqs = { P_ref: p_ref_expr, Q_ref: q_ref_expr, Vdc_ref: vdc_ref_expr, i_A: i_A_init, i_B: i_B_init, i_C: i_C_init, i_dc: -P_dc_init_pu / (Vdc_ref + eps), P: P_ac_init_pu, Q: Q_init_pu, theta: phi_v, } # ================================================================= # BUILD THE BLOCK # ================================================================= converter_block = Block( state_eqs=state_eqs, state_vars=[theta], diff_vars=[d_theta], algebraic_eqs=algebraic_eqs, algebraic_vars=algebraic_vars, event_dict=event_dict, init_eqs=init_eqs, diff_init_eqs={d_theta: omega_base}, in_vars=inputs, out_vars=[i_A, i_B, i_C, i_dc], ) converter_block.api_obj_mapping = { ParamPowerFlowReferenceType.Sbase: sbase, ParamPowerFlowReferenceType.converter_loss_power_0: P_loss, # ParamPowerFlowReferenceType.P0: P0, 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, } # ================================================================= # EXTERNAL MAPPING # ================================================================= # This mapping tells EmtProblemDae how to connect the converter # to the buses based on the device configuration converter_block.external_mapping = { VarPowerFlowReferenceType.v_A: v_A, VarPowerFlowReferenceType.v_B: v_B, VarPowerFlowReferenceType.v_C: v_C, VarPowerFlowReferenceType.Vdc: v_dc, VarPowerFlowReferenceType.i_A: i_A, VarPowerFlowReferenceType.i_B: i_B, VarPowerFlowReferenceType.i_C: i_C, # For KCL at DC bus (bus_from) VarPowerFlowReferenceType.Idc: i_dc, VarPowerFlowReferenceType.P: P, VarPowerFlowReferenceType.Q: Q, VarPowerFlowReferenceType.phi_v: phi_v, VarPowerFlowReferenceType.Vpk: Vpk, } converter_block.name = name templ.block = converter_block return templ
############################################################################ def _build_pseudo_emt_converter_vsc_block( vf: VarFactory, name: str, ) -> Block: """VSC electrical/DC block with converter parameters and power/loss equations.""" v_d = vf.add_var(name=f"v_d_in_{name}") v_q = vf.add_var(name=f"v_q_in_{name}") v_0 = vf.add_var(name=f"v_0_in_{name}") i_d = vf.add_var(name=f"i_d_in_{name}") i_q = vf.add_var(name=f"i_q_in_{name}") i_0 = vf.add_var(name=f"i_0_in_{name}") v_dc_bus = vf.add_var(name=f"v_dc_bus_in_{name}") v_dc = vf.add_var(name=f"v_dc_{name}") d_v_dc = vf.add_diff_var(name=f"d_v_dc_{name}", base_var=v_dc) i_dc = vf.add_var(name=f"i_dc_{name}", reference=VarPowerFlowReferenceType.Idc) P = vf.add_var(name=f"P_{name}", reference=VarPowerFlowReferenceType.P) Q = vf.add_var(name=f"Q_{name}", reference=VarPowerFlowReferenceType.Q) i_mag = vf.add_var(name=f"i_mag_{name}") P_loss = vf.add_var(name=f"P_loss_{name}") i_dc_conv = vf.add_var(name=f"i_dc_conv_{name}") sbase = vf.add_var(name=f"sbase_{name}") P_ref = vf.add_var(name=f"P_ref_{name}") Q_ref = vf.add_var(name=f"Q_ref_{name}") Vdc_ref = vf.add_var(name=f"Vdc_ref_{name}") P0_sched = vf.add_var(name=f"P0_{name}") control1 = vf.add_var(name=f"control1_{name}") control2 = vf.add_var(name=f"control2_{name}") control1_val = vf.add_var(name=f"control1_val_{name}") control2_val = vf.add_var(name=f"control2_val_{name}") omega_base = vf.add_var(name=f"omega_base_{name}") phi_v = vf.add_var(name=f"phi_v_{name}") Vpk = vf.add_var(name=f"Vpk_{name}") R_eq = vf.add_var(name=f"R_eq_{name}") L_eq = vf.add_var(name=f"L_eq_{name}") C_dc = vf.add_var(name=f"C_dc_{name}") R_dc = vf.add_var(name=f"R_dc_{name}") R_dc_term = vf.add_var(name=f"R_dc_term_{name}") pll_kp = vf.add_var(name=f"pll_kp_{name}") pll_ki = vf.add_var(name=f"pll_ki_{name}") i_kp = vf.add_var(name=f"i_kp_{name}") i_ki = vf.add_var(name=f"i_ki_{name}") vdc_kp = vf.add_var(name=f"vdc_kp_{name}") vdc_ki = vf.add_var(name=f"vdc_ki_{name}") q_kp = vf.add_var(name=f"q_kp_{name}") q_ki = vf.add_var(name=f"q_ki_{name}") i_max = vf.add_var(name=f"i_max_{name}") m_max = vf.add_var(name=f"m_max_{name}") P_loss0 = vf.add_var(name=f"P_loss0_{name}") P_loss_i1 = vf.add_var(name=f"P_loss_i1_{name}") P_loss_i2 = vf.add_var(name=f"P_loss_i2_{name}") tau_meas = vf.add_var(name=f"tau_meas_{name}") aw_gain = vf.add_var(name=f"aw_gain_{name}") vdc_floor = vf.add_var(name=f"vdc_floor_{name}") Vdc_nom = vf.add_var(name=f"Vdc_nom_{name}") regulate_vdc_mode = vf.add_var(name=f"regulate_vdc_mode_{name}") regulate_q_mode = vf.add_var(name=f"regulate_q_mode_{name}") regulate_active_mode = vf.add_var(name=f"regulate_active_mode_{name}") p_ref_expr, q_ref_expr, vdc_ref_expr, regulate_vdc, regulate_q, regulate_active = _resolve_converter_control_reference_exprs( control1=control1, control2=control2, control1_val=control1_val, control2_val=control2_val, p0=P0_sched, vdc_nom=Vdc_nom, ) eps = vf.add_const(1e-10) c0 = vf.add_const(0.0) c1 = vf.add_const(1.0) c3 = vf.add_const(3.0) c32 = vf.add_const(1.5) sbase_eff = sym.max(sbase, eps) R_dc_eff = sym.max(R_dc, eps) R_dc_term_eff = sym.max(R_dc_term, eps) C_dc_eff = sym.max(C_dc, eps) P_loss0_pu = P_loss0 / sbase_eff P_loss_i1_pu = P_loss_i1 / sbase_eff P_loss_i2_pu = P_loss_i2 / sbase_eff i_leak = v_dc / R_dc_eff v_dc_eff = sym.max(v_dc, vdc_floor) i_d0 = (vf.add_const(2.0 / 3.0) * ((P_ref / sbase_eff) + (P_loss0 / sbase_eff))) / (Vpk + eps) i_q0 = (vf.add_const(2.0 / 3.0) * (Q_ref / sbase_eff)) / (Vpk + eps) i_mag0 = sym.sqrt(i_d0 * i_d0 + i_q0 * i_q0 + eps) P0 = c32 * Vpk * i_d0 Q0 = c32 * Vpk * i_q0 P_loss0_expr = P_loss0_pu + P_loss_i1_pu * i_mag0 + P_loss_i2_pu * i_mag0 * i_mag0 # DC-side power balance uses the AC transferred power plus converter losses. i_dc_conv0 = -(P0 + P_loss0_expr) / (Vdc_ref + eps) i_dc0 = (i_dc_conv0 + v_dc_bus / R_dc_eff) / (c1 + R_dc_term_eff / R_dc_eff) v_dc0 = v_dc_bus - R_dc_term * i_dc0 block = Block( # DC-side physical model and converter power/loss balance. state_eqs=[ # DC-link capacitor dynamics. (i_dc - i_dc_conv - i_leak) / C_dc_eff, ], state_vars=[v_dc], diff_vars=[d_v_dc], algebraic_eqs=[ P_ref - p_ref_expr, Q_ref - q_ref_expr, Vdc_ref - vdc_ref_expr, # Instantaneous active power in dq0 coordinates. P - (c32 * (v_d * i_d + v_q * i_q) + c3 * v_0 * i_0), # Instantaneous reactive power in dq coordinates. Q - c32 * (v_d * i_q - v_q * i_d), # Converter current magnitude used by the loss model. i_mag - sym.sqrt(i_d * i_d + i_q * i_q + c3 * i_0 * i_0 + eps), # Current-dependent converter loss model. P_loss - (P_loss0_pu + P_loss_i1_pu * i_mag + P_loss_i2_pu * i_mag * i_mag), # AC-to-DC bridge current implied by power transfer and losses. i_dc_conv + (P + P_loss) / v_dc_eff, # Resistive coupling between the DC bus and the internal capacitor node. i_dc - (v_dc_bus - v_dc) / R_dc_term_eff, # Expose mode-selection flags so outer loops can gate themselves. regulate_vdc_mode - regulate_vdc, regulate_q_mode - regulate_q, regulate_active_mode - regulate_active, ], algebraic_vars=[P_ref, Q_ref, Vdc_ref, i_dc, P, Q, i_mag, P_loss, i_dc_conv, regulate_vdc_mode, regulate_q_mode, regulate_active_mode], event_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)), Vdc_nom: vf.add_const(1.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={ 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, regulate_vdc_mode: regulate_vdc, regulate_q_mode: regulate_q, regulate_active_mode: regulate_active, }, diff_init_eqs={d_v_dc: c0}, in_vars=[v_d, v_q, v_0, i_d, i_q, i_0, v_dc_bus], out_vars=[ 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, Vdc_nom, i_dc_conv, regulate_vdc_mode, regulate_q_mode, regulate_active_mode, ], name=f"{name}_vsc", ) block.api_obj_mapping = { 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_pll_block(vf: VarFactory, name: str) -> Block: """SRF-PLL aligned with the q-axis voltage error convention used by the converter.""" v_q = vf.add_var(name=f"v_q_pll_in_{name}") omega_base = vf.add_var(name=f"omega_base_pll_in_{name}") pll_kp = vf.add_var(name=f"pll_kp_in_{name}") pll_ki = vf.add_var(name=f"pll_ki_in_{name}") phi_v = vf.add_var(name=f"phi_v_pll_in_{name}") theta_pll = vf.add_var(name=f"theta_pll_{name}") xi_pll = vf.add_var(name=f"xi_pll_{name}") omega_pll = vf.add_var(name=f"omega_pll_{name}") d_theta_pll = vf.add_diff_var(name=f"d_theta_pll_{name}", base_var=theta_pll) d_xi_pll = vf.add_diff_var(name=f"d_xi_pll_{name}", base_var=xi_pll) c0 = vf.add_const(0.0) return Block( state_eqs=[ # PLL angle dynamics. omega_pll, # PLL integrator dynamics driven by q-axis voltage. -pll_ki * v_q, ], state_vars=[theta_pll, xi_pll], diff_vars=[d_theta_pll, d_xi_pll], algebraic_eqs=[ # PLL frequency output from proportional and integral action. omega_pll - (omega_base - pll_kp * v_q + xi_pll), ], algebraic_vars=[omega_pll], init_eqs={theta_pll: phi_v, xi_pll: c0, omega_pll: omega_base}, diff_init_eqs={d_theta_pll: omega_base, d_xi_pll: c0}, in_vars=[v_q, omega_base, pll_kp, pll_ki, phi_v], out_vars=[theta_pll, omega_pll], name=f"{name}_pll", ) def _build_pseudo_emt_converter_outer_loop_block(vf: VarFactory, name: str) -> Block: """Outer control hierarchy: measurements, DC-voltage loop, Q loop, and current limits.""" v_d = vf.add_var(name=f"v_d_outer_in_{name}") v_q = vf.add_var(name=f"v_q_outer_in_{name}") v_0 = vf.add_var(name=f"v_0_outer_in_{name}") i_d = vf.add_var(name=f"i_d_outer_in_{name}") i_q = vf.add_var(name=f"i_q_outer_in_{name}") i_0 = vf.add_var(name=f"i_0_outer_in_{name}") v_dc = vf.add_var(name=f"v_dc_outer_in_{name}") P = vf.add_var(name=f"P_outer_in_{name}") Q = vf.add_var(name=f"Q_outer_in_{name}") sbase = vf.add_var(name=f"sbase_outer_in_{name}") P_ref = vf.add_var(name=f"P_ref_outer_in_{name}") Q_ref = vf.add_var(name=f"Q_ref_outer_in_{name}") Vdc_ref = vf.add_var(name=f"Vdc_ref_outer_in_{name}") Vpk = vf.add_var(name=f"Vpk_outer_in_{name}") P_loss0 = vf.add_var(name=f"P_loss0_outer_in_{name}") vdc_kp = vf.add_var(name=f"vdc_kp_outer_in_{name}") vdc_ki = vf.add_var(name=f"vdc_ki_outer_in_{name}") q_kp = vf.add_var(name=f"q_kp_outer_in_{name}") q_ki = vf.add_var(name=f"q_ki_outer_in_{name}") i_max = vf.add_var(name=f"i_max_outer_in_{name}") tau_meas = vf.add_var(name=f"tau_meas_outer_in_{name}") aw_gain = vf.add_var(name=f"aw_gain_outer_in_{name}") regulate_vdc_mode = vf.add_var(name=f"regulate_vdc_mode_outer_in_{name}") regulate_q_mode = vf.add_var(name=f"regulate_q_mode_outer_in_{name}") regulate_active_mode = vf.add_var(name=f"regulate_active_mode_outer_in_{name}") xi_vdc = vf.add_var(name=f"xi_vdc_{name}") xi_q = vf.add_var(name=f"xi_q_{name}") P_f = vf.add_var(name=f"P_f_{name}") Q_f = vf.add_var(name=f"Q_f_{name}") d_xi_vdc = vf.add_diff_var(name=f"d_xi_vdc_{name}", base_var=xi_vdc) d_xi_q = vf.add_diff_var(name=f"d_xi_q_{name}", base_var=xi_q) d_P_f = vf.add_diff_var(name=f"d_P_f_{name}", base_var=P_f) d_Q_f = vf.add_diff_var(name=f"d_Q_f_{name}", base_var=Q_f) v_mag = vf.add_var(name=f"v_mag_{name}") i_d_ff = vf.add_var(name=f"i_d_ff_{name}") i_q_ff = vf.add_var(name=f"i_q_ff_{name}") i_0_ref_u = vf.add_var(name=f"i_0_ref_u_{name}") i_d_ref_u = vf.add_var(name=f"i_d_ref_u_{name}") i_q_ref_u = vf.add_var(name=f"i_q_ref_u_{name}") i_0_ref = vf.add_var(name=f"i_0_ref_{name}") i_d_ref = vf.add_var(name=f"i_d_ref_{name}") i_q_ref = vf.add_var(name=f"i_q_ref_{name}") eps = vf.add_const(1e-10) c0 = vf.add_const(0.0) c23 = vf.add_const(2.0 / 3.0) c3 = vf.add_const(3.0) c32 = vf.add_const(1.5) sbase_eff = sym.max(sbase, eps) tau_meas_eff = sym.max(tau_meas, eps) P_ref_pu = P_ref / sbase_eff Q_ref_pu = Q_ref / sbase_eff P_ac_ff_pu = P_ref_pu + P_loss0 / sbase_eff active_control_error = regulate_vdc_mode * (Vdc_ref - v_dc) + regulate_active_mode * (P_ref_pu - P_f) i_d0 = c23 * P_ac_ff_pu / (Vpk + eps) q_ref_enabled_pu = Q_ref_pu * regulate_q_mode i_q0 = c23 * q_ref_enabled_pu / (Vpk + eps) Q0 = c32 * Vpk * i_q0 i_d_cap = sym.hard_sat(i_d_ref_u, -i_max, i_max) i_q_cap = sym.sqrt(sym.max(i_max * i_max - i_d_ref * i_d_ref, eps)) i_0_cap = 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=[ # Shared active-axis outer-loop integrator. In `Vm_dc` mode it # regulates the DC-link voltage. In `Pac/Pdc` mode it regulates the # active-power transfer around the scheduled feedforward term. (regulate_vdc_mode + regulate_active_mode) * vdc_ki * (active_control_error + aw_gain * (i_d_ref - i_d_ref_u)), # Reactive-power outer-loop integrator with anti-windup feedback. regulate_q_mode * q_ki * ((q_ref_enabled_pu - Q_f) + aw_gain * (i_q_ref - i_q_ref_u)), # Active-power measurement filter. (P - P_f) / tau_meas_eff, # Reactive-power measurement filter. (Q - Q_f) / tau_meas_eff, ], state_vars=[xi_vdc, xi_q, P_f, Q_f], diff_vars=[d_xi_vdc, d_xi_q, d_P_f, d_Q_f], algebraic_eqs=[ # AC voltage magnitude used by the feedforward terms. v_mag - sym.sqrt(v_d * v_d + v_q * v_q + eps), # Active-current feedforward from scheduled active power and base losses. i_d_ff - c23 * P_ac_ff_pu / (v_mag + eps), # Reactive-current feedforward from scheduled reactive power. i_q_ff - c23 * q_ref_enabled_pu / (v_mag + eps), # Zero-sequence reference schedule for the balanced operating mode. i_0_ref_u, # Unsaturated d-axis current reference from the gated active loop. i_d_ref_u - (i_d_ff + (regulate_vdc_mode + regulate_active_mode) * (vdc_kp * active_control_error + xi_vdc)), # Priority-limited d-axis current reference. i_d_ref - i_d_cap, # Unsaturated q-axis current reference from reactive-power control. i_q_ref_u - (i_q_ff + regulate_q_mode * (q_kp * (q_ref_enabled_pu - Q_f) + xi_q)), # Residual-current-limited q-axis current reference. i_q_ref - sym.hard_sat(i_q_ref_u, -i_q_cap, i_q_cap), # Residual-current-limited zero-sequence current reference. i_0_ref - sym.hard_sat(i_0_ref_u, -i_0_cap, i_0_cap), ], algebraic_vars=[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={ xi_vdc: (regulate_vdc_mode + regulate_active_mode) * (i_d0 - (c23 * P_ac_ff_pu / (Vpk + eps)) - vdc_kp * active_control_error), xi_q: regulate_q_mode * (i_q0 - (c23 * q_ref_enabled_pu / (Vpk + eps)) - q_kp * (q_ref_enabled_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_enabled_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={d_xi_vdc: c0, d_xi_q: c0, d_P_f: c0, d_Q_f: c0}, in_vars=[ 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, regulate_vdc_mode, regulate_q_mode, regulate_active_mode, ], out_vars=[P_f, Q_f, i_0_ref, i_d_ref, i_q_ref], name=f"{name}_outer_loop", ) def _build_pseudo_emt_converter_inner_loop_block(vf: VarFactory, name: str) -> Block: """Inner dq0 current controller and modulation-limited voltage command generation.""" v_d = vf.add_var(name=f"v_d_inner_in_{name}") v_q = vf.add_var(name=f"v_q_inner_in_{name}") v_0 = vf.add_var(name=f"v_0_inner_in_{name}") i_d = vf.add_var(name=f"i_d_inner_in_{name}") i_q = vf.add_var(name=f"i_q_inner_in_{name}") i_0 = vf.add_var(name=f"i_0_inner_in_{name}") omega_pll = vf.add_var(name=f"omega_pll_inner_in_{name}") omega_base = vf.add_var(name=f"omega_base_inner_in_{name}") R_eq = vf.add_var(name=f"R_eq_inner_in_{name}") L_eq = vf.add_var(name=f"L_eq_inner_in_{name}") i_0_ref = vf.add_var(name=f"i_0_ref_inner_in_{name}") i_d_ref = vf.add_var(name=f"i_d_ref_inner_in_{name}") i_q_ref = vf.add_var(name=f"i_q_ref_inner_in_{name}") i_kp = vf.add_var(name=f"i_kp_inner_in_{name}") i_ki = vf.add_var(name=f"i_ki_inner_in_{name}") aw_gain = vf.add_var(name=f"aw_gain_inner_in_{name}") m_max = vf.add_var(name=f"m_max_inner_in_{name}") Vdc_ref = vf.add_var(name=f"Vdc_ref_inner_in_{name}") v_dc = vf.add_var(name=f"v_dc_inner_in_{name}") vdc_floor = vf.add_var(name=f"vdc_floor_inner_in_{name}") sbase = vf.add_var(name=f"sbase_inner_in_{name}") P_ref = vf.add_var(name=f"P_ref_inner_in_{name}") Q_ref = vf.add_var(name=f"Q_ref_inner_in_{name}") P_loss0 = vf.add_var(name=f"P_loss0_inner_in_{name}") Vpk = vf.add_var(name=f"Vpk_inner_in_{name}") xi_id = vf.add_var(name=f"xi_id_{name}") xi_iq = vf.add_var(name=f"xi_iq_{name}") xi_i0 = vf.add_var(name=f"xi_i0_{name}") d_xi_id = vf.add_diff_var(name=f"d_xi_id_{name}", base_var=xi_id) d_xi_iq = vf.add_diff_var(name=f"d_xi_iq_{name}", base_var=xi_iq) d_xi_i0 = vf.add_diff_var(name=f"d_xi_i0_{name}", base_var=xi_i0) v_pi_d_u = vf.add_var(name=f"v_pi_d_u_{name}") v_pi_q_u = vf.add_var(name=f"v_pi_q_u_{name}") v_pi_0_u = vf.add_var(name=f"v_pi_0_u_{name}") v_cmd_d_u = vf.add_var(name=f"v_cmd_d_u_{name}") v_cmd_q_u = vf.add_var(name=f"v_cmd_q_u_{name}") v_cmd_0_u = vf.add_var(name=f"v_cmd_0_u_{name}") k_v_conv = vf.add_var(name=f"k_v_conv_{name}") v_lim = vf.add_var(name=f"v_lim_{name}") v_cmd_d = vf.add_var(name=f"v_cmd_d_{name}") v_cmd_q = vf.add_var(name=f"v_cmd_q_{name}") v_cmd_0 = vf.add_var(name=f"v_cmd_0_{name}") eps = vf.add_const(1e-10) c0 = vf.add_const(0.0) c23 = vf.add_const(2.0 / 3.0) sbase_eff = sym.max(sbase, eps) vdc_floor_eff = sym.max(vdc_floor, eps) v_dc_eff = sym.max(v_dc, vdc_floor_eff) omega_ratio = omega_pll / (omega_base + eps) i_d0 = c23 * ((P_ref / sbase_eff) + (P_loss0 / sbase_eff)) / (Vpk + eps) i_q0 = c23 * (Q_ref / sbase_eff) / (Vpk + eps) # Match the switched-converter branch-drop convention used by the EMT seed # helper so the pseudo model starts from the same commanded dq voltage. v_cmd_d0 = Vpk - R_eq * i_d0 + L_eq * i_q0 v_cmd_q0 = -R_eq * i_q0 - L_eq * i_d0 v_d_cap = sym.hard_sat(v_cmd_d_u, -v_lim, v_lim) v_q_cap = sym.sqrt(sym.max(v_lim * v_lim - v_cmd_d * v_cmd_d, eps)) v_0_cap = sym.sqrt(sym.max((v_lim * v_lim - v_cmd_d * v_cmd_d - v_cmd_q * v_cmd_q) / vf.add_const(3.0), eps / vf.add_const(3.0))) return Block( state_eqs=[ # d-axis current-controller integrator with anti-windup feedback. i_ki * ((i_d_ref - i_d) + aw_gain * (v_cmd_d - v_cmd_d_u)), # q-axis current-controller integrator with anti-windup feedback. i_ki * ((i_q_ref - i_q) + aw_gain * (v_cmd_q - v_cmd_q_u)), # 0-axis current-controller integrator with anti-windup feedback. i_ki * ((i_0_ref - i_0) + aw_gain * (v_cmd_0 - v_cmd_0_u)), ], state_vars=[xi_id, xi_iq, xi_i0], diff_vars=[d_xi_id, d_xi_iq, d_xi_i0], algebraic_eqs=[ # Unsaturated d-axis PI output. v_pi_d_u - (i_kp * (i_d_ref - i_d) + xi_id), # Unsaturated q-axis PI output. v_pi_q_u - (i_kp * (i_q_ref - i_q) + xi_iq), # Unsaturated 0-axis PI output. v_pi_0_u - (i_kp * (i_0_ref - i_0) + xi_i0), # Unsaturated d-axis converter voltage command with decoupling. v_cmd_d_u - (v_d - R_eq * i_d + omega_ratio * L_eq * i_q - v_pi_d_u), # Unsaturated q-axis converter voltage command with decoupling. v_cmd_q_u - (v_q - R_eq * i_q - omega_ratio * L_eq * i_d - v_pi_q_u), # Unsaturated 0-axis converter voltage command. v_cmd_0_u - (v_0 - R_eq * i_0 - v_pi_0_u), # Modulation-limit gain based on the nominal operating-point command. k_v_conv - (sym.sqrt(v_cmd_d0 * v_cmd_d0 + v_cmd_q0 * v_cmd_q0 + eps) / (m_max * (Vdc_ref + eps))), # Available converter voltage magnitude from modulation and DC voltage. v_lim - (k_v_conv * m_max * v_dc_eff), # Saturated d-axis converter voltage command. v_cmd_d - v_d_cap, # Saturated q-axis converter voltage command. v_cmd_q - sym.hard_sat(v_cmd_q_u, -v_q_cap, v_q_cap), # Saturated 0-axis converter voltage command. v_cmd_0 - sym.hard_sat(v_cmd_0_u, -v_0_cap, v_0_cap), ], algebraic_vars=[v_pi_d_u, v_pi_q_u, v_pi_0_u, v_cmd_d_u, v_cmd_q_u, v_cmd_0_u, k_v_conv, v_lim, v_cmd_d, v_cmd_q, v_cmd_0], init_eqs={ xi_id: c0, xi_iq: c0, xi_i0: c0, v_pi_d_u: c0, v_pi_q_u: c0, v_pi_0_u: c0, v_cmd_d_u: v_cmd_d0, v_cmd_q_u: v_cmd_q0, v_cmd_0_u: c0, k_v_conv: sym.sqrt(v_cmd_d0 * v_cmd_d0 + v_cmd_q0 * v_cmd_q0 + eps) / (m_max * (Vdc_ref + eps)), v_lim: sym.sqrt(v_cmd_d0 * v_cmd_d0 + v_cmd_q0 * v_cmd_q0 + eps) * v_dc / (Vdc_ref + eps), v_cmd_d: v_cmd_d0, v_cmd_q: v_cmd_q0, v_cmd_0: c0, }, diff_init_eqs={d_xi_id: c0, d_xi_iq: c0, d_xi_i0: c0}, in_vars=[ v_d, v_q, v_0, i_d, i_q, i_0, omega_pll, omega_base, R_eq, L_eq, i_0_ref, i_d_ref, i_q_ref, i_kp, i_ki, aw_gain, m_max, Vdc_ref, v_dc, vdc_floor, sbase, P_ref, Q_ref, P_loss0, Vpk, ], out_vars=[v_cmd_d, v_cmd_q, v_cmd_0, k_v_conv], name=f"{name}_inner_loop", ) def _build_pseudo_emt_converter_transformer_block(vf: VarFactory, name: str) -> Block: """AC-side interface branch with dq0 current dynamics and abc coupling.""" v_A = vf.add_var(name=f"v_A_in_tr_{name}") v_B = vf.add_var(name=f"v_B_in_tr_{name}") v_C = vf.add_var(name=f"v_C_in_tr_{name}") theta_pll = vf.add_var(name=f"theta_pll_in_tr_{name}") omega_pll = vf.add_var(name=f"omega_pll_in_tr_{name}") omega_base = vf.add_var(name=f"omega_base_in_tr_{name}") R_eq = vf.add_var(name=f"R_eq_in_tr_{name}") L_eq = vf.add_var(name=f"L_eq_in_tr_{name}") v_cmd_d = vf.add_var(name=f"v_cmd_d_in_tr_{name}") v_cmd_q = vf.add_var(name=f"v_cmd_q_in_tr_{name}") v_cmd_0 = vf.add_var(name=f"v_cmd_0_in_tr_{name}") sbase = vf.add_var(name=f"sbase_in_tr_{name}") P_ref = vf.add_var(name=f"P_ref_in_tr_{name}") Q_ref = vf.add_var(name=f"Q_ref_in_tr_{name}") P_loss0 = vf.add_var(name=f"P_loss0_in_tr_{name}") Vpk = vf.add_var(name=f"Vpk_in_tr_{name}") i_d = vf.add_var(name=f"i_d_{name}") i_q = vf.add_var(name=f"i_q_{name}") i_0 = vf.add_var(name=f"i_0_{name}") d_i_d = vf.add_diff_var(name=f"d_i_d_{name}", base_var=i_d) d_i_q = vf.add_diff_var(name=f"d_i_q_{name}", base_var=i_q) d_i_0 = vf.add_diff_var(name=f"d_i_0_{name}", base_var=i_0) i_A = vf.add_var(name=f"i_A_{name}", reference=VarPowerFlowReferenceType.i_A) i_B = vf.add_var(name=f"i_B_{name}", reference=VarPowerFlowReferenceType.i_B) i_C = vf.add_var(name=f"i_C_{name}", reference=VarPowerFlowReferenceType.i_C) v_d = vf.add_var(name=f"v_d_{name}") v_q = vf.add_var(name=f"v_q_{name}") v_0 = vf.add_var(name=f"v_0_{name}") eps = vf.add_const(1e-10) c0 = vf.add_const(0.0) c13 = vf.add_const(1.0 / 3.0) c23 = vf.add_const(2.0 / 3.0) shift = vf.add_const(2.0 * np.pi / 3.0) theta_b = theta_pll - shift theta_c = theta_pll + shift omega_ratio = omega_pll / (omega_base + eps) i_d0 = c23 * ((P_ref + P_loss0) / sbase) / (Vpk + eps) i_q0 = c23 * (Q_ref / sbase) / (Vpk + eps) i_0_decay = vf.add_const(1.0) return Block( state_eqs=[ # Match the switched-converter RL branch sign convention so the pseudo # interface current dynamics use the same physical voltage drop model. (omega_base * (v_d - v_cmd_d - R_eq * i_d + omega_ratio * L_eq * i_q)) / L_eq, (omega_base * (v_q - v_cmd_q - R_eq * i_q - omega_ratio * L_eq * i_d)) / L_eq, # Balanced three-wire operation has no neutral return path, so the # pseudo-EMT converter must not build up a zero-sequence current. # Keep the 0-axis state explicitly damped to zero. -(omega_base * i_0_decay * i_0) / L_eq, ], state_vars=[i_d, i_q, i_0], diff_vars=[d_i_d, d_i_q, d_i_0], algebraic_eqs=[ # abc-to-d transformation in the PLL reference frame. v_d - c23 * (sym.sin(theta_pll) * v_A + sym.sin(theta_b) * v_B + sym.sin(theta_c) * v_C), # abc-to-q transformation in the PLL reference frame. v_q + c23 * (sym.cos(theta_pll) * v_A + sym.cos(theta_b) * v_B + sym.cos(theta_c) * v_C), # abc-to-0 transformation for zero-sequence voltage. v_0 - c13 * (v_A + v_B + v_C), # dq0-to-abc reconstruction of phase-A current injection. i_A - (i_d * sym.sin(theta_pll) - i_q * sym.cos(theta_pll)), # dq0-to-abc reconstruction of phase-B current injection. i_B - (i_d * sym.sin(theta_b) - i_q * sym.cos(theta_b)), # dq0-to-abc reconstruction of phase-C current injection. i_C - (i_d * sym.sin(theta_c) - i_q * sym.cos(theta_c)), ], algebraic_vars=[i_A, i_B, i_C, v_d, v_q, v_0], init_eqs={i_d: i_d0, i_q: i_q0, i_0: c0, v_d: Vpk, v_q: c0, v_0: c0}, diff_init_eqs={d_i_d: c0, d_i_q: c0, d_i_0: c0}, in_vars=[v_A, v_B, v_C, theta_pll, omega_pll, omega_base, R_eq, L_eq, v_cmd_d, v_cmd_q, v_cmd_0, sbase, P_ref, Q_ref, P_loss0, Vpk], out_vars=[i_A, i_B, i_C, i_d, i_q, i_0, v_d, v_q, v_0], name=f"{name}_transformer", )
[docs] def get_full_pseudo_emt_converter( vf: VarFactory, name: str = "pseudo_converter_emt", ) -> EmtModelTemplate: """Assemble the pseudo-EMT converter from meaningful electrical and control sub-blocks.""" templ = EmtModelTemplate() templ.tpe = DeviceType.VscDevice templ.name = name templ.block.name = name v_A = vf.add_var(name=f"v_A_{name}", reference=VarPowerFlowReferenceType.v_A) v_B = vf.add_var(name=f"v_B_{name}", reference=VarPowerFlowReferenceType.v_B) v_C = vf.add_var(name=f"v_C_{name}", reference=VarPowerFlowReferenceType.v_C) # AC/DC physical converter block: parameters, DC link, power, losses, and DC terminal relation. vsc_block = _build_pseudo_emt_converter_vsc_block( vf=vf, name=name, ) # Reuse the physical VSC block DC-bus input directly at the template root. # Creating an extra wrapper variable here leaves one free symbolic node in the # unified equations, which later confuses the structural compiled backend. v_dc_bus = vsc_block.in_vars[6] # Control hierarchy blocks. pll_block = _build_pseudo_emt_converter_pll_block(vf=vf, name=name) outer_loop_block = _build_pseudo_emt_converter_outer_loop_block(vf=vf, name=name) inner_loop_block = _build_pseudo_emt_converter_inner_loop_block(vf=vf, name=name) # AC-side transformer/interface block with dq0 current dynamics. transformer_block = _build_pseudo_emt_converter_transformer_block(vf=vf, name=name) # Direct network wiring: the terminal pass-through block is intentionally removed. vf.add_connections(transformer_block.in_vars[0:3], [v_A, v_B, v_C]) vsc_block.in_vars[6] = v_dc_bus # Transformer provides dq0 measurements and interface currents to the VSC/control hierarchy. vf.add_connections(vsc_block.in_vars[0:3], transformer_block.out_vars[6:9]) vf.add_connections(vsc_block.in_vars[3:6], transformer_block.out_vars[3:6]) vf.add_connections([pll_block.in_vars[0]], [transformer_block.out_vars[7]]) vf.add_connections(outer_loop_block.in_vars[0:3], transformer_block.out_vars[6:9]) vf.add_connections(outer_loop_block.in_vars[3:6], transformer_block.out_vars[3:6]) vf.add_connections(inner_loop_block.in_vars[0:3], transformer_block.out_vars[6:9]) vf.add_connections(inner_loop_block.in_vars[3:6], transformer_block.out_vars[3:6]) # VSC block exports physical quantities and parameters used by the controls. vf.add_connections( pll_block.in_vars[1:5], [vsc_block.out_vars[8], vsc_block.out_vars[16], vsc_block.out_vars[17], vsc_block.out_vars[9]], ) vf.add_connections( [outer_loop_block.in_vars[6], outer_loop_block.in_vars[7], outer_loop_block.in_vars[8]], [vsc_block.out_vars[0], vsc_block.out_vars[2], vsc_block.out_vars[3]], ) vf.add_connections(outer_loop_block.in_vars[9:25], [ vsc_block.out_vars[4], vsc_block.out_vars[5], vsc_block.out_vars[6], vsc_block.out_vars[7], vsc_block.out_vars[10], vsc_block.out_vars[26], vsc_block.out_vars[20], vsc_block.out_vars[21], vsc_block.out_vars[22], vsc_block.out_vars[23], vsc_block.out_vars[24], vsc_block.out_vars[29], vsc_block.out_vars[30], vsc_block.out_vars[33], vsc_block.out_vars[34], vsc_block.out_vars[35], ]) vf.add_connections([inner_loop_block.in_vars[6], inner_loop_block.in_vars[7], inner_loop_block.in_vars[8], inner_loop_block.in_vars[9]], [ pll_block.out_vars[1], vsc_block.out_vars[8], vsc_block.out_vars[11], vsc_block.out_vars[12], ]) vf.add_connections(inner_loop_block.in_vars[10:13], outer_loop_block.out_vars[2:5]) vf.add_connections(inner_loop_block.in_vars[13:25], [ vsc_block.out_vars[18], vsc_block.out_vars[19], vsc_block.out_vars[30], vsc_block.out_vars[25], vsc_block.out_vars[7], vsc_block.out_vars[0], vsc_block.out_vars[31], vsc_block.out_vars[4], vsc_block.out_vars[5], vsc_block.out_vars[6], vsc_block.out_vars[26], vsc_block.out_vars[10], ]) # Controller outputs drive the AC interface dynamics. vf.add_connections([transformer_block.in_vars[3], transformer_block.in_vars[4]], pll_block.out_vars) vf.add_connections(transformer_block.in_vars[5:8], [vsc_block.out_vars[8], vsc_block.out_vars[11], vsc_block.out_vars[12]]) vf.add_connections(transformer_block.in_vars[8:11], inner_loop_block.out_vars) vf.add_connections(transformer_block.in_vars[11:16], [ vsc_block.out_vars[4], vsc_block.out_vars[5], vsc_block.out_vars[6], vsc_block.out_vars[26], vsc_block.out_vars[10], ]) templ.block.children.extend([vsc_block, pll_block, inner_loop_block, outer_loop_block, transformer_block]) templ.block.unify_blocks() templ.block.in_vars = [v_A, v_B, v_C, v_dc_bus] templ.block.out_vars = [ transformer_block.out_vars[0], transformer_block.out_vars[1], transformer_block.out_vars[2], vsc_block.out_vars[1], ] templ.block.external_mapping = { VarPowerFlowReferenceType.v_N: None, VarPowerFlowReferenceType.v_A: v_A, VarPowerFlowReferenceType.v_B: v_B, VarPowerFlowReferenceType.v_C: v_C, VarPowerFlowReferenceType.Vdc: v_dc_bus, VarPowerFlowReferenceType.i_N: None, VarPowerFlowReferenceType.i_A: transformer_block.out_vars[0], VarPowerFlowReferenceType.i_B: transformer_block.out_vars[1], VarPowerFlowReferenceType.i_C: transformer_block.out_vars[2], VarPowerFlowReferenceType.if_N: None, VarPowerFlowReferenceType.if_A: None, VarPowerFlowReferenceType.if_B: None, VarPowerFlowReferenceType.if_C: None, VarPowerFlowReferenceType.it_N: None, VarPowerFlowReferenceType.it_A: None, VarPowerFlowReferenceType.it_B: None, VarPowerFlowReferenceType.it_C: None, VarPowerFlowReferenceType.Sf_A: None, VarPowerFlowReferenceType.Sf_B: None, VarPowerFlowReferenceType.Sf_C: None, VarPowerFlowReferenceType.St_A: None, VarPowerFlowReferenceType.St_B: None, VarPowerFlowReferenceType.St_C: None, VarPowerFlowReferenceType.d_v_N_f: None, VarPowerFlowReferenceType.d_v_A_f: None, VarPowerFlowReferenceType.d_v_B_f: None, VarPowerFlowReferenceType.d_v_C_f: None, VarPowerFlowReferenceType.d_v_N_t: None, VarPowerFlowReferenceType.d_v_A_t: None, VarPowerFlowReferenceType.d_v_B_t: None, VarPowerFlowReferenceType.d_v_C_t: None, VarPowerFlowReferenceType.Idc: vsc_block.out_vars[1], VarPowerFlowReferenceType.P: vsc_block.out_vars[2], VarPowerFlowReferenceType.Q: vsc_block.out_vars[3], VarPowerFlowReferenceType.phi_v: vsc_block.out_vars[9], VarPowerFlowReferenceType.Vpk: vsc_block.out_vars[10], } templ.block.api_obj_mapping = dict(vsc_block.api_obj_mapping) return templ