# 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 enum import IntEnum
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, Var, 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]
class PvEmtModelLevel(IntEnum):
"""
Enumerate the supported PV EMT template detail levels.
``LEVEL_1`` is the simplified PV availability plus MPPT-lag model, while
``LEVEL_2`` adds the averaged PV plus DC-DC boost dynamics.
:ivar LEVEL_1: Simplified PV availability plus MPPT-lag model.
:ivar LEVEL_2: Averaged PV plus DC-DC boost model.
"""
LEVEL_1 = 1
LEVEL_2 = 2
# =============================================================================
# Shared converter assembly helpers
# =============================================================================
def _build_converter_blocks(vf: VarFactory, name: str) -> tuple[Block, Block, Block, Block, Block]:
"""
Build the imported pseudo-EMT converter sub-blocks used by both PV levels.
This helper intentionally reuses the existing VeraGrid converter blocks. The
PV templates in this file add only the PV/DC-source side and then wire it to
the imported VSC DC terminal. This keeps PLL, VSC, outer-loop, inner-loop
and dq0/abc interface behaviour consistent with the already available
pseudo-EMT converter template.
:param vf: EMT symbolic variable factory.
:param name: Symbolic model name.
:return: Tuple with VSC, PLL, outer-loop, inner-loop and AC-interface blocks.
"""
vsc_block: Block = _build_pseudo_emt_converter_vsc_block(vf=vf, name=name)
pll_block: Block = _build_pseudo_emt_converter_pll_block(vf=vf, name=name)
outer_loop_block: Block = _build_pseudo_emt_converter_outer_loop_block(vf=vf, name=name)
inner_loop_block: Block = _build_pseudo_emt_converter_inner_loop_block(vf=vf, name=name)
transformer_block: Block = _build_pseudo_emt_converter_transformer_block(vf=vf, name=name)
return vsc_block, pll_block, outer_loop_block, inner_loop_block, transformer_block
def _connect_imported_converter_blocks(
v_A: Var,
v_B: Var,
v_C: Var,
dc_voltage_source: Var,
vsc_block: Block,
pll_block: Block,
outer_loop_block: Block,
inner_loop_block: Block,
transformer_block: Block,
) -> None:
"""
Wire the imported pseudo-EMT converter blocks to one internal DC source.
The wiring mirrors ``get_full_pseudo_emt_converter`` except that the VSC
DC-terminal voltage is not an external DC bus. Instead it is supplied by the
PV-side block of the current template. The VSC DC current output is also
exposed to the PV-side block so that the PV/DC stage can compute its own DC
power and internal dynamics.
:param v_A: Phase-A terminal voltage input.
:param v_B: Phase-B terminal voltage input.
:param v_C: Phase-C terminal voltage input.
:param dc_voltage_source: Internal DC terminal voltage supplied to the VSC.
:param vsc_block: Imported VSC/DC-link block.
:param pll_block: Imported PLL block.
:param outer_loop_block: Imported outer-loop block.
:param inner_loop_block: Imported inner-loop block.
:param transformer_block: Imported AC-side dq0/abc interface block.
:return: None.
"""
transformer_block.connect(transformer_block.in_vars[0:3], [v_A, v_B, v_C])
vsc_block.connect([vsc_block.in_vars[6]], [dc_voltage_source])
vsc_block.connect(vsc_block.in_vars[0:3], transformer_block.out_vars[6:9])
vsc_block.connect(vsc_block.in_vars[3:6], transformer_block.out_vars[3:6])
pll_block.connect([pll_block.in_vars[0]], [transformer_block.out_vars[7]])
outer_loop_block.connect(outer_loop_block.in_vars[0:3], transformer_block.out_vars[6:9])
outer_loop_block.connect(outer_loop_block.in_vars[3:6], transformer_block.out_vars[3:6])
inner_loop_block.connect(inner_loop_block.in_vars[0:3], transformer_block.out_vars[6:9])
inner_loop_block.connect(inner_loop_block.in_vars[3:6], transformer_block.out_vars[3:6])
pll_block.connect(
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]],
)
outer_loop_block.connect(
[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]],
)
outer_loop_block.connect(
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],
],
)
inner_loop_block.connect(
[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]],
)
inner_loop_block.connect(inner_loop_block.in_vars[10:13], outer_loop_block.out_vars[2:5])
inner_loop_block.connect(
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],
],
)
transformer_block.connect([transformer_block.in_vars[3], transformer_block.in_vars[4]], pll_block.out_vars)
transformer_block.connect(
transformer_block.in_vars[5:8],
[vsc_block.out_vars[8], vsc_block.out_vars[11], vsc_block.out_vars[12]],
)
transformer_block.connect(transformer_block.in_vars[8:11], inner_loop_block.out_vars)
transformer_block.connect(
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]],
)
def _set_common_external_mapping(
templ: EmtModelTemplate,
v_A: Var,
v_B: Var,
v_C: Var,
vsc_block: Block,
transformer_block: Block,
) -> None:
"""
Publish the AC-injection external mapping expected by ``EmtProblemDae``.
The PV model is an AC injection with an internal DC side. Its DC-link voltage
and DC current are internal diagnostic quantities, not external network ports.
Exposing them in ``external_mapping`` would incorrectly tell the EMT network
assembler to create an additional DC-side connection.
:param templ: EMT template being assembled.
:param v_A: Phase-A terminal voltage variable.
:param v_B: Phase-B terminal voltage variable.
:param v_C: Phase-C terminal voltage variable.
:param vsc_block: Imported VSC block.
:param transformer_block: Imported AC-side interface block.
:return: None.
"""
templ.block.external_mapping = {
VarPowerFlowReferenceType.v_A: v_A,
VarPowerFlowReferenceType.v_B: v_B,
VarPowerFlowReferenceType.v_C: v_C,
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.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],
}
# =============================================================================
# Level 1 β PV AVM grid-following
# PV availability + MPPT lag + imported VSC controls
# =============================================================================
def _build_level1_pv_availability_block(vf: VarFactory, name: str) -> Block:
"""
Build the Level-1 PV availability and MPPT-lag DC source block.
The Level-1 PV model is intentionally equivalent in complexity to the BESS
Level-1 block. It does not model a DC-DC converter. Instead, it aggregates
the PV array into a DC source whose available power depends on irradiance
and cell temperature, and whose active-power availability follows an MPPT
first-order lag.
The imported VSC block still owns the explicit DC-link capacitor ``C_dc``.
Therefore this PV block supplies only the internal DC terminal voltage used
by the VSC and computes diagnostic PV current/power quantities from the VSC
DC current feedback.
:param vf: EMT symbolic variable factory.
:param name: Symbolic model name.
:return: Level-1 PV availability block.
"""
i_dc = vf.add_var(name=f"i_dc_pv_l1_in_{name}")
v_dc_ctrl = vf.add_var(name=f"v_dc_ctrl_pv_l1_in_{name}")
p_mppt = vf.add_var(name=f"p_mppt_l1_{name}")
d_p_mppt = vf.add_diff_var(name=f"d_p_mppt_l1_{name}", base_var=p_mppt)
v_dc_bus = vf.add_var(name=f"v_dc_bus_pv_l1_{name}", reference=VarPowerFlowReferenceType.Vdc)
p_avail = vf.add_var(name=f"p_avail_l1_{name}")
p_pv = vf.add_var(name=f"p_pv_l1_{name}")
i_pv = vf.add_var(name=f"i_pv_l1_{name}")
v_oc = vf.add_var(name=f"v_oc_pv_l1_{name}")
p_curt = vf.add_var(name=f"p_curt_l1_{name}")
p_rated = vf.add_var(name=f"p_rated_l1_{name}")
irradiance = vf.add_var(name=f"irradiance_l1_{name}")
irradiance_ref = vf.add_var(name=f"irradiance_ref_l1_{name}")
t_cell = vf.add_var(name=f"t_cell_l1_{name}")
t_ref = vf.add_var(name=f"t_ref_l1_{name}")
temp_coeff_p = vf.add_var(name=f"temp_coeff_p_l1_{name}")
t_mppt = vf.add_var(name=f"t_mppt_l1_{name}")
v_dc_ref = vf.add_var(name=f"v_dc_ref_l1_{name}")
r_pv = vf.add_var(name=f"r_pv_l1_{name}")
k_pv_power = vf.add_var(name=f"k_pv_power_l1_{name}")
k_vdc_ctrl = vf.add_var(name=f"k_vdc_ctrl_l1_{name}")
c0 = vf.add_const(0.0)
eps = vf.add_const(1e-10)
irradiance_eff: Expr = sym.max(irradiance_ref, eps)
p_avail_raw: Expr = p_rated * (irradiance / irradiance_eff) * (vf.add_const(1.0) + temp_coeff_p * (t_cell - t_ref))
p_avail_expr: Expr = sym.hard_sat(p_avail_raw, c0, p_rated)
mppt_limited_power: Expr = sym.hard_sat(p_mppt, c0, p_avail)
r_pv_eff: Expr = sym.max(r_pv, eps)
p_pv_init: Expr = sym.hard_sat(mppt_limited_power, c0, (v_dc_ref * v_dc_ref) / (vf.add_const(4.0) * r_pv_eff))
p_init_disc: Expr = sym.max(v_dc_ref * v_dc_ref - vf.add_const(4.0) * r_pv_eff * p_pv_init, eps)
i_pv_init_mag: Expr = (v_dc_ref - sym.sqrt(p_init_disc)) / (vf.add_const(2.0) * r_pv_eff)
i_pv_init: Expr = -i_pv_init_mag
v_dc_bus_init: Expr = v_dc_ref + r_pv_eff * i_pv_init
v_oc_ctrl_expr: Expr = v_dc_ref - k_vdc_ctrl * (v_dc_ctrl - v_dc_ref)
return Block(
state_eqs=[
# Irradiance and temperature modify the available PV power. MPPT is
# represented as a first-order lag between available power and the
# power that the inverter can actually request from the PV array.
(p_avail - p_mppt) / (t_mppt + eps),
],
state_vars=[p_mppt],
diff_vars=[d_p_mppt],
algebraic_eqs=[
p_avail - p_avail_expr,
# PV-side DC sign convention:
# * positive ``p_pv`` means PV power delivered to the converter,
# * positive ``i_pv`` would mean source current delivered to the
# converter, but the imported pseudo-EMT converter reports the
# same terminal current with the opposite sign during export.
# Therefore exported PV operation uses ``i_pv < 0`` and ``i_dc > 0``
# or equivalently ``i_pv + i_dc = 0`` at the shared internal port.
i_pv + i_dc,
p_pv + v_dc_bus * i_pv,
# The Level-1 source is a Thevenin-like PV equivalent around one
# MPPT operating voltage. A power droop term drives the delivered
# DC power toward the irradiance-limited MPPT target without adding
# a second internal DC storage state.
# Local PV-side Vdc feedback trims the source open-circuit voltage so
# the PV block can regulate the converter DC-link without modifying
# the shared converter outer-loop implementation.
v_oc - v_oc_ctrl_expr,
v_dc_bus - (v_oc + r_pv * i_pv - k_pv_power * (p_pv - mppt_limited_power)),
# Curtailment is the available PV power that is not delivered to the
# converter. Keep it non-negative even under small numerical errors.
p_curt - sym.max(p_avail - mppt_limited_power, c0),
],
algebraic_vars=[v_dc_bus, p_avail, p_pv, i_pv, v_oc, p_curt],
event_dict={
p_rated: vf.add_const(1.0),
irradiance: vf.add_const(1000.0),
irradiance_ref: vf.add_const(1000.0),
t_cell: vf.add_const(25.0),
t_ref: vf.add_const(25.0),
temp_coeff_p: vf.add_const(-0.004),
t_mppt: vf.add_const(0.20),
v_dc_ref: vf.add_const(1.0),
r_pv: vf.add_const(0.02),
k_pv_power: vf.add_const(10.0),
k_vdc_ctrl: vf.add_const(1.0),
},
init_eqs={
p_avail: p_avail_expr,
p_mppt: p_avail_expr,
p_pv: p_pv_init,
i_pv: i_pv_init,
v_oc: v_dc_ref,
v_dc_bus: v_dc_bus_init,
p_curt: p_avail_expr - p_pv_init,
},
diff_init_eqs={d_p_mppt: c0},
in_vars=[i_dc, v_dc_ctrl],
out_vars=[v_dc_bus, p_avail, p_mppt, p_pv, i_pv, p_curt],
name=f"{name}_pv_level1",
)
[docs]
def get_pv_avm_grid_following_emt_template(
vf: VarFactory,
name: str = "pv_avm_grid_following_emt",
) -> EmtModelTemplate:
"""
Build the Level-1 PV averaged-value grid-following EMT template.
This model is the PV counterpart of the Level-1 BESS model. It can replace
the BESS template in the same AC EMT examples because the external network
interface is the same: three AC phase voltages as inputs and three AC phase
currents as outputs.
Structure
---------
The assembled model is::
PV availability from irradiance and temperature
-> first-order MPPT power availability
-> internal DC terminal voltage
-> imported VSC/DC-link block
-> imported SRF-PLL
-> imported outer P/Q or Vdc/Q loop
-> imported inner dq0 current controller
-> imported AC-side dq0/abc interface
-> abc current injection into the EMT network
Included behaviour
------------------
* Irradiance-dependent available active power.
* Cell-temperature active-power correction.
* MPPT first-order lag.
* PV curtailment diagnostic channel.
* Existing averaged VSC power balance and DC-link dynamics.
* Existing PLL, dq0 current control, current limiting and modulation limit.
Not included
------------
* Explicit PV diode equation or cell-level model.
* Explicit averaged boost converter.
* Switching-level PWM or semiconductor devices.
* Plant-level PPC beyond the imported converter control interface.
:param vf: EMT symbolic variable factory.
:param name: Symbolic model name.
:return: Fully assembled Level-1 PV EMT template.
"""
templ = EmtModelTemplate()
templ.tpe = DeviceType.GeneratorDevice
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)
vsc_block: Block = _build_pseudo_emt_converter_vsc_block(vf=vf, name=name)
pll_block: Block = _build_pseudo_emt_converter_pll_block(vf=vf, name=name)
outer_loop_block: Block = _build_pseudo_emt_converter_outer_loop_block(vf=vf, name=name)
inner_loop_block: Block = _build_pseudo_emt_converter_inner_loop_block(vf=vf, name=name)
transformer_block: Block = _build_pseudo_emt_converter_transformer_block(vf=vf, name=name)
pv_block: Block = _build_level1_pv_availability_block(vf=vf, name=name)
_connect_imported_converter_blocks(
v_A=v_A,
v_B=v_B,
v_C=v_C,
dc_voltage_source=pv_block.out_vars[0],
vsc_block=vsc_block,
pll_block=pll_block,
outer_loop_block=outer_loop_block,
inner_loop_block=inner_loop_block,
transformer_block=transformer_block,
)
pv_block.connect(pv_block.in_vars[0:2], [vsc_block.out_vars[1], vsc_block.out_vars[0]])
templ.block.children.extend([pv_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],
pv_block.out_vars[1],
pv_block.out_vars[2],
pv_block.out_vars[3],
pv_block.out_vars[5],
]
_set_common_external_mapping(
templ=templ,
v_A=v_A,
v_B=v_B,
v_C=v_C,
vsc_block=vsc_block,
transformer_block=transformer_block,
)
templ.block.api_obj_mapping = dict(vsc_block.api_obj_mapping)
return templ
# =============================================================================
# Level 2 β PV + averaged DC-DC
# PV array + boost converter average + DC-link + VSC
# =============================================================================
def _build_level2_pv_boost_block(vf: VarFactory, name: str) -> Block:
"""
Build the Level-2 PV array plus averaged boost-converter DC source block.
This block adds an explicit averaged boost stage between the PV array and
the imported VSC DC terminal. It remains an aggregated network EMT model,
not a switching model. The PV array is represented by an irradiance and
temperature dependent MPP voltage/current pair. The boost duty cycle follows
a PI MPPT control law and determines the PV-side voltage through the average
boost relation::
v_pv = (1 - duty) * v_dc_bus
The block still does not duplicate the VSC internal DC-link capacitor. The
imported VSC block owns that state. This block supplies a controlled DC
terminal voltage and exposes PV-side diagnostics such as ``v_pv``, ``duty``,
``p_src`` and ``p_curt``.
:param vf: EMT symbolic variable factory.
:param name: Symbolic model name.
:return: Level-2 PV averaged boost block.
"""
i_dc = vf.add_var(name=f"i_dc_pv_l2_in_{name}")
v_dc_ctrl = vf.add_var(name=f"v_dc_ctrl_pv_l2_in_{name}")
xi_mppt = vf.add_var(name=f"xi_mppt_l2_{name}")
duty = vf.add_var(name=f"duty_l2_{name}")
d_xi_mppt = vf.add_diff_var(name=f"d_xi_mppt_l2_{name}", base_var=xi_mppt)
d_duty = vf.add_diff_var(name=f"d_duty_l2_{name}", base_var=duty)
v_dc_bus = vf.add_var(name=f"v_dc_bus_pv_l2_{name}", reference=VarPowerFlowReferenceType.Vdc)
v_mp = vf.add_var(name=f"v_mp_l2_{name}")
i_mp = vf.add_var(name=f"i_mp_l2_{name}")
p_avail = vf.add_var(name=f"p_avail_l2_{name}")
v_pv = vf.add_var(name=f"v_pv_l2_{name}")
p_src = vf.add_var(name=f"p_src_l2_{name}")
p_to_vsc = vf.add_var(name=f"p_to_vsc_l2_{name}")
i_pv = vf.add_var(name=f"i_pv_l2_{name}")
duty_ref = vf.add_var(name=f"duty_ref_l2_{name}")
p_curt = vf.add_var(name=f"p_curt_l2_{name}")
p_rated = vf.add_var(name=f"p_rated_l2_{name}")
irradiance = vf.add_var(name=f"irradiance_l2_{name}")
irradiance_ref = vf.add_var(name=f"irradiance_ref_l2_{name}")
t_cell = vf.add_var(name=f"t_cell_l2_{name}")
t_ref = vf.add_var(name=f"t_ref_l2_{name}")
v_mp_ref = vf.add_var(name=f"v_mp_ref_l2_{name}")
i_mp_ref = vf.add_var(name=f"i_mp_ref_l2_{name}")
temp_coeff_v = vf.add_var(name=f"temp_coeff_v_l2_{name}")
temp_coeff_i = vf.add_var(name=f"temp_coeff_i_l2_{name}")
v_dc_ref = vf.add_var(name=f"v_dc_ref_l2_{name}")
r_dc = vf.add_var(name=f"r_dc_l2_{name}")
d0 = vf.add_var(name=f"d0_l2_{name}")
d_min = vf.add_var(name=f"d_min_l2_{name}")
d_max = vf.add_var(name=f"d_max_l2_{name}")
kp_mppt = vf.add_var(name=f"kp_mppt_l2_{name}")
ki_mppt = vf.add_var(name=f"ki_mppt_l2_{name}")
t_duty = vf.add_var(name=f"t_duty_l2_{name}")
eta_boost = vf.add_var(name=f"eta_boost_l2_{name}")
k_src = vf.add_var(name=f"k_src_l2_{name}")
k_boost_power = vf.add_var(name=f"k_boost_power_l2_{name}")
k_vdc_ctrl = vf.add_var(name=f"k_vdc_ctrl_l2_{name}")
c0 = vf.add_const(0.0)
c1 = vf.add_const(1.0)
eps = vf.add_const(1e-10)
irradiance_eff: Expr = sym.max(irradiance_ref, eps)
v_mp_expr: Expr = sym.max(v_mp_ref * (c1 + temp_coeff_v * (t_cell - t_ref)), eps)
i_mp_expr: Expr = sym.max(i_mp_ref * (irradiance / irradiance_eff) * (c1 + temp_coeff_i * (t_cell - t_ref)), c0)
p_avail_expr: Expr = sym.hard_sat(v_mp_expr * i_mp_expr, c0, p_rated)
v_pv_expr: Expr = sym.max((c1 - duty) * v_dc_bus, eps)
mppt_error: Expr = v_mp - v_pv
duty_ref_expr: Expr = sym.hard_sat(d0 + kp_mppt * mppt_error + ki_mppt * xi_mppt, d_min, d_max)
p_src_raw: Expr = p_avail - k_src * (v_pv - v_mp) * (v_pv - v_mp)
p_src_expr: Expr = sym.hard_sat(p_src_raw, c0, p_avail)
p_to_vsc_expr: Expr = eta_boost * p_src
r_dc_eff: Expr = sym.max(r_dc, eps)
v_dc_src_ctrl_expr: Expr = v_dc_ref - k_vdc_ctrl * (v_dc_ctrl - v_dc_ref)
p_to_vsc_init_disc: Expr = sym.max(v_dc_ref * v_dc_ref + vf.add_const(4.0) * r_dc_eff * p_to_vsc_expr, eps)
i_pv_init: Expr = (v_dc_ref - sym.sqrt(p_to_vsc_init_disc)) / (vf.add_const(2.0) * r_dc_eff)
v_dc_bus_init: Expr = v_dc_ref - r_dc_eff * i_pv_init
return Block(
state_eqs=[
# MPPT PI integrator driven by the PV voltage error.
mppt_error,
# Averaged duty-cycle actuator.
(duty_ref - duty) / (t_duty + eps),
],
state_vars=[xi_mppt, duty],
diff_vars=[d_xi_mppt, d_duty],
algebraic_eqs=[
v_mp - v_mp_expr,
i_mp - i_mp_expr,
p_avail - p_avail_expr,
v_pv - v_pv_expr,
duty_ref - duty_ref_expr,
p_src - p_src_expr,
# PV-side DC sign convention matches Level 1 and the imported VSC
# terminal current convention during export: positive PV power
# delivery to the converter is represented with ``i_pv < 0`` while
# the imported VSC reports the shared terminal current with the
# opposite sign. Therefore the common internal port satisfies
# ``i_pv + i_dc = 0`` during export.
i_pv + i_dc,
# Keep ``p_to_vsc`` positive when PV power is delivered from the
# source side into the converter interface.
p_to_vsc + v_dc_bus * i_pv,
# The boost output is represented as a controlled Thevenin-like DC
# source around v_dc_ref. A power droop term drives the delivered
# power toward the MPPT/boost target while the imported VSC block
# keeps the explicit converter DC-link state.
v_dc_bus - (v_dc_src_ctrl_expr - r_dc * i_pv - k_boost_power * (p_to_vsc - p_to_vsc_expr)),
p_curt - (p_avail - p_src),
],
algebraic_vars=[v_dc_bus, v_mp, i_mp, p_avail, v_pv, p_src, p_to_vsc, i_pv, duty_ref, p_curt],
event_dict={
p_rated: vf.add_const(1.0),
irradiance: vf.add_const(1000.0),
irradiance_ref: vf.add_const(1000.0),
t_cell: vf.add_const(25.0),
t_ref: vf.add_const(25.0),
v_mp_ref: vf.add_const(0.80),
i_mp_ref: vf.add_const(1.0),
temp_coeff_v: vf.add_const(-0.002),
temp_coeff_i: vf.add_const(0.0005),
v_dc_ref: vf.add_const(1.0),
r_dc: vf.add_const(0.02),
d0: vf.add_const(0.0000001),#0.20),
d_min: vf.add_const(0.0),
d_max: vf.add_const(0.95),
kp_mppt: vf.add_const(0.5),
ki_mppt: vf.add_const(1.0),
t_duty: vf.add_const(0.05),
eta_boost: vf.add_const(0.98),
k_src: vf.add_const(1.0),
k_boost_power: vf.add_const(10.0),
k_vdc_ctrl: vf.add_const(1.0),
},
init_eqs={
xi_mppt: c0,
duty: d0,
v_mp: v_mp_expr,
i_mp: i_mp_expr,
p_avail: p_avail_expr,
v_pv: v_dc_ref, #(c1 - d0) * v_dc_ref,
duty_ref: d0,
p_src: p_avail_expr,
p_to_vsc: p_to_vsc_expr,
i_pv: i_pv_init,
v_dc_bus: v_dc_bus_init,
p_curt: c0,
},
diff_init_eqs={d_xi_mppt: c0, d_duty: c0},
in_vars=[i_dc, v_dc_ctrl],
out_vars=[v_dc_bus, p_avail, p_src, p_to_vsc, v_pv, duty, v_mp, i_mp, p_curt],
name=f"{name}_pv_level2_boost",
)
[docs]
def get_pv_avm_boost_grid_following_emt_template(
vf: VarFactory,
name: str = "pv_avm_boost_grid_following_emt",
) -> EmtModelTemplate:
"""
Build the Level-2 PV averaged DC-DC plus grid-following VSC EMT template.
Structure
---------
The assembled model is::
PV array equivalent from irradiance and temperature
-> averaged boost converter with MPPT duty-cycle dynamics
-> internal DC terminal voltage
-> imported VSC/DC-link block
-> imported SRF-PLL
-> imported outer P/Q or Vdc/Q loop
-> imported inner dq0 current controller
-> imported AC-side dq0/abc interface
-> abc current injection into the EMT network
Compared with Level 1
---------------------
Level 1 only computes available PV power and applies an MPPT lag. Level 2
adds explicit averaged boost-converter variables: PV MPP voltage/current,
PV-side voltage, duty-cycle state, MPPT PI integrator and boost efficiency.
Not included
------------
* Switching-level DC-DC or PWM detail.
* Semiconductor device models.
* Cell-level diode equations.
* Plant-level PPC beyond the imported converter control interface.
:param vf: EMT symbolic variable factory.
:param name: Symbolic model name.
:return: Fully assembled Level-2 PV EMT template.
"""
templ = EmtModelTemplate()
templ.tpe = DeviceType.GeneratorDevice
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)
vsc_block, pll_block, outer_loop_block, inner_loop_block, transformer_block = _build_converter_blocks(vf=vf, name=name)
pv_boost_block: Block = _build_level2_pv_boost_block(vf=vf, name=name)
_connect_imported_converter_blocks(
v_A=v_A,
v_B=v_B,
v_C=v_C,
dc_voltage_source=pv_boost_block.out_vars[0],
vsc_block=vsc_block,
pll_block=pll_block,
outer_loop_block=outer_loop_block,
inner_loop_block=inner_loop_block,
transformer_block=transformer_block,
)
pv_boost_block.connect(pv_boost_block.in_vars[0:2], [vsc_block.out_vars[1], vsc_block.out_vars[0]])
templ.block.children.extend([
pv_boost_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],
pv_boost_block.out_vars[1],
pv_boost_block.out_vars[2],
pv_boost_block.out_vars[3],
pv_boost_block.out_vars[4],
pv_boost_block.out_vars[5],
pv_boost_block.out_vars[8],
]
_set_common_external_mapping(
templ=templ,
v_A=v_A,
v_B=v_B,
v_C=v_C,
vsc_block=vsc_block,
transformer_block=transformer_block,
)
templ.block.api_obj_mapping = dict(vsc_block.api_obj_mapping)
return templ
[docs]
def get_pv_grid_following_emt_template(
vf: VarFactory,
level: PvEmtModelLevel | int,
name: str,
) -> EmtModelTemplate:
"""
Build one PV EMT template from the requested model-detail level.
This helper keeps the Level-1 and Level-2 public constructors unchanged
while giving scripts one typed selection point for level-aware examples.
:param vf: EMT symbolic variable factory.
:param level: Requested PV model-detail level.
:param name: Symbolic model name.
:return: Fully assembled PV EMT template for the requested level.
:raises ValueError: If the requested level is not supported.
"""
pv_level: PvEmtModelLevel = PvEmtModelLevel(level)
if pv_level == PvEmtModelLevel.LEVEL_1:
return get_pv_avm_grid_following_emt_template(vf=vf, name=name)
if pv_level == PvEmtModelLevel.LEVEL_2:
return get_pv_avm_boost_grid_following_emt_template(vf=vf, name=name)
raise ValueError(f"Unsupported PV EMT model level: {level}")