Source code for g2elin_core.components.gfl

"""Grid-following converter (LCL filter + PLL), ported from
``Functions/symGFL.m`` + ``Functions/GFL_subs.m``. A GFL is never the
slack DG, so unlike SM/GFM there's only one variant.
"""

from __future__ import annotations

import cmath
from collections.abc import Mapping
from functools import lru_cache

import sympy as sp

from .base import (
    ComponentDAE, LinearComponent, NonlinearFuncs, NonlinearJacobians,
    RebuiltWithParams,
    apply_reduction, build_dae, equilibrium_subs, mode_key,
)

wb, wff, Rf, Lf, Cf, Rt, Lt = sp.symbols("wb wff Rf Lf Cf Rt Lt")
Kpd, Kid, Kiq, KpCL, KiCL, Kffv, Cdc, Gdc, Tdc, Kipll, Kppll = sp.symbols(
    "Kpd Kid Kiq KpCL KiCL Kffv Cdc Gdc Tdc Kipll Kppll"
)
M_d, M_q, M_CLd, M_CLq, M_pll, theta_pll = sp.symbols("M_d M_q M_CLd M_CLq M_pll theta_pll")
isd, isq, igd, igq, ved, veq, vdc, idc = sp.symbols("isd isq igd igq ved veq vdc idc")
md, mq, w_pll = sp.symbols("md mq w_pll")
idc_ref, vdc_ref, q_ref, theta_g, vgd_g, vgq_g = sp.symbols("idc_ref vdc_ref q_ref theta_g vgd_g vgq_g")

_STATE_NAMES = ["i_sd", "i_sq", "i_gd", "i_gq", "v_ed", "v_eq", "v_dc", "i_dc",
                "M_d", "M_q", "M_CLd", "M_CLq", "M_pll", "theta_pll"]


[docs] @lru_cache(maxsize=16) def gfl_dae(modes: tuple[tuple[str, str], ...] = ()) -> ComponentDAE: """``modes`` is a :func:`~g2elin_core.components.base.mode_key` tuple naming the states this converter gives up -- see :mod:`g2elin_core.reduction`. The default ``()`` is the full 14-state model. """ state_vec = [isd, isq, igd, igq, ved, veq, vdc, idc, M_d, M_q, M_CLd, M_CLq, M_pll, theta_pll] alg_vec = [md, mq, w_pll] us_vec = [vdc_ref, q_ref, idc_ref] ug_vec = [theta_g, vgd_g, vgq_g] input_vec = us_vec + ug_vec vgd = vgd_g * sp.cos(theta_g - theta_pll) - vgq_g * sp.sin(theta_g - theta_pll) vgq = vgd_g * sp.sin(theta_g - theta_pll) + vgq_g * sp.cos(theta_g - theta_pll) disd = (wb / Lf) * (md * vdc - ved - Rf * isd + w_pll * Lf * isq) disq = (wb / Lf) * (mq * vdc - veq - Rf * isq - w_pll * Lf * isd) digd = (wb / Lt) * (ved - vgd - Rt * igd + w_pll * Lt * igq) digq = (wb / Lt) * (veq - vgq - Rt * igq - w_pll * Lt * igd) dved = (wb / Cf) * (isd - igd + w_pll * Cf * veq) dveq = (wb / Cf) * (isq - igq - w_pll * Cf * ved) dvdc = (wb / Cdc) * (idc - Gdc * vdc - md * isd - mq * isq) didc = (1 / Tdc) * (idc_ref - idc) isd_ref = Kpd * (vdc_ref - vdc) + M_d dM_d = Kid * (vdc_ref - vdc) q = -ved * igq + veq * igd p = ved * igd + veq * igq dM_q = Kiq * (q_ref - q) dM_CLd = KiCL * (isd_ref - isd) dM_CLq = KiCL * (M_q - isq) dM_pll = Kipll * veq dtheta_pll = wb * w_pll diffeq_vec = [disd, disq, digd, digq, dved, dveq, dvdc, didc, dM_d, dM_q, dM_CLd, dM_CLq, dM_pll, dtheta_pll] alg = [ md - (1 / vdc) * (KpCL * (isd_ref - isd) + Kffv * ved - wff * Lf * isq + M_CLd), mq - (1 / vdc) * (KpCL * (M_q - isq) + Kffv * veq + wff * Lf * isd + M_CLq), w_pll - M_pll - Kppll * veq - wff, ] igd_g_out = igd * sp.cos(theta_pll - theta_g) - igq * sp.sin(theta_pll - theta_g) igq_g_out = igd * sp.sin(theta_pll - theta_g) + igq * sp.cos(theta_pll - theta_g) w_pll_out = M_pll + Kppll * veq + wff # a *different* expression from the alg var w_pll Vt = sp.sqrt(ved**2 + veq**2) output_vec = [p, q, w_pll_out, Vt, igd_g_out, igq_g_out] state_vec, diffeq_vec, state_names, alg_vec, alg, output_vec = apply_reduction( states=list(zip(state_vec, diffeq_vec, _STATE_NAMES)), modes=dict(modes), alg_vec=alg_vec, algeq_vec=alg, output_vec=output_vec, ) return build_dae( state_vec=state_vec, alg_vec=alg_vec, input_vec=input_vec, diffeq_vec=diffeq_vec, algeq_vec=alg, output_vec=output_vec, n_us=3, n_ug=3, n_out_s=4, n_out_g=2, state_names=state_names, input_names=["Vdc_ref", "Q_ref", "Idc_ref"], output_names=["p_e", "q_e", "w_pll", "V_t"], )
[docs] @lru_cache(maxsize=16) def gfl_nonlinear_funcs(modes: tuple[tuple[str, str], ...] = ()) -> NonlinearFuncs: return gfl_dae(modes).nonlinear_funcs()
[docs] @lru_cache(maxsize=16) def gfl_nonlinear_jacobians(modes: tuple[tuple[str, str], ...] = ()) -> NonlinearJacobians: return gfl_dae(modes).nonlinear_jacobians()
[docs] class GflOperatingPoint(RebuiltWithParams): """Mirrors ``GFL_subs.m``. Eg0/Ig0/Is0/Vm0 physics are identical to :class:`g2elin_core.components.gfm.GfmOperatingPoint` (same LCL-filter converter structure) — see there for the derivation. ``M_d0``/``M_q0``/ ``M_CLd0``/``M_CLq0``/``M_pll0`` are derived from the inner-loop integrators' own equilibrium conditions (dM_x = 0 at the operating point), since ``GFL_subs.m`` reads them from data columns this port doesn't have an upstream initializer for; the resulting formulas match GFM's structurally-identical ones exactly, as a cross-check. """ def __init__( self, *, params: dict, v_terminal_pu: float, angle_terminal_rad: float, p_terminal_pu: float, q_terminal_pu: float, v_grid_pu: float, angle_grid_rad: float, theta_g_rad: float, ): # locals() here, before anything else runs, is exactly the # arguments -- see RebuiltWithParams. self._built_from = {k: v for k, v in locals().items() if k != "self"} self.p = params Rf_, Lf_ = params["Rf"], params["Lf"] theta0 = angle_terminal_rad # theta_pll_0: PLL locks to the bus's own angle Eg0 = v_terminal_pu * cmath.exp(1j * theta0) Ig0 = (p_terminal_pu - 1j * q_terminal_pu) / Eg0.conjugate() Ic0 = Eg0 * 1j * params["Cf"] Is0 = Ig0 + Ic0 Vm0 = Eg0 + Is0 * (Rf_ + 1j * Lf_) Eg0dq = Eg0 * cmath.exp(-1j * theta0) Ig0dq = Ig0 * cmath.exp(-1j * theta0) Is0dq = Is0 * cmath.exp(-1j * theta0) Vm0dq = Vm0 * cmath.exp(-1j * theta0) self.theta_pll0 = theta0 self.ved0, self.veq0 = Eg0dq.real, Eg0dq.imag # veq0 == 0 exactly (PLL-locked frame) self.igd0, self.igq0 = Ig0dq.real, Ig0dq.imag self.isd0, self.isq0 = Is0dq.real, Is0dq.imag vmd0, vmq0 = Vm0dq.real, Vm0dq.imag grid_v = v_grid_pu * cmath.exp(1j * angle_grid_rad) vg_local = grid_v * cmath.exp(-1j * theta0) self.vgd0, self.vgq0 = vg_local.real, vg_local.imag vg_global = grid_v * cmath.exp(-1j * theta_g_rad) self.vgd_g0, self.vgq_g0 = vg_global.real, vg_global.imag self.theta_g0 = theta_g_rad self.p_ref0 = p_terminal_pu self.q_ref0 = q_terminal_pu self.vdc_ref0 = 1.0 self.vdc0 = self.vdc_ref0 self.idc_ref0 = self.p_ref0 / self.vdc_ref0 self.idc0 = self.idc_ref0 self.w_pll0 = 1.0 self.md0, self.mq0 = vmd0 / self.vdc_ref0, vmq0 / self.vdc_ref0 Kffv_, wff_, Kppll_ = params["Kffv"], params["wff"], params["Kppll"] self.M_pll0 = -Kppll_ * self.veq0 self.M_d0 = self.isd0 self.M_q0 = self.isq0 self.M_CLd0 = vmd0 - self.ved0 * Kffv_ + self.isq0 * Lf_ * wff_ self.M_CLq0 = vmq0 - self.veq0 * Kffv_ - self.isd0 * Lf_ * wff_
def _gfl_operating_subs(op: GflOperatingPoint) -> dict: p = op.p return equilibrium_subs({ wb: p["wb"], wff: p["wff"], Rf: p["Rf"], Lf: p["Lf"], Cf: p["Cf"], Rt: p["Rt"], Lt: p["Lt"], Kpd: p["Kpd"], Kid: p["Kid"], Kiq: p["Kiq"], KpCL: p["KpCL"], KiCL: p["KiCL"], Kffv: p["Kffv"], Cdc: p["Cdc"], Gdc: p["Gdc"], Tdc: p["Tdc"], Kipll: p["Kipll"], Kppll: p["Kppll"], isd: op.isd0, isq: op.isq0, igd: op.igd0, igq: op.igq0, ved: op.ved0, veq: op.veq0, vdc: op.vdc0, idc: op.idc0, M_d: op.M_d0, M_q: op.M_q0, M_CLd: op.M_CLd0, M_CLq: op.M_CLq0, M_pll: op.M_pll0, theta_pll: op.theta_pll0, md: op.md0, mq: op.mq0, w_pll: op.w_pll0, idc_ref: op.idc_ref0, vdc_ref: op.vdc_ref0, q_ref: op.q_ref0, theta_g: op.theta_g0, vgd_g: op.vgd_g0, vgq_g: op.vgq_g0, })
[docs] def linearize_gfl(op: GflOperatingPoint, modes: Mapping[str, str] | None = None) -> LinearComponent: return gfl_dae(mode_key(modes)).linearize(_gfl_operating_subs(op))
[docs] def gfl_nonlinear_point(op: GflOperatingPoint, modes: Mapping[str, str] | None = None) -> tuple: """``(x0, z0, u0, p0)`` for :func:`gfl_nonlinear_funcs`.""" return gfl_dae(mode_key(modes)).point_from_subs(_gfl_operating_subs(op))