Source code for VeraGridEngine.Templates.Emt.transformer_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

import uuid
from typing import Dict, List, Tuple

from VeraGridEngine.enumerations import (
    BlockType,
    DeviceType,
    ParamPowerFlowReferenceType,
    VarPowerFlowReferenceType,
    WindingType,
)
from VeraGridEngine.Devices.Dynamic.var_factory import VarFactory
from VeraGridEngine.Devices.Dynamic.emt_template import EmtModelTemplate
from VeraGridEngine.Templates.Emt.load_RLC_emt_template import get_grounding_link_emt_template
from VeraGridEngine.Utils.Symbolic.block import Block, Var, Expr


def _create_transformer_editor_connection_child(var: Var, is_input: bool, name: str) -> Block:
    """
    Build one visual-only connection child used by ``Edit Hierarchy``.

    :param var: Shared root-port variable.
    :param is_input: True for one input connector.
    :param name: Child block name.
    :return: Lightweight block exposing the shared port variable.
    """
    child_block: Block = Block(name=name)

    if is_input:
        child_block.out_vars.append(var)
    else:
        child_block.in_vars.append(var)

    return child_block


def _attach_transformer_editor_diagram(root_block: Block,
                                       input_vars: List[Var],
                                       output_vars: List[Var],
                                       grounding_pairs: List[Tuple[Var, Block]]) -> None:
    """
    Attach one persisted minimal editor diagram for an EMT transformer block.

    :param root_block: Transformer root block.
    :param input_vars: Public input variables.
    :param output_vars: Public output variables.
    :param grounding_pairs: Pairs ``(neutral_input_var, grounding_link_block)``.
    :return: None.
    """
    input_blocks_by_name: Dict[str, Block] = dict()
    input_index: int
    output_index: int
    grounding_index: int
    input_var: Var
    output_var: Var
    grounding_pair: Tuple[Var, Block]

    for input_index, input_var in enumerate(input_vars):
        input_block: Block = _create_transformer_editor_connection_child(
            var=input_var,
            is_input=True,
            name=input_var.name,
        )
        root_block.add(input_block)
        root_block.diagram.add_node(
            name=input_block.name,
            x=40.0,
            y=80.0 + 80.0 * float(input_index),
            tpe=BlockType.INPUT_CONN.name,
            device_uid=input_block.uid,
        )
        input_blocks_by_name[input_var.name] = input_block

    for output_index, output_var in enumerate(output_vars):
        output_block: Block = _create_transformer_editor_connection_child(
            var=output_var,
            is_input=False,
            name=output_var.name,
        )
        root_block.add(output_block)
        root_block.diagram.add_node(
            name=output_block.name,
            x=640.0,
            y=80.0 + 50.0 * float(output_index),
            tpe=BlockType.OUTPUT_CONN.name,
            device_uid=output_block.uid,
        )

    for grounding_index, grounding_pair in enumerate(grounding_pairs):
        neutral_input_var, grounding_link_block = grounding_pair
        root_block.diagram.add_node(
            name=grounding_link_block.name,
            x=340.0,
            y=180.0 + 140.0 * float(grounding_index),
            tpe=BlockType.GROUNDING_LINK_EMT.name,
            device_uid=grounding_link_block.uid,
        )
        neutral_input_block: Block | None = input_blocks_by_name.get(neutral_input_var.name, None)

        if neutral_input_block is not None:
            root_block.diagram.add_branch(
                connectionitem_uid=uuid.uuid4().int,
                device_uid_from=neutral_input_block.uid,
                device_uid_to=grounding_link_block.uid,
                port_number_from=0,
                port_number_to=0,
                color="#587291",
            )
        else:
            pass




[docs] def get_transformer_emt_template( vf: VarFactory, conn_f: WindingType | None = None, conn_t: WindingType | None = None, name: str = "transformer_emt_template", ) -> EmtModelTemplate: """ Build a linear two-winding transformer EMT template using coupled winding currents as state variables. This simplified version keeps only the series coupled-winding model and omits the primary shunt branch from the EMT equations. The total voltage ratio is exposed through api_obj_mapping and is used to initialize the primary winding current from the secondary-side port current in a way that is consistent with the external port convention. Assumptions: - `l1`, `l2`, and `m12` already represent the physical transformer model in the chosen winding coordinates. - branch currents are positive when leaving the connected bus. - `if_act` and `it_act` are externally initialized by the EMT framework from the power-flow solution through `external_mapping`. :param vf: EMT variable factory. :param conn_f: Optional from-side winding connection. :param conn_t: Optional to-side winding connection. :param name: Symbolic model name. :return: EMT transformer model template. """ templ: EmtModelTemplate = EmtModelTemplate() templ.tpe = DeviceType.Transformer2WDevice templ.name = name templ.block.name = name if conn_f in {WindingType.GroundedStar, WindingType.NeutralStar}: from_has_neutral_port: bool = True else: from_has_neutral_port = False if conn_t in {WindingType.GroundedStar, WindingType.NeutralStar}: to_has_neutral_port: bool = True else: to_has_neutral_port = False c_eps = vf.add_const(1e-12) # ------------------------------------------------------------------ # Static parameters injected from the API object # ------------------------------------------------------------------ r1: Var = vf.add_var(name=f"trafo_r1_{name}") r2: Var = vf.add_var(name=f"trafo_r2_{name}") l1: Var = vf.add_var(name=f"trafo_l1_{name}") l2: Var = vf.add_var(name=f"trafo_l2_{name}") m12: Var = vf.add_var(name=f"trafo_m_{name}") gm: Var = vf.add_var(name=f"trafo_gm_{name}") # kept for compatibility, unused here total_voltage_ratio: Var = vf.add_var(name=f"trafo_tap_ratio_{name}") templ.block.api_obj_mapping[ParamPowerFlowReferenceType.transformer_winding1_resistance_pu] = r1 templ.block.api_obj_mapping[ParamPowerFlowReferenceType.transformer_winding2_resistance_pu] = r2 templ.block.api_obj_mapping[ParamPowerFlowReferenceType.transformer_winding1_inductance_pu_s] = l1 templ.block.api_obj_mapping[ParamPowerFlowReferenceType.transformer_winding2_inductance_pu_s] = l2 templ.block.api_obj_mapping[ParamPowerFlowReferenceType.transformer_mutual_inductance_pu_s] = m12 templ.block.api_obj_mapping[ParamPowerFlowReferenceType.transformer_magnetizing_conductance_pu] = gm templ.block.api_obj_mapping[ParamPowerFlowReferenceType.transformer_total_voltage_ratio] = total_voltage_ratio # ------------------------------------------------------------------ # Terminal voltages # ------------------------------------------------------------------ vf_n: Var | None = None vt_n: Var | None = None from_ground_current: Var | None = None to_ground_current: Var | None = None from_grounding_link_block: Block | None = None to_grounding_link_block: Block | None = None if from_has_neutral_port: vf_n = vf.add_var(name=f"vf_N_{name}", reference=VarPowerFlowReferenceType.vf_N) else: pass if to_has_neutral_port: vt_n = vf.add_var(name=f"vt_N_{name}", reference=VarPowerFlowReferenceType.vt_N) else: pass if conn_f == WindingType.GroundedStar and vf_n is not None: from_grounding_link_template: EmtModelTemplate = get_grounding_link_emt_template( vf=vf, include_r=False, include_l=False, include_c=False, solid_connection=True, nested=True, name=name + "_from_grounding_link", ) vf.add_connections(from_grounding_link_template.block.in_vars, [vf_n]) from_ground_current = from_grounding_link_template.block.out_vars[0] from_grounding_link_block = from_grounding_link_template.block templ.block.add(from_grounding_link_block) else: pass if conn_t == WindingType.GroundedStar and vt_n is not None: to_grounding_link_template: EmtModelTemplate = get_grounding_link_emt_template( vf=vf, include_r=False, include_l=False, include_c=False, solid_connection=True, nested=True, name=name + "_to_grounding_link", ) vf.add_connections(to_grounding_link_template.block.in_vars, [vt_n]) to_ground_current = to_grounding_link_template.block.out_vars[0] to_grounding_link_block = to_grounding_link_template.block templ.block.add(to_grounding_link_block) else: pass vf_vars: list[Var] = [ vf.add_var(name=f"vf_A_{name}", reference=VarPowerFlowReferenceType.vf_A), vf.add_var(name=f"vf_B_{name}", reference=VarPowerFlowReferenceType.vf_B), vf.add_var(name=f"vf_C_{name}", reference=VarPowerFlowReferenceType.vf_C), ] vt_vars: list[Var] = [ vf.add_var(name=f"vt_A_{name}", reference=VarPowerFlowReferenceType.vt_A), vf.add_var(name=f"vt_B_{name}", reference=VarPowerFlowReferenceType.vt_B), vf.add_var(name=f"vt_C_{name}", reference=VarPowerFlowReferenceType.vt_C), ] # ------------------------------------------------------------------ # Internal winding-current states # ------------------------------------------------------------------ i_f: list[Var] = [ vf.add_var(name=f"i_f_A_{name}"), vf.add_var(name=f"i_f_B_{name}"), vf.add_var(name=f"i_f_C_{name}"), ] i_t: list[Var] = [ vf.add_var(name=f"i_t_A_{name}"), vf.add_var(name=f"i_t_B_{name}"), vf.add_var(name=f"i_t_C_{name}"), ] di_f: list[Var] = [ vf.add_diff_var(name=f"di_f_A_{name}", base_var=i_f[0]), vf.add_diff_var(name=f"di_f_B_{name}", base_var=i_f[1]), vf.add_diff_var(name=f"di_f_C_{name}", base_var=i_f[2]), ] di_t: list[Var] = [ vf.add_diff_var(name=f"di_t_A_{name}", base_var=i_t[0]), vf.add_diff_var(name=f"di_t_B_{name}", base_var=i_t[1]), vf.add_diff_var(name=f"di_t_C_{name}", base_var=i_t[2]), ] # ------------------------------------------------------------------ # External branch-port currents # ------------------------------------------------------------------ if_act: list[Var] = [ vf.add_var(name=f"if_{name}_A"), vf.add_var(name=f"if_{name}_B"), vf.add_var(name=f"if_{name}_C"), ] it_act: list[Var] = [ vf.add_var(name=f"it_{name}_A"), vf.add_var(name=f"it_{name}_B"), vf.add_var(name=f"it_{name}_C"), ] if_n_act: Var | None = vf.add_var(name=f"if_{name}_N") if from_has_neutral_port else None it_n_act: Var | None = vf.add_var(name=f"it_{name}_N") if to_has_neutral_port else None input_vars: list[Var] = list() if vf_n is not None: input_vars.append(vf_n) else: pass input_vars.extend(vf_vars) if vt_n is not None: input_vars.append(vt_n) else: pass input_vars.extend(vt_vars) algebraic_vars: list[Var] = list(if_act + it_act) if if_n_act is not None: algebraic_vars.append(if_n_act) else: pass if it_n_act is not None: algebraic_vars.append(it_n_act) else: pass templ.block.in_vars = input_vars templ.block.state_vars = i_f + i_t templ.block.diff_vars = di_f + di_t templ.block.algebraic_vars = algebraic_vars v_f_term: list[Expr] = list() v_t_term: list[Expr] = list() idx: int for idx in range(3): if vf_n is not None: v_f_term.append(vf_vars[idx] - vf_n) else: v_f_term.append(vf_vars[idx]) if vt_n is not None: v_t_term.append(vt_vars[idx] - vt_n) else: v_t_term.append(vt_vars[idx]) # ------------------------------------------------------------------ # Coupled winding dynamics # # [l1 m12] [di_f/dt] = [vf - r1*i_f] # [m12 l2 ] [di_t/dt] [vt - r2*i_t] # # Inverting the inductance matrix gives negative signs in the cross terms. # ------------------------------------------------------------------ state_eqs_f: list[Expr] = [] state_eqs_t: list[Expr] = [] det_l: Expr = l1 * l2 - m12 * m12 for idx in range(3): primary_rhs: Expr = v_f_term[idx] - r1 * i_f[idx] secondary_rhs: Expr = v_t_term[idx] - r2 * i_t[idx] state_eqs_f.append((l2 * primary_rhs - m12 * secondary_rhs) / (det_l + c_eps)) state_eqs_t.append((l1 * secondary_rhs - m12 * primary_rhs) / (det_l + c_eps)) templ.block.state_eqs = state_eqs_f + state_eqs_t # ------------------------------------------------------------------ # Port-current algebraic equations # # In this simplified EMT model, the port currents are exactly the series # winding currents. The primary-side shunt branch is intentionally omitted. # ------------------------------------------------------------------ alg_eqs: list[Expr] = [] for idx in range(3): alg_eqs.append(if_act[idx] - (i_f[idx] + gm * v_f_term[idx])) alg_eqs.append(it_act[idx] - i_t[idx]) if if_n_act is not None: neutral_from_kcl: Expr = if_n_act + if_act[0] + if_act[1] + if_act[2] if from_ground_current is not None: neutral_from_kcl = neutral_from_kcl + from_ground_current else: pass alg_eqs.append(neutral_from_kcl) else: pass if it_n_act is not None: neutral_to_kcl: Expr = it_n_act + it_act[0] + it_act[1] + it_act[2] if to_ground_current is not None: neutral_to_kcl = neutral_to_kcl + to_ground_current else: pass alg_eqs.append(neutral_to_kcl) else: pass templ.block.algebraic_eqs = alg_eqs output_vars: list[Var] = list() if if_n_act is not None: output_vars.append(if_n_act) else: pass output_vars.extend(if_act) if it_n_act is not None: output_vars.append(it_n_act) else: pass output_vars.extend(it_act) templ.block.out_vars = output_vars # ------------------------------------------------------------------ # External mapping for network stamping / PF seeding # ------------------------------------------------------------------ templ.block.external_mapping = dict({ VarPowerFlowReferenceType.if_N: if_n_act, VarPowerFlowReferenceType.if_A: if_act[0], VarPowerFlowReferenceType.if_B: if_act[1], VarPowerFlowReferenceType.if_C: if_act[2], VarPowerFlowReferenceType.it_N: it_n_act, VarPowerFlowReferenceType.it_A: it_act[0], VarPowerFlowReferenceType.it_B: it_act[1], VarPowerFlowReferenceType.it_C: it_act[2], }) templ.block.external_mapping[VarPowerFlowReferenceType.vf_N] = vf_n templ.block.external_mapping[VarPowerFlowReferenceType.vt_N] = vt_n templ.block.external_mapping[VarPowerFlowReferenceType.vf_A] = vf_vars[0] templ.block.external_mapping[VarPowerFlowReferenceType.vf_B] = vf_vars[1] templ.block.external_mapping[VarPowerFlowReferenceType.vf_C] = vf_vars[2] templ.block.external_mapping[VarPowerFlowReferenceType.vt_A] = vt_vars[0] templ.block.external_mapping[VarPowerFlowReferenceType.vt_B] = vt_vars[1] templ.block.external_mapping[VarPowerFlowReferenceType.vt_C] = vt_vars[2] # ------------------------------------------------------------------ # State initialization # # Since the EMT model omits the shunt branch, initialize the secondary # winding current directly from the secondary port current and initialize # the primary winding current from the secondary current referred through # the total voltage ratio. # # With branch currents positive when leaving the bus: # primary series current ~= -(secondary port current) / total_voltage_ratio # ------------------------------------------------------------------ init_eqs: Dict[Var, Expr] = dict() for idx in range(3): init_eqs[i_f[idx]] = if_act[idx] - gm * v_f_term[idx] init_eqs[i_t[idx]] = it_act[idx] templ.block.init_eqs = init_eqs # ------------------------------------------------------------------ # Derivative initialization aligned with runtime dynamics # ------------------------------------------------------------------ diff_init_eqs: Dict[Var, Expr] = dict() for idx in range(3): primary_rhs: Expr = v_f_term[idx] - r1 * i_f[idx] secondary_rhs: Expr = v_t_term[idx] - r2 * i_t[idx] diff_init_eqs[di_f[idx]] = (l2 * primary_rhs - m12 * secondary_rhs) / (det_l + c_eps) diff_init_eqs[di_t[idx]] = (l1 * secondary_rhs - m12 * primary_rhs) / (det_l + c_eps) templ.block.diff_init_eqs = diff_init_eqs grounding_pairs: List[Tuple[Var, Block]] = list() if vf_n is not None and from_grounding_link_block is not None: grounding_pairs.append((vf_n, from_grounding_link_block)) else: pass if vt_n is not None and to_grounding_link_block is not None: grounding_pairs.append((vt_n, to_grounding_link_block)) else: pass _attach_transformer_editor_diagram( root_block=templ.block, input_vars=input_vars, output_vars=output_vars, grounding_pairs=grounding_pairs, ) return templ