Source code for VeraGridEngine.Templates.Emt.bess_avm_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 __future__ import annotations

from VeraGridEngine.enumerations import DeviceType, VarPowerFlowReferenceType
from VeraGridEngine.Devices.Dynamic.emt_template import EmtModelTemplate
from VeraGridEngine.Devices.Dynamic.var_factory import VarFactory
from VeraGridEngine.Utils.Symbolic.block import Block, Expr
import VeraGridEngine.Utils.Symbolic.symbolic as sym

from VeraGridEngine.Templates.Emt.converter_emt_template import (
    _build_pseudo_emt_converter_vsc_block,
    _build_pseudo_emt_converter_pll_block,
    _build_pseudo_emt_converter_outer_loop_block,
    _build_pseudo_emt_converter_inner_loop_block,
    _build_pseudo_emt_converter_transformer_block,
)


[docs] def get_battery_avm_emt_template(vf: VarFactory, name: str) -> EmtModelTemplate: """ Build the aggregated battery block used by the Level-1 BESS template. The block represents the storage side as a Thevenin battery equivalent connected to the DC terminal used by the existing pseudo-EMT VSC block. It intentionally does not include a DC-link capacitor because the imported VSC block already owns the internal DC-link state ``v_dc`` and its capacitance ``C_dc``. Duplicating that capacitor here would create two competing DC energy buffers. Electrical model ---------------- The battery is represented by a smooth open-circuit voltage and a terminal resistance:: v_oc = v_min + (v_max - v_min) * soc_limited v_bus = v_oc - r_bat * i_bat where ``i_bat`` is taken as positive when the BESS discharges and delivers active power to the converter DC side. The DC terminal voltage ``v_bus`` is passed to the imported VSC block as its ``v_dc_bus`` input. Energy model ------------ The state of charge is updated from the battery terminal power. Positive power discharges the battery, while negative power charges it. Separate charge and discharge efficiencies are used through a smooth power split. Scope ----- This is a Level-1 network EMT approximation. It includes SoC, aggregated open-circuit voltage, internal resistance and efficiency. It does not include cell electrochemistry, balancing, thermal behaviour, degradation, a detailed BMS, or a bidirectional DC-DC converter. A future Level-2 model can replace this block by an averaged DC-DC stage while keeping the imported VSC/control blocks unchanged. :param vf: EMT symbolic variable factory. :param name: Symbolic model name. :return: Battery-side EMT template. """ templ = EmtModelTemplate() templ.tpe = DeviceType.BatteryDevice templ.name = name templ.block.name = name # ------------------------------------------------------------------ # Inputs from the imported VSC block. # ------------------------------------------------------------------ i_dc = vf.add_var(name=f"i_dc_bat_in_{name}") i_dc_conv_init = vf.add_var(name=f"i_dc_conv_bat_in_{name}") r_dc_init = vf.add_var(name=f"r_dc_bat_in_{name}") r_dc_term_init = vf.add_var(name=f"r_dc_term_bat_in_{name}") # ------------------------------------------------------------------ # Battery state and derivative. # ------------------------------------------------------------------ soc = vf.add_var(name=f"soc_{name}") d_soc = vf.add_diff_var(name=f"d_soc_{name}", base_var=soc) # ------------------------------------------------------------------ # Algebraic battery variables. # ------------------------------------------------------------------ v_dc_bus = vf.add_var(name=f"v_dc_bus_bat_{name}", reference=VarPowerFlowReferenceType.Vdc) v_oc = vf.add_var(name=f"v_oc_bat_{name}") i_bat = vf.add_var(name=f"i_bat_{name}") p_bat = vf.add_var(name=f"p_bat_{name}") p_dis = vf.add_var(name=f"p_dis_bat_{name}") p_ch = vf.add_var(name=f"p_ch_bat_{name}") # ------------------------------------------------------------------ # Local battery parameters. # ------------------------------------------------------------------ e_cap = vf.add_var(name=f"e_cap_bat_{name}") v_min = vf.add_var(name=f"v_min_bat_{name}") v_max = vf.add_var(name=f"v_max_bat_{name}") r_bat = vf.add_var(name=f"r_bat_{name}") eta_ch = vf.add_var(name=f"eta_ch_bat_{name}") eta_dis = vf.add_var(name=f"eta_dis_bat_{name}") soc_min = vf.add_var(name=f"soc_min_bat_{name}") soc_max = vf.add_var(name=f"soc_max_bat_{name}") soc0 = vf.add_var(name=f"soc0_bat_{name}") c0 = vf.add_const(0.0) c1 = vf.add_const(1.0) c_half = vf.add_const(0.5) eps = vf.add_const(1e-10) soc_limited: Expr = sym.hard_sat(soc, soc_min, soc_max) discharge_enabled: Expr = sym.heaviside(soc - soc_min) charge_enabled: Expr = sym.heaviside(soc_max - soc) p_abs: Expr = sym.sqrt(p_bat * p_bat + eps) p_dis_expr: Expr = c_half * (p_bat + p_abs) p_ch_expr: Expr = c_half * (-p_bat + p_abs) v_oc0: Expr = v_min + (v_max - v_min) * soc0 # Initialize the battery variables from a closed-form solution of the # coupled battery/VSC DC-side steady-state equations. This avoids an # artificial current step at t = 0 while keeping the existing battery and # VSC block structure unchanged. i_dc0_init: Expr = (i_dc_conv_init + v_oc0 / (r_dc_init + eps)) / (c1 + (r_dc_term_init - r_bat) / (r_dc_init + eps) + eps) i_bat0: Expr = -i_dc0_init v_dc_bus0: Expr = v_oc0 - r_bat * i_bat0 p_bat0: Expr = v_dc_bus0 * i_bat0 p_abs0: Expr = sym.sqrt(p_bat0 * p_bat0 + eps) p_dis0: Expr = c_half * (p_bat0 + p_abs0) p_ch0: Expr = c_half * (-p_bat0 + p_abs0) templ.block = Block( state_eqs=[ # Positive battery power discharges the battery; negative power # charges it. Efficiencies are applied on the energy side. ((-p_dis / (eta_dis + eps)) * discharge_enabled + (eta_ch * p_ch) * charge_enabled) / (e_cap + eps), ], state_vars=[soc], diff_vars=[d_soc], algebraic_eqs=[ # Smooth SoC-dependent open-circuit voltage. v_oc - (v_min + (v_max - v_min) * soc_limited), # The imported VSC block exposes DC current as positive when the # converter absorbs energy from the DC source and negative when it # delivers DC energy into AC power. The battery model uses the # opposite physical convention: positive current means battery # discharge into the converter. The algebraic coupling therefore # flips the sign so both subsystems share one consistent generator # convention at the BESS level. i_bat + i_dc, # Resistive Thevenin terminal relation feeding the VSC DC bus. v_dc_bus - (v_oc - r_bat * i_bat), # Battery terminal power. With the current mapping above, positive # battery current means discharge and therefore positive battery # terminal power means export from the battery into the converter. p_bat - v_dc_bus * i_bat, # Smooth discharge and charge power components. p_dis - p_dis_expr, p_ch - p_ch_expr, ], algebraic_vars=[v_dc_bus, v_oc, i_bat, p_bat, p_dis, p_ch], event_dict={ e_cap: vf.add_const(10.0), v_min: vf.add_const(0.80), v_max: vf.add_const(1.20), r_bat: vf.add_const(0.02), eta_ch: vf.add_const(0.96), eta_dis: vf.add_const(0.96), soc_min: vf.add_const(0.05), soc_max: vf.add_const(0.95), soc0: vf.add_const(0.50), }, init_eqs={ soc: soc0, v_oc: v_oc0, i_bat: i_bat0, v_dc_bus: v_dc_bus0, p_bat: p_bat0, p_dis: p_dis0, p_ch: p_ch0, }, diff_init_eqs={d_soc: c0}, in_vars=[i_dc, i_dc_conv_init, r_dc_init, r_dc_term_init], out_vars=[v_dc_bus, soc, v_oc, i_bat, p_bat, p_dis, p_ch], name=f"{name}_battery", ) return templ
def _build_bess_outer_power_loop_block(vf: VarFactory, name: str) -> Block: """ Build one BESS-specific outer loop that directly tracks active/reactive power. Unlike the generic pseudo-converter outer loop, the Level-1 BESS does not need to regulate an externally imposed DC-link voltage. The battery is the DC source, so the outer loop is simpler and more robust when it regulates the requested AC active and reactive powers directly. :param vf: EMT symbolic variable factory. :param name: Symbolic model name. :return: Outer-loop block. """ 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}") 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}") Vpk = vf.add_var(name=f"Vpk_outer_in_{name}") P_loss0 = vf.add_var(name=f"P_loss0_outer_in_{name}") p_kp = vf.add_var(name=f"vdc_kp_outer_in_{name}") p_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}") 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) P_ref_pu = P_ref / sbase Q_ref_pu = Q_ref / sbase P_ac_ff_pu = P_ref_pu + P_loss0 / sbase i_d0 = c23 * P_ac_ff_pu / (Vpk + eps) i_q0 = c23 * Q_ref_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=[ p_ki * ((P_ref_pu - P_f) + 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=[xi_vdc, xi_q, P_f, Q_f], diff_vars=[d_xi_vdc, d_xi_q, d_P_f, d_Q_f], algebraic_eqs=[ 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 + p_kp * (P_ref_pu - P_f) + 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=[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: i_d0 - (c23 * P_ac_ff_pu / (Vpk + eps)) - p_kp * (P_ref_pu - P), 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={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, P, Q, sbase, P_ref, Q_ref, Vpk, P_loss0, p_kp, p_ki, q_kp, q_ki, i_max, tau_meas, aw_gain, ], out_vars=[P_f, Q_f, i_0_ref, i_d_ref, i_q_ref], name=f"{name}_outer_loop_bess", )
[docs] def get_bess_avm_grid_following_emt_template( vf: VarFactory, name: str = "bess_avm_grid_following_emt", ) -> EmtModelTemplate: """ Build a Level-1 averaged-value grid-following BESS EMT template. This template assembles an aggregated Battery Energy Storage System by reusing the existing pseudo-EMT converter blocks from ``converter_emt_template.py`` and adding only the BESS-specific storage block. The goal is to avoid duplicating PLL, VSC, outer-loop, inner-loop and dq0/abc interface logic that already exists in the standard VeraGrid converter template. Physical structure ------------------ The assembled model is:: battery Thevenin equivalent + SoC -> VSC DC terminal -> imported VSC/DC-link block -> imported SRF-PLL -> imported outer Vdc/Q or P/Q loop -> imported inner dq0 current controller -> imported AC-side L/R dq0 interface -> abc current injection into the EMT network The battery block provides the DC terminal voltage consumed by the imported VSC block. The imported VSC block still owns the internal DC-link capacitor and converter power/loss equations. This avoids having two DC-link capacitors in the same BESS model. Included behaviour ------------------ * Aggregated SoC state. * SoC-dependent open-circuit voltage. * Battery internal resistance. * Charge/discharge efficiencies. * Existing averaged VSC power balance and DC-link dynamics. * Existing PLL, dq0 current control, current limiting and modulation limit. * Existing dq0-to-abc network current injection. Not included ------------ * Switching-level PWM or semiconductor devices. * Cell-level electrochemistry, thermal behaviour or degradation. * Detailed BMS and cell balancing. * Bidirectional DC-DC converter stage. * Additional plant-level PPC logic beyond the imported converter controls. Sign convention --------------- Positive AC active power follows the existing pseudo-EMT converter convention. Positive battery power means the battery discharges into the converter DC side and SoC decreases. :param vf: EMT symbolic variable factory. :param name: Symbolic model name. :return: Fully assembled EMT BESS template. """ templ = EmtModelTemplate() templ.tpe = DeviceType.BatteryDevice # TODO: not sure if VSC or PV/BESS if exists templ.name = name templ.block.name = name # ------------------------------------------------------------------ # Network-facing AC terminal variables. The BESS is modelled as an AC # device with an internal battery, so the DC bus voltage is produced by the # battery block rather than received from an external DC network. # ------------------------------------------------------------------ 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) # ------------------------------------------------------------------ # Imported converter/control blocks. # ------------------------------------------------------------------ vsc_block = _build_pseudo_emt_converter_vsc_block(vf=vf, name=name) pll_block = _build_pseudo_emt_converter_pll_block(vf=vf, name=name) outer_loop_block = _build_bess_outer_power_loop_block(vf=vf, name=name) inner_loop_block = _build_pseudo_emt_converter_inner_loop_block(vf=vf, name=name) transformer_block = _build_pseudo_emt_converter_transformer_block(vf=vf, name=name) # ------------------------------------------------------------------ # New BESS-specific block. # ------------------------------------------------------------------ battery_block = get_battery_avm_emt_template(vf=vf, name=name).block # ------------------------------------------------------------------ # Wiring copied from the existing full pseudo-EMT converter assembly, with # only one change: the VSC DC terminal input is connected to the battery # block output instead of an external DC bus variable. # ------------------------------------------------------------------ vf.add_connections(transformer_block.in_vars[0:3], [v_A, v_B, v_C]) vf.add_connections([vsc_block.in_vars[6]], [battery_block.out_vars[0]]) # The VSC DC terminal current is fed back to the battery block so the SoC # and Thevenin terminal voltage follow the converter operating point. vf.add_connections([battery_block.in_vars[0]], [vsc_block.out_vars[1]]) # These additional VSC variables are only used to build a closed-form, # algebraically consistent battery/DC-link initialization point. vf.add_connections( battery_block.in_vars[1:4], [vsc_block.out_vars[33], vsc_block.out_vars[14], vsc_block.out_vars[15]], ) # Transformer/interface provides dq0 voltages and currents to the VSC and # the 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]], [vsc_block.out_vars[2], vsc_block.out_vars[3]]) vf.add_connections( outer_loop_block.in_vars[8:20], [ vsc_block.out_vars[4], vsc_block.out_vars[5], vsc_block.out_vars[6], 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], ], ) 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]], ) # ------------------------------------------------------------------ # Unified model assembly. # ------------------------------------------------------------------ templ.block.children.extend([ battery_block, 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] templ.block.out_vars = [ transformer_block.out_vars[0], transformer_block.out_vars[1], transformer_block.out_vars[2], # vsc_block.out_vars[1], # battery_block.out_vars[1], # battery_block.out_vars[4], ] # ------------------------------------------------------------------ # External mapping for EmtProblemDae. # ------------------------------------------------------------------ templ.block.external_mapping = { VarPowerFlowReferenceType.v_A: v_A, VarPowerFlowReferenceType.v_B: v_B, VarPowerFlowReferenceType.v_C: v_C, # The BESS has an internal battery/DC source. Do not expose an external # DC-voltage network port here, otherwise the EMT assembler tries to # connect an additional DC-network equation to an internal algebraic # variable and the compiled DAE can become non-square. VarPowerFlowReferenceType.i_A: transformer_block.out_vars[0], VarPowerFlowReferenceType.i_B: transformer_block.out_vars[1], VarPowerFlowReferenceType.i_C: transformer_block.out_vars[2], # Idc is internal to the BESS battery/VSC coupling. The AC network only # sees the three-phase current injection. 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], } # The static/API mapping remains exactly the one expected by the imported # VSC block, so existing converter initialization code can keep working. templ.block.api_obj_mapping = dict(vsc_block.api_obj_mapping) return templ