From 3983623fd891e402954b3f2d9f8c4fd92ff8d977 Mon Sep 17 00:00:00 2001 From: Zilin Zhuang Date: Fri, 6 Mar 2026 15:17:49 -0500 Subject: [PATCH 1/6] Save work for REGCM --- andes/cases/smib/regfmc1_smib_with_plant.xlsx | Bin 0 -> 7855 bytes andes/models/__init__.py | 95 +- andes/models/renewable/__init__.py | 2 + andes/models/renewable/regfmc1.py | 998 ++++++++++++++++++ andes/models/renewable/repcgfmc1.py | 970 +++++++++++++++++ 5 files changed, 2018 insertions(+), 47 deletions(-) create mode 100644 andes/cases/smib/regfmc1_smib_with_plant.xlsx create mode 100644 andes/models/renewable/regfmc1.py create mode 100644 andes/models/renewable/repcgfmc1.py diff --git a/andes/cases/smib/regfmc1_smib_with_plant.xlsx b/andes/cases/smib/regfmc1_smib_with_plant.xlsx new file mode 100644 index 0000000000000000000000000000000000000000..85e251a0d71457714104120ca4a2bebdc2c897ff GIT binary patch literal 7855 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 literal 0 HcmV?d00001 diff --git a/andes/models/__init__.py b/andes/models/__init__.py index 094e939b9..2bf180e69 100644 --- a/andes/models/__init__.py +++ b/andes/models/__init__.py @@ -1,47 +1,48 @@ -""" -The package for DAE models in ANDES. -""" - -# Notes: -# - `timer`s are moved to the beginning for initialization. -# Connectivity statuses should be restored before initializing the rest. - -# `file_classes` records the `.py` files under `andes/models` and the classes in -# each file. Models will be initialized in the order given below. - -file_classes = list([ - ('info', ['Summary']), - ('misc', ['Output']), - ('timer', ['Toggle', 'Fault', 'Alter']), - ('timeseries', ['TimeSeries']), - ('bus', ['Bus']), - ('static', ['PQ', 'PV', 'Slack']), - ('shunt', ['Shunt', "ShuntTD", 'ShuntSw']), - ('interface', ['Fortescue']), - ('line', ['Line', 'Jumper']), - ('area', ['Area', 'ACE', 'ACEc']), - ('dynload', ['ZIP', 'FLoad']), - ('synchronous', ['GENCLS', 'GENROU', 'PLBVFU1']), - ('governor', ['TG2', 'TGOV1', 'TGOV1DB', 'TGOV1N', 'TGOV1NDB', - 'IEEEG1', 'IEESGO', 'GAST', 'HYGOV', 'HYGOVDB', 'HYGOV4']), - ('vcomp', ['IEEEVC']), - ('exciter', ['EXDC2', 'IEEEX1', 'ESDC1A', 'ESDC2A', 'EXST1', 'ESST3A', 'SEXS', - 'IEEET1', 'EXAC1', 'EXAC2', 'EXAC4', 'ESST4B', 'AC8B', 'IEEET3', - 'ESAC1A', 'ESST1A', 'ESAC5A']), - ('pss', ['IEEEST', 'ST2CUT']), - ('motor', ['Motor3', 'Motor5']), - ('measurement', ['BusFreq', 'BusROCOF', 'PMU', 'PLL1', 'PLL2']), - ('dc', ['Node', 'Ground', 'R', 'L', 'C', 'RCp', 'RCs', 'RLs', 'RLCs', 'RLCp']), - ('acdc', ['VSCShunt']), - ('renewable', ['REGCA1', 'REGCP1']), - ('renewable', ['REECA1', 'REECA1E', 'REECA1G']), - ('renewable', ['REPCA1']), - ('renewable', ['WTDTA1', 'WTDS', 'WTARA1', 'WTPTA1', 'WTTQA1', 'WTARV1', - 'REGCV1', 'REGCV2', 'REGF1', 'REGF2', 'REGF3']), - ('distributed', ['PVD1', 'ESD1', 'EV1', 'EV2', 'DGPRCT1', 'DGPRCTExt']), - ('coi', ['COI']), - # ('experimental', ['PI2', 'TestDB1', 'TestPI', 'TestLagAWFreeze', 'FixedGen']), -]) - - -model_aliases = {"REGCVSG": "REGCV1", "REGCVSG2": "REGCV2", "Toggler": "Toggle"} +""" +The package for DAE models in ANDES. +""" + +# Notes: +# - `timer`s are moved to the beginning for initialization. +# Connectivity statuses should be restored before initializing the rest. + +# `file_classes` records the `.py` files under `andes/models` and the classes in +# each file. Models will be initialized in the order given below. + +file_classes = list([ + ('info', ['Summary']), + ('misc', ['Output']), + ('timer', ['Toggle', 'Fault', 'Alter']), + ('timeseries', ['TimeSeries']), + ('bus', ['Bus']), + ('static', ['PQ', 'PV', 'Slack']), + ('shunt', ['Shunt', "ShuntTD", 'ShuntSw']), + ('interface', ['Fortescue']), + ('line', ['Line', 'Jumper']), + ('area', ['Area', 'ACE', 'ACEc']), + ('dynload', ['ZIP', 'FLoad']), + ('synchronous', ['GENCLS', 'GENROU', 'PLBVFU1']), + ('governor', ['TG2', 'TGOV1', 'TGOV1DB', 'TGOV1N', 'TGOV1NDB', + 'IEEEG1', 'IEESGO', 'GAST', 'HYGOV', 'HYGOVDB', 'HYGOV4']), + ('vcomp', ['IEEEVC']), + ('exciter', ['EXDC2', 'IEEEX1', 'ESDC1A', 'ESDC2A', 'EXST1', 'ESST3A', 'SEXS', + 'IEEET1', 'EXAC1', 'EXAC2', 'EXAC4', 'ESST4B', 'AC8B', 'IEEET3', + 'ESAC1A', 'ESST1A', 'ESAC5A']), + ('pss', ['IEEEST', 'ST2CUT']), + ('motor', ['Motor3', 'Motor5']), + ('measurement', ['BusFreq', 'BusROCOF', 'PMU', 'PLL1', 'PLL2']), + ('dc', ['Node', 'Ground', 'R', 'L', 'C', 'RCp', 'RCs', 'RLs', 'RLCs', 'RLCp']), + ('acdc', ['VSCShunt']), + ('renewable', ['REGCA1', 'REGCP1']), + ('renewable', ['REECA1', 'REECA1E', 'REECA1G']), + ('renewable', ['REPCA1']), + ('renewable', ['WTDTA1', 'WTDS', 'WTARA1', 'WTPTA1', 'WTTQA1', 'WTARV1', + 'REGCV1', 'REGCV2', 'REGF1', 'REGF2', 'REGF3', 'REGFMC1', + 'REPCGFMC1']), + ('distributed', ['PVD1', 'ESD1', 'EV1', 'EV2', 'DGPRCT1', 'DGPRCTExt']), + ('coi', ['COI']), + # ('experimental', ['PI2', 'TestDB1', 'TestPI', 'TestLagAWFreeze', 'FixedGen']), +]) + + +model_aliases = {"REGCVSG": "REGCV1", "REGCVSG2": "REGCV2", "Toggler": "Toggle"} diff --git a/andes/models/renewable/__init__.py b/andes/models/renewable/__init__.py index 9b5adbe5f..2f3aa5817 100644 --- a/andes/models/renewable/__init__.py +++ b/andes/models/renewable/__init__.py @@ -19,3 +19,5 @@ from andes.models.renewable.regf1 import REGF1 # NOQA from andes.models.renewable.regf2 import REGF2 # NOQA from andes.models.renewable.regf3 import REGF3 # NOQA +from andes.models.renewable.regfmc1 import REGFMC1 # NOQA +from andes.models.renewable.repcgfmc1 import REPCGFMC1 # NOQA \ No newline at end of file diff --git a/andes/models/renewable/regfmc1.py b/andes/models/renewable/regfmc1.py new file mode 100644 index 000000000..3043134c6 --- /dev/null +++ b/andes/models/renewable/regfmc1.py @@ -0,0 +1,998 @@ +""" +REGFMC1 - Hybrid Grid-Forming Converter Model. + +This model implements a parallel combination of: +- Grid-forming (GFM) voltage source with series impedance +- Grid-following (GFL) current source +""" + +from andes.core import ( + Algeb, ConstService, ExtAlgeb, ExtService, IdxParam, + Lag, Model, ModelData, NumParam, State, Switcher, + Limiter +) + +from andes.core.service import NumSelect, VarService +from andes.core.block import PIController, Washout + + +class REGFMC1Data(ModelData): + """ + REGFMC1 model data. + """ + + def __init__(self): + ModelData.__init__(self) + + # --- General Parameters --- + self.bus = IdxParam(model='Bus', + info="Interface bus id", + mandatory=True, + ) + self.gen = IdxParam(info="Static generator index", + mandatory=True, + ) + self.Sn = NumParam(default=100.0, tex_name='S_n', + info='Model MVA base', + unit='MVA', + ) + self.gammap = NumParam(default=1.0, + info="P ratio of linked static gen", + tex_name=r'\gamma_P' + ) + self.gammaq = NumParam(default=1.0, + info="Q ratio of linked static gen", + tex_name=r'\gamma_Q' + ) + + # --- Circuit Parameters --- + self.Rs = NumParam(default=0.05, + info="Series resistance for GFM branch", + z=True, + tex_name='R_s' + ) + self.Xs = NumParam(default=0.2, + info="Series reactance for GFM branch", + z=True, + tex_name='X_s' + ) + + # --- GFM Voltage Control Parameters --- + self.Tvr = NumParam(default=0.025, + tex_name='T_{vr}', + info='Time constant for Vref filter', + unit='s', + ) + self.Tomegam = NumParam(default=0.3183, + tex_name=r'T_{\omega m}', + info='Time constant for omegam filter', + unit='s', + ) + + self.kq = NumParam(default=1, + tex_name='k_q', + info='Reactive power gain in voltage control', + ) + self.mq = NumParam(default=0.4, + tex_name='m_q', + info='Reactive power measurement gain', + ) + self.kpE = NumParam(default=0.333, + tex_name='k_{pE}', + info='Proportional gain for voltage magnitude error', + ) + self.kiE = NumParam(default=3.333, + tex_name='k_{iE}', + info='Integral gain for voltage magnitude error', + ) + self.Tvsm = NumParam(default=0.318, + tex_name='T_{vsm}', + info='Time constant for voltage controller output filter', + unit='s', + ) + self.dEmax = NumParam(default=0.2083, + tex_name=r'\Delta E_{max}', + info='Maximum voltage magnitude deviation (PLACEHOLDER)', + ) + self.dEmin = NumParam(default=-0.2083, + tex_name=r'\Delta E_{min}', + info='Minimum voltage magnitude deviation (PLACEHOLDER)', + ) + + # --- GFM VSM Control Parameters --- + self.fn = NumParam(default=60.0, + info="System frequency", + tex_name='f_n', + unit='Hz', + ) + self.Tomegar = NumParam(default=0.02, + tex_name=r'T_{\omega r}', + info='Time constant for omega_ref filter', + unit='s', + ) + self.TIf = NumParam(default=0.02, + tex_name=r'T_{If}', + info='Time constant for I_d and I_q filter', + unit='s', + ) + + self.mp = NumParam(default=0.041667, + tex_name='m_p', + info='Active power droop gain', + ) + self.Tomegacmd = NumParam(default=0.02, + tex_name=r'T_{\omega cmd}', + info='Time constant for omega command filter', + unit='s', + ) + self.Tfrq = NumParam(default=0.02, + tex_name=r'T_{frq}', + info='Time constant for power measurement filter', + unit='s', + ) + self.Hs = NumParam(default=1.6, + tex_name='H_s', + info='Inertia constant (2H)', + unit='s', + ) + self.D1 = NumParam(default=0, + tex_name='D_1', + info='Primary damping coefficient', + ) + self.D2 = NumParam(default=90, + tex_name='D_2', + info='Secondary damping coefficient', + ) + self.omegaD = NumParam(default=3.14, + tex_name=r'\omega_D', + info='Damping filter frequency', + unit='rad/s', + ) + self.domegamax = NumParam(default=0.1667, + tex_name=r'\Delta\omega_{max}', + info='Maximum frequency deviation (PLACEHOLDER)', + ) + self.domegamin = NumParam(default=-0.3333, + tex_name=r'\Delta\omega_{min}', + info='Minimum frequency deviation (PLACEHOLDER)', + ) + self.dPGFMmax = NumParam(default=1.36, + tex_name=r'\Delta P_{GFM,max}', + info='Maximum active power deviation for GFM (PLACEHOLDER)', + ) + self.dPGFMmin = NumParam(default=-1.36, + tex_name=r'\Delta P_{GFM,min}', + info='Minimum active power deviation for GFM (PLACEHOLDER)', + ) + self.Tpf = NumParam(default=0.02, + tex_name='T_{pf}', + info='Time constant for frequency flag filter', + unit='s', + ) + self.FFFlag = NumParam(default=0.0, + tex_name='FF_{Flag}', + info='Frequency flag (0 or 1)', + unit='bool', + ) + + # --- GFL Control Parameters --- + self.Tvf = NumParam(default=0.02, + tex_name='T_{vf}', + info='Time constant for voltage filter in GFL', + unit='s', + ) + self.kqv = NumParam(default=2, # modified + tex_name='k_{qv}', + info='Voltage error gain in GFL', + ) + self.dbVLI = NumParam(default=-0.12, + tex_name='db_{VLI}', + info='Voltage deadband lower limit (PLACEHOLDER)', + ) + self.dbVHI = NumParam(default=0.12, + tex_name='db_{VHI}', + info='Voltage deadband upper limit (PLACEHOLDER)', + ) + self.Pcmd_GFL_max = NumParam(default=0.8, + tex_name='P_{cmd,GFL,max}', + info='Maximum active power command for GFL (PLACEHOLDER)', + ) + self.Pcmd_GFL_min = NumParam(default=-0.8, + tex_name='P_{cmd,GFL,min}', + info='Minimum active power command for GFL (PLACEHOLDER)', + ) + self.Qcmd_GFL_max = NumParam(default=0.6, + tex_name='Q_{cmd,GFL,max}', + info='Maximum reactive power command for GFL (PLACEHOLDER)', + ) + self.Qcmd_GFL_min = NumParam(default=-0.6, + tex_name='Q_{cmd,GFL,min}', + info='Minimum reactive power command for GFL (PLACEHOLDER)', + ) + self.Ipmax_GFL = NumParam(default=1.2, + tex_name='I_{pmax,GFL}', + info='Maximum active current for GFL (PLACEHOLDER)', + ) + self.Ipmin_GFL = NumParam(default=-1.2, + tex_name='I_{pmin,GFL}', + info='Minimum active current for GFL (PLACEHOLDER)', + ) + self.Iqmax_GFL = NumParam(default=1.2, + tex_name='I_{qmax,GFL}', + info='Maximum reactive current for GFL (PLACEHOLDER)', + ) + self.Iqmin_GFL = NumParam(default=-1.2, + tex_name='I_{qmin,GFL}', + info='Minimum reactive current for GFL (PLACEHOLDER)', + ) + self.PQFlag = NumParam(default=1.0, + tex_name='PQ_{Flag}', + info='1=P priority, 0=Q priority', + unit='bool', + ) + + # --- Current Limiting Parameters --- + self.Imax = NumParam(default=1.2, + tex_name='I_{max}', + info='Maximum total output current', + current=True, + ) + self.Vmin = NumParam(default=0.88, + tex_name='V_{min}', + info='Minimum voltage for current limiting (PLACEHOLDER)', + ) + + +class REGFMC1Model(Model): + """ + REGFMC1 model implementation. + """ + + def __init__(self, system, config): + Model.__init__(self, system, config) + self.flags.tds = True + self.group = 'RenGen' + + # --- External References --- + + self.a = ExtAlgeb(model='Bus', + src='a', + indexer=self.bus, + tex_name=r'\theta', + info='Bus voltage angle', + e_str='-u * Pe', + ) + self.v = ExtAlgeb(model='Bus', + src='v', + indexer=self.bus, + tex_name='V', + info='Bus voltage magnitude', + e_str='-u * Qe', + ) + + self.p0s = ExtService(model='StaticGen', + src='p', + indexer=self.gen, + tex_name=r'P_{0s}', + info='Total P of the static gen', + ) + self.q0s = ExtService(model='StaticGen', + src='q', + indexer=self.gen, + tex_name=r'Q_{0s}', + info='Total Q of the static gen', + ) + + # --- Initialization Services --- + self.p0 = ConstService(v_str='gammap * p0s', + tex_name='P_0', + info='Initial P for this device', + ) + self.q0 = ConstService(v_str='gammaq * q0s', + tex_name='Q_0', + info='Initial Q for this device', + ) + + # Initial current calculations (for both branches) + self.Id0_GFL = ConstService(tex_name=r'I_{d0,GFL}', + v_str='u * p0 / v', + ) + self.Iq0_GFL = ConstService(tex_name=r'I_{q0,GFL}', + v_str='u * q0 / v', + ) + + # Damping washout time constant (1 / omegaD) + self.Tdamp = ConstService(tex_name=r'T_{damp}', + v_str='1 / omegaD', + info='Damping washout time constant', + ) + + # Voltage reference for GFL - initialized from bus voltage + self.Vref0 = ConstService(v_str='v', + tex_name='V_{ref0}', + info='Reference voltage for GFL', + ) + + # GFM branch impedance squared (for current calculation) + self.Zs2 = ConstService(v_str='Rs**2 + Xs**2', + tex_name='Z_s^2', + info='GFM series impedance magnitude squared', + ) + + # --- External reference variables (to be controlled by plant controller) --- + # GFM voltage reference (external input to voltage control) # mistake + self.Vref_GFM = Algeb(tex_name='V_{ref,GFM}', + info='Voltage reference for GFM branch (from plant controller)', + v_str='Vref0', # modified + e_str='Vref0 - Vref_GFM', # Default: maintain initial bus voltage + ) + + # GFM frequency reference (external input to VSM control) # mistake + self.fref_GFM = Algeb(tex_name='f_{ref,GFM}', + info='Frequency reference for GFM branch (from plant controller)', + v_str='1.0', + e_str='1.0 - fref_GFM', # Default: maintain nominal frequency + ) + + # --- GFM Branch: Voltage Control --- + # Voltage reference filter + self.VrefLag = Lag(u='Vref_GFM', T=self.Tvr, K=1, + info='Voltage reference filter', + name='VrefLag', + ) + + # Inverter voltage filter + self.VinvLag = Lag(u='v', T=self.Tvr, K=1, + info='Inverter voltage filter', + name='VinvLag', + ) + + + # Reactive power measurement path (per diagram: Iq_GFM filtered through 1/(Tif*s+1)) + self.Iq_VSMLag = Lag(u='Iq_VSM_lim', T=self.TIf, K=1, info='Filter for I_q GFM', name='Iq_VSMLag') + + # Voltage magnitude error - TESTING POSITIVE SIGN + # Testing: Verr = (Vref - Vinv) * kq/mq + Iq_VSMLag_y + self.Verr = Algeb(tex_name='V_{err}', + info='Voltage magnitude error', + v_str='0', + e_str='(VrefLag_y - VinvLag_y) * kq / mq + Iq_VSMLag_y - Verr', + ) + + # PI controller for voltage magnitude + self.VmagPI = PIController(u=self.Verr, + kp=self.kpE, + ki=self.kiE, + x0='0', + info='Voltage magnitude PI controller', + name='VmagPI', + ) + + + self.EVSM_initial = Algeb(tex_name='E_{VSM_initial}', # modified + info='EVSM_initial', + v_str='v', + e_str='VmagPI_y+ VrefLag_y - EVSM_initial', + ) + + # Output filter for EVSM (TODO: add limiters for dEmax, dEmin) + self.EVSMLag = Lag(u='EVSM_initial', + T=self.Tvsm, + K=1, + info='EVSM output filter', + name='EVSMLag', + ) + + # EVSM is the output of EVSMLag + self.EVSM = Algeb(tex_name='E_{VSM}', + info='GFM voltage magnitude', + v_str='v', + e_str='EVSMLag_y - EVSM', + ) + + # --- GFM Branch: VSM Control --- + # Omega reference filter + self.OmegarefLag = Lag(u='fref_GFM', + T=self.Tomegar, + K=1, + info='Omega reference filter', + name='OmegarefLag', + ) + + # Active power reference - can be controlled by plant controller + self.Pref_GFM = Algeb(v_str='0', + e_str='0 - Pref_GFM', # Defaults to 0, can be overridden externally + tex_name='P_{ref,GFM}', + info='Active power reference for GFM', + ) + + # TODO: USE `fref_GFM` as the input terminal for frequency reference + + # # Plant controller changes omega_ref (PLACEHOLDER - use constant 1 for now) + # self.omega_ref = Algeb(tex_name=r'\omega_{ref}', + # info='Omega reference', + # v_str='1.0', + # e_str='OmegarefLag_y - omega_ref', + # ) + + # GFM branch power measurement (for droop feedback) + self.Pmv_GFM = Lag(u='PGFM', + T=self.Tfrq, + K=1, + info='Measured GFM power for droop control', + name='Pmv_GFM', + ) + + # Frequency droop: converts frequency error to power command + # dP_GFM_droop = (omegarefLag_y - omegamLag_y) / mp + self.dP_GFM_droop = Algeb(tex_name=r'\Delta P_{GFM,droop}', + info='Power command from frequency droop', + v_str='0', + e_str='(OmegarefLag_y - omegamLag_y) / mp - dP_GFM_droop', + ) + + + # TODO: CONSIDER `FFlag` to allow turning off the droop control + # FFlag = 0: `dP_GFM_droop` = 0 -- this is currently missing + + # Power command for GFM branch + # Pcmd_GFM = Pref_GFM + dP_GFM_droop (reference + droop correction) + self.Pcmd_GFM = Algeb(tex_name='P_{cmd,GFM}', + info='Power command for GFM branch', + v_str='Pref_GFM', + e_str='Pref_GFM + dP_GFM_droop - Pcmd_GFM', + ) + + # Damping filter: sD2/(s+omegaD) using Washout + # Washout implements sK/(1+sT), so we need K=D2, T=1/omegaD + self.domegam = Algeb( # modified-11.6 + name='domegam', + tex_name=r'\Delta\omega', + info='Frequency deviation (pu)', + v_str='0.0', + e_str='omegam - 1.0 - domegam', + ) + + self.DampWash = Washout(u='domegam', + T=self.Tdamp, + K=self.D2, # should be self.D2* self.Tdamp??-11.6 + info='Damping washout filter', + name='DampWash', + ) + + # Inverter active power for GFM - measured at voltage source (for swing equation) + self.Pinv_GFM = Algeb(tex_name='P_{inv,GFM}', + info='GFM inverter active power at voltage source', + v_str='0', + e_str='(EVSM * cos(dVSM - a) * Id_VSM_lim + EVSM * sin(dVSM - a) * Iq_VSM_lim) - Pinv_GFM', + ) + # self.Pinv_GFMLag = Lag(u='Pinv_GFM', # modified-11.6 + # T=self.Tpf, + # K=1, + # info='GFM VSM PGFM filter', + # name='Pinv_GFMLag', + # ) + + # Virtual machine angular frequency (swing equation) + # Power balance: 2*Hs*d(Δω)/dt = P_cmd - P_inv - D1*Δω - D2*d(Δω)/dt + # Since omegam is absolute frequency: Δω = omegam - 1.0 + # Equation: d(omegam)/dt = (Pcmd_GFM - Pinv_GFM - D1*(omegam-1) - DampWash_y) / (2*Hs) + self.omegam = State( + info='Virtual machine angular frequency (pu)', + tex_name=r'\omega_m', + v_str='1.0', + e_str='(Pcmd_GFM - Pinv_GFM - D1 * domegam - DampWash_y) / 2', + t_const=self.Hs, + ) + + self.omegamLag = Lag(u='omegam', T=self.Tomegam, K=1, info='Filter for omegam', name='omegamLag') + + # Virtual synchronous machine angle (integration of omega deviation) + self.dVSM = State( + info='Virtual synchronous machine angle', + tex_name=r'\delta_{VSM}', + v_str='a', + e_str='2 * pi * fn * (omegam - 1.0)', + ) + + # --- GFL Branch: Control --- + # Voltage filter for GFL + self.VinvGFLLag = Lag(u='v', + T=self.Tvf, + K=1, + info='GFL voltage filter', + name='VinvGFLLag', + ) # same as VinvLag_y + + # Voltage error for GFL (TODO: add deadband) + self.Verr_GFL = Algeb(tex_name='V_{err,GFL}', + info='Voltage error for GFL', + v_str='Vref0 - v', + e_str='Vref0 - VinvGFLLag_y - Verr_GFL', # Vref0 or VrefLag_y? shouldbe Vref0! + ) + + # Active and reactive power commands (controlled by plant controller) + # Default equations lock to p0/q0, but can be overridden externally + self.Pcmd_GFL = Algeb(tex_name='P_{cmd,GFL}', + info='Active power command for GFL', + v_str='p0', + e_str='p0 - Pcmd_GFL', # Defaults to p0, can be overridden externally + ) + + self.Qcmd_GFL = Algeb(tex_name='Q_{cmd,GFL}', + info='Reactive power command for GFL', + v_str='q0', + e_str='q0 - Qcmd_GFL', # Defaults to q0, can be overridden externally + ) + + # Current commands (PLACEHOLDER - TODO: add limiters and PQ priority) + self.Ipcmd_GFL = Algeb(tex_name='I_{pcmd,GFL}', + info='Active current command for GFL', + v_str='Id0_GFL', + e_str='Pcmd_GFL / v - Ipcmd_GFL', + ) + + self.Iqcmd_GFL = Algeb(tex_name='I_{qcmd,GFL}', + info='Reactive current command for GFL', + v_str='kqv * (Vref0 - v) + Iq0_GFL', + e_str='kqv * Verr_GFL + Qcmd_GFL / v - Iqcmd_GFL', + ) + + # GFL PQ Priority Current Limiting + + # dynamic max,min boundary (PQsel choice) + # a new version of PQ priority current limiting algorithm + # choose priority 1=P ,0=Q + + + # self.PQsel = VarService(v_str='Indicator(PQFlag >= 0.5)', tex_name='PQ_{sel}', + # info='1=P priority, 0=Q priority') + + # get Ipmax_GFL, Ipmin_GFL, Iqmax_GFL, Iqmin_GFL + self.Ipmax_GFL1 = VarService(v_str='PQFlag * Imax + (1-PQFlag) * ( sqrt(0.5*((Imax**2 - Iqcmd_sat_val**2) + Abs(Imax**2 - Iqcmd_sat_val**2))) ) ', + tex_name='I_{p,max,GFL}', + info='Ipmax_GFL for Current Limiting Algorithm') + + self.Ipmin_GFL1 = VarService(v_str='-(Ipmax_GFL1)', tex_name='I_{p,min,GFL}', + info='Ipmin_GFL for Current Limiting Algorithm') + + + self.Iqmax_GFL1 = VarService(v_str='(1-PQFlag) * Imax + (PQFlag) * ( sqrt(0.5*((Imax**2 - Ipcmd_sat_val**2) + Abs(Imax**2 - Ipcmd_sat_val**2))) )', + tex_name='I_{q,max,GFL}', + info='Iqmax_GFL for Current Limiting Algorithm') + + self.Iqmin_GFL1 = VarService(v_str='-(Iqmax_GFL1)', tex_name='I_{q,min,GFL}', + info='Iqmin_GFL for Current Limiting Algorith') + + + + self.Ipmax_GFL1_out = Algeb(v_str='Ipmax_GFL1', + e_str='Ipmax_GFL1 - Ipmax_GFL1_out', + tex_name='I_{p,max,GFL}^{out}', info='Ipmax_GFL for Current Limiting Algorithm(Algeb) ') + + + self.Ipmin_GFL1_out = Algeb(v_str='Ipmin_GFL1', + e_str='Ipmin_GFL1 - Ipmin_GFL1_out', + tex_name='I_{p,min,GFL}^{out}', info='Ipmin_GFL for Current Limiting Algorithm(Algeb)') + + self.Iqmax_GFL1_out = Algeb(v_str='Iqmax_GFL1', + e_str='Iqmax_GFL1 - Iqmax_GFL1_out', + tex_name='I_{q,max,GFL}^{out}', info='Iqmax_GFL for Current Limiting Algorithm(Algeb)') + + self.Iqmin_GFL1_out = Algeb(v_str='Iqmin_GFL1', + e_str='Iqmin_GFL1 - Iqmin_GFL1_out', + tex_name='I_{q,min,GFL}^{out}', info='Iqmin_GFL for Current Limiting Algorithm(Algeb)') + + + # Ipmax_GFL, Ipmin_GFL, Iqmax_GFL, Iqmin_GFL (Algeb) + + + + # self.Ipmax_GFL1 = Algeb( + # tex_name='I_{p,max,GFL}', + # info='Ipmax_GFL for Current Limiting Algorithm', + # v_str='PQFlag*Imax + (1-PQFlag)*sqrt( Imax**2 - (kqv * (Vref0 - v) + Iq0_GFL)**2)', + # e_str='(PQFlag*Imax + (1-PQFlag)*sqrt(0.5*((Imax**2 - Iqcmd_sat_val**2) + Abs(Imax**2 - Iqcmd_sat_val**2)))) - Ipmax_GFL1', + # ) + + + # self.Ipmin_GFL1 = Algeb( + # tex_name='I_{p,min,GFL}', + # info='Ipmin_GFL for Current Limiting Algorithm ', + # v_str='-PQFlag*Imax - (1-PQFlag)*sqrt( Imax**2 - (kqv * (Vref0 - v) + Iq0_GFL)**2)', + # e_str='(-Ipmax_GFL1) - Ipmin_GFL1', + # ) + + # self.Iqmax_GFL1 = Algeb( + # tex_name='I_{q,max,GFL}', + # info='Iqmax_GFL for Current Limiting Algorithm', + # v_str='(1-PQFlag)*Imax + (PQFlag)*sqrt( Imax**2 - Id0_GFL**2)', + # e_str='((1-PQFlag)*Imax + PQFlag*sqrt(0.5*((Imax**2 - Ipcmd_sat_val**2) + Abs(Imax**2 - Ipcmd_sat_val**2)))) - Iqmax_GFL1', + # ) + + # self.Iqmin_GFL1 = Algeb( + # tex_name='I_{q,min,GFL}', + # info='Iqmin_GFL for Current Limiting Algorithm', + # v_str='-(1-PQFlag)*Imax - (PQFlag)*sqrt( Imax**2 - Id0_GFL**2)', + # e_str='(-Iqmax_GFL1) - Iqmin_GFL1', + # ) + + + + + + + + self.Ipcmd_sat = Limiter(u=self.Ipcmd_GFL, lower=self.Ipmin_GFL1, upper=self.Ipmax_GFL1, name='Ipcmd_sat') + self.Iqcmd_sat = Limiter(u=self.Iqcmd_GFL, lower=self.Iqmin_GFL1, upper=self.Iqmax_GFL1, name='Iqcmd_sat') + + # get the Ip_GFL and Iq_GFL + self.Ipcmd_sat_val = Algeb(tex_name='Ipcmd_{sat,val}', + info='Limited Ipcmd', + v_str='Id0_GFL', + e_str='Ipcmd_GFL * Ipcmd_sat_zi + Ipmax_GFL1 * Ipcmd_sat_zu + Ipmin_GFL1 * Ipcmd_sat_zl - Ipcmd_sat_val', + ) + + self.Iqcmd_sat_val = Algeb(tex_name='Iqcmd_{sat,val}', + info='Limited Iqcmd', + v_str='kqv * (Vref0 - v) + Iq0_GFL', + e_str='Iqcmd_GFL * Iqcmd_sat_zi + Iqmax_GFL1 * Iqcmd_sat_zu + Iqmin_GFL1 * Iqcmd_sat_zl - Iqcmd_sat_val', + ) + + # # choose priority 1=P ,0=Q + # self.PQsel = VarService(v_str='Indicator(PQFlag >= 0.5)', tex_name='PQ_{sel}', + # info='1=P priority, 0=Q priority') + # # A:P prior, first limit Ip,then Iq + # self.Irem_p = VarService( + # v_str='sqrt( (Imax**2 - Ipcmd_sat_val**2) * Indicator(Imax >= abs(Ipcmd_sat_val)) )', + # tex_name='I_{rem}^{(P)}', + # info='Remaining current radius after Ip saturation' + # ) + + # # step1:Irem_p boundary + # self.Iq_step1_p = VarService( + # v_str='Iqcmd_sat_val * Indicator(abs(Iqcmd_sat_val) <= Irem_p) + ' + # 'Irem_p * Indicator(Iqcmd_sat_val > Irem_p) - ' + # 'Irem_p * Indicator(Iqcmd_sat_val < -Irem_p)', + # tex_name='I_{q,step1}^{(P)}', + # info='Iq after Irem bounds' + # ) + + # # step2:apply Iqmin/Iqmax boundary + # self.Iq_lim_p = Algeb( + # name='Iq_lim_p', v_str='(kqv * (Vref0 - v) + Iq0_GFL) ', + # e_str='Iq_step1_p * Indicator((Iq_step1_p >= Iqmin_GFL) & (Iq_step1_p <= Iqmax_GFL)) + ' + # 'Iqmax_GFL * Indicator(Iq_step1_p > Iqmax_GFL) + ' + # 'Iqmin_GFL * Indicator(Iq_step1_p < Iqmin_GFL) - Iq_lim_p', + # tex_name='I_{q}^{(P)}', info='Iq limited with P priority' + # ) + + # # B:Q prior, first limit Iq,then Ip + # self.Irem_q = VarService( + # v_str='sqrt( (Imax**2 - Iqcmd_sat_val**2) * Indicator(Imax >= abs(Iqcmd_sat_val)) )', + # tex_name='I_{rem}^{(Q)}', + # info='Remaining current radius after Iq saturation' + # ) + + # # step1: apply Irem_q + # self.Ip_step1_q = VarService( + # v_str='Ipcmd_sat_val * Indicator(abs(Ipcmd_sat_val) <= Irem_q) + ' + # 'Irem_q * Indicator(Ipcmd_sat_val > Irem_q) - ' + # 'Irem_q * Indicator(Ipcmd_sat_val < -Irem_q)', + # tex_name='I_{p,step1}^{(Q)}', + # info='Ip after Irem bounds' + # ) + + # # step2:apply Ipmin/Ipmax boundary + # self.Ip_lim_q = Algeb( + # name='Ip_lim_q', v_str='Id0_GFL', + # e_str='Ip_step1_q * Indicator((Ip_step1_q >= Ipmin_GFL) & (Ip_step1_q <= Ipmax_GFL)) + ' + # 'Ipmax_GFL * Indicator(Ip_step1_q > Ipmax_GFL) + ' + # 'Ipmin_GFL * Indicator(Ip_step1_q < Ipmin_GFL) - Ip_lim_q', + # tex_name='I_{p}^{(Q)}', info='Ip limited with Q priority' + # ) + + + # # Current outputs (PLACEHOLDER - limiting yet) + # # P prior + # self.Iq_upper_p = VarService( + # v_str='Iqmax_GFL * Indicator(Iqmax_GFL <= Irem_p) + ' + # 'Irem_p * Indicator(Iqmax_GFL > Irem_p)', + # tex_name=r'\overline{I_q}^{(P)}', + # info='Upper bound of Iq for P priority' + # ) + # self.Iq_lower_p = VarService( + # v_str='Iqmin_GFL * Indicator(Iqmin_GFL >= -Irem_p) + ' + # '(-Irem_p) * Indicator(Iqmin_GFL < -Irem_p)', + # tex_name=r'\underline{I_q}^{(P)}', + # info='Lower bound of Iq for P priority' + # ) + + # # q prior + # self.Ip_upper_q = VarService( + # v_str='Ipmax_GFL * Indicator(Ipmax_GFL <= Irem_q) + ' + # 'Irem_q * Indicator(Ipmax_GFL > Irem_q)', + # tex_name=r'\overline{I_p}^{(Q)}', + # info='Upper bound of Ip for Q priority' + # ) + # self.Ip_lower_q = VarService( + # v_str='Ipmin_GFL * Indicator(Ipmin_GFL >= -Irem_q) + ' + # '(-Irem_q) * Indicator(Ipmin_GFL < -Irem_q)', + # tex_name=r'\underline{I_p}^{(Q)}', + # info='Lower bound of Ip for Q priority' + # ) + + # # final Ip, Iq for gfm gfl + # self.Ip_GFL = Algeb(tex_name='I_{p,GFL}', + # info='Active current output for GFL', + # v_str='Id0_GFL', + # e_str='PQsel * Ipcmd_sat_val + (1 - PQsel) * Ip_lim_q - Ip_GFL', + # ) + + # self.Iq_GFL = Algeb(tex_name='I_{q,GFL}', + # info='Reactive current output for GFL', + # v_str='kqv * (Vref0 - v) + Iq0_GFL', + # e_str='PQsel * Iq_lim_p + (1 - PQsel) * Iqcmd_sat_val - Iq_GFL', + # ) + + + + + # --- Current Calculation (PLACEHOLDER - simplified) --- + # GFM branch current magnitude (simplified) + self.IVSM_mag = Algeb(tex_name='I_{VSM,mag}', + info='GFM branch current magnitude (PLACEHOLDER)', + v_str='1e-8', + e_str='sqrt(Id_VSM_lim**2 + Iq_VSM_lim**2+1e-8) - IVSM_mag', # To be calculated + ) + + # GFM branch current angle (simplified) + self.IVSM_ang = Algeb(tex_name=r'\phi_{VSM}', + info='GFM branch current angle (PLACEHOLDER)', + v_str='a', + e_str='dVSM + atan2(Iq_VSM_lim, Id_VSM_lim+1e-8) - IVSM_ang', # To be calculated + ) + + # GFL branch current magnitude + # ---------- dq -> xy(αβ) rotation for GFL current ---------- + self.deltaV = VarService(v_str='a', tex_name=r'\delta_V', info='dq to xy rotation angle') # relative angle? + + self.Ialpha_GFL = Algeb( + name='Ialpha_GFL', v_str='Id0_GFL* cos(a) + ( kqv * (Vref0 - v) + Iq0_GFL )* sin(a) ', + e_str='Ipcmd_sat_val * cos(deltaV) + Iqcmd_sat_val * sin(deltaV) - Ialpha_GFL', + tex_name='I_{\alpha,GFL}', info='GFL current alpha' + ) + self.Ibeta_GFL = Algeb( + name='Ibeta_GFL', v_str='Id0_GFL * sin(a) - ( kqv * (Vref0 - v) + Iq0_GFL )* cos(a) ', + e_str='Ipcmd_sat_val * sin(deltaV) - Iqcmd_sat_val * cos(deltaV) - Ibeta_GFL', + tex_name='I_{\beta,GFL}', info='GFL current beta' + ) + self.phi_gfl = Algeb( + name='phi_gfl', v_str='atan2(Id0_GFL * sin(a) - ( kqv * (Vref0 - v) + Iq0_GFL )* cos(a) , Id0_GFL* cos(a) + ( kqv * (Vref0 - v) + Iq0_GFL )* sin(a))', + e_str='atan2(Ibeta_GFL, Ialpha_GFL) - phi_gfl', # might have problem! + tex_name=r'\phi_{GFL}', info='GFL current angle in xy' + ) + + self.IGFL_mag = Algeb(tex_name='I_{GFL,mag}', + info='GFL branch current magnitude', + v_str='sqrt(Id0_GFL**2 + (kqv * (Vref0 - v) + Iq0_GFL)**2)', + e_str='sqrt(Ip_GFL_lim**2 + Iq_GFL_lim**2) - IGFL_mag', + ) + + # Total current magnitude (PLACEHOLDER - vector sum needed) + self.Itotal = Algeb(tex_name='I_{total}', + info='Total current magnitude (PLACEHOLDER)', + v_str='sqrt(Id0_GFL**2 + (kqv * (Vref0 - v) + Iq0_GFL)**2)', + e_str='sqrt((Id_VSM + Ipcmd_sat_val)**2 + (Iq_VSM + Iqcmd_sat_val)**2 + 1e-8) - Itotal', # Simplified, should be vector sum + ) + + self.k_scale = VarService( + v_str='1.0 + Indicator(Itotal > Imax) * (Itotal / (Imax + 1e-8) - 1.0)', + tex_name='k', + info='Scaling factor (>=1) for total current limiting' + ) + + + self.k_scale_out = Algeb(tex_name='k_{scale}', + info='Scaling factor (>=1) for total current limiting(output)', + v_str='k_scale', + e_str='k_scale - k_scale_out', + ) + + # --- Limited branch currents (both branches divided by k_scale) --- + + + self.Id_VSM_lim = Algeb( + name='Id_VSM_lim', v_str='0.0', + e_str='Id_VSM / (k_scale+1e-8) - Id_VSM_lim', + tex_name='I_{d,VSM}^{lim}', info='Limited GFM d-axis current' + ) + self.Iq_VSM_lim = Algeb( + name='Iq_VSM_lim', v_str='0.0', + e_str='Iq_VSM / (k_scale+1e-8) - Iq_VSM_lim', + tex_name='I_{q,VSM}^{lim}', info='Limited GFM q-axis current' + ) + + self.Ip_GFL_lim = Algeb( + name='Ip_GFL_lim', v_str='Id0_GFL', + e_str='Ipcmd_sat_val / (k_scale+1e-8) - Ip_GFL_lim', + tex_name='I_{p,GFL}^{lim}', info='Limited GFL p-axis current' + ) + self.Iq_GFL_lim = Algeb( + name='Iq_GFL_lim', v_str='kqv * (Vref0 - v) + Iq0_GFL', + e_str='Iqcmd_sat_val / (k_scale+1e-8) - Iq_GFL_lim', + tex_name='I_{q,GFL}^{lim}', info='Limited GFL q-axis current' + ) + + # total limit current + self.Itotal_lim = Algeb( + name='Itotal_lim', v_str='sqrt(Id0_GFL**2+(kqv * (Vref0 - v) + Iq0_GFL)**2 + 1e-8)', + e_str='sqrt((Id_VSM_lim + Ip_GFL_lim)**2 + (Iq_VSM_lim + Iq_GFL_lim)**2+1e-8) - Itotal_lim', + tex_name='I_{total}^{lim}', info='Total output current after limiting' + ) + # Scaling factor for current limiting (PLACEHOLDER) + # self.k_factor = Algeb(tex_name='k_{factor}', + # info='Current scaling factor (PLACEHOLDER)', + # v_str='1.0', + # e_str='1.0 - k_factor', # No limiting initially + # ) + + # self.k_factor = Algeb( + # tex_name='k_{factor}', + # info='Current scaling factor (PLACEHOLDER)', + # v_str='Itotal/Imax', + # e_str='Itotal/Imax - k_factor', + # ) + + + + # --- Power Calculations --- + # GFM branch current in dq-frame + # Voltage source EVSM∠dVSM behind impedance Rs+jXs to bus V∠a + # In dq-frame (d-axis aligned with bus voltage V∠a): whe + # delta = dVSM - a (angle difference) + # Ed_VSM = EVSM * cos(delta), Eq_VSM = EVSM * sin(delta) + # Id_VSM = ((Ed_VSM - v)*Rs + Eq_VSM*Xs) / Zs2 + # Iq_VSM = (Eq_VSM*Rs - (Ed_VSM - v)*Xs) / Zs2 + + self.Ed_VSM = Algeb(tex_name='E_{d,VSM}', + info='GFM d-axis voltage', + v_str='v', + e_str='EVSM * cos(dVSM - a) - Ed_VSM', + ) + + self.Eq_VSM = Algeb(tex_name='E_{q,VSM}', + info='GFM q-axis voltage', + v_str='0', + e_str='EVSM * sin(dVSM - a) - Eq_VSM', + ) + + self.Id_VSM = Algeb(tex_name='I_{d,VSM}', + info='GFM d-axis current', + v_str='0', + e_str='((EVSM * cos(dVSM - a) - v) * Rs + EVSM * sin(dVSM - a) * Xs) / Zs2 - Id_VSM', + ) + + # should be negative? + self.Iq_VSM = Algeb(tex_name='I_{q,VSM}', + info='GFM q-axis current', + v_str='0', + e_str='(EVSM * sin(dVSM - a) * Rs - (EVSM * cos(dVSM - a) - v) * Xs) / Zs2 - Iq_VSM', + ) + + # self.Id_VSM = Algeb(tex_name='I_{d,VSM}', + # info='GFM d-axis current', + # v_str='0', + # e_str='( (EVSM- v * cos(dVSM - a)) * Rs + v * sin(dVSM - a) * Xs )/ Zs2 - Id_VSM', + # ) + + # self.Iq_VSM = Algeb(tex_name='I_{q,VSM}', + # info='GFM q-axis current', + # v_str='0', + # e_str='( v * sin(dVSM - a) * Rs - (EVSM - v * cos(dVSM - a)) * Xs ) / Zs2 - Iq_VSM', + # ) + + + # GFM branch power at bus terminals (for bus injection) + # In dq frame aligned with bus: Vd=v, Vq=0 + # P = Vd*Id + Vq*Iq = v*Id_VSM + # Q = Vq*Id - Vd*Iq = -v*Iq_VSM (negative sign for generator convention) + + self.Vd = VarService(v_str='v*cos(a)') + self.Vq = VarService(v_str='v*sin(a)') + + self.PGFM = Algeb(tex_name='P_{GFM}', + info='GFM branch active power at bus', + v_str='0', + e_str='v * Id_VSM_lim - PGFM', + ) + + self.QGFM = Algeb(tex_name='Q_{GFM}', + info='GFM branch reactive power at bus', + v_str='0', + e_str='v * Iq_VSM_lim - QGFM', + ) + # self.PGFM = Algeb(tex_name='P_{GFM}', + # info='GFM branch active power at bus', + # v_str='0', + # e_str='Ed_VSM * Id_VSM_lim + Eq_VSM * Iq_VSM_lim - PGFM', + # ) + + # self.QGFM = Algeb(tex_name='Q_{GFM}', + # info='GFM branch reactive power at bus', + # v_str='0', + # e_str='-Ed_VSM * Iq_VSM_lim + Eq_VSM * Id_VSM_lim - QGFM', # -Ed_VSM * Iq_VSM+ Eq_VSM*Id_VSM - QGFM + # ) + + # GFL branch power + + # self.PGFL = Algeb(tex_name='P_{GFL}', + # info='GFL branch active power', + # v_str='v*cos(a)*(Id0_GFL) + v*sin(a)*(kqv * (Vref0 - v) + Iq0_GFL)', + # e_str='Vd * Ip_GFL_lim + Vq *Iq_GFL_lim - PGFL', + # ) + + # self.QGFL = Algeb(tex_name='Q_{GFL}', + # info='GFL branch reactive power', + # v_str='v*sin(a)*(Id0_GFL)- v*cos(a)*(kqv * (Vref0 - v) + Iq0_GFL)', + # e_str='Vq * Ip_GFL_lim - Vd *Iq_GFL_lim - QGFL', + # ) + self.PGFL = Algeb(tex_name='P_{GFL}', + info='GFL branch active power', + v_str='v* Id0_GFL', + e_str='v * Ip_GFL_lim - PGFL', + ) + + self.QGFL = Algeb(tex_name='Q_{GFL}', + info='GFL branch reactive power', + v_str='v*(kqv * (Vref0 - v) + Iq0_GFL)', + e_str='v *Iq_GFL_lim - QGFL', + ) + + + # Total power injection + self.Pe = Algeb(tex_name='P_e', + info='Total active power injection', + v_str='p0', + e_str='PGFM + PGFL - Pe', + ) + + self.Qe = Algeb(tex_name='Q_e', + info='Total reactive power injection', + v_str='q0', + e_str='QGFM + QGFL - Qe', + ) + + def v_numeric(self, **kwargs): + """ + Disable the corresponding StaticGen. + """ + self.system.groups['StaticGen'].set(src='u', idx=self.gen.v, attr='v', value=0) + + +class REGFMC1(REGFMC1Data, REGFMC1Model): + """ + Hybrid Grid-Forming Converter Model (REGFMC1). + + This model represents a parallel combination of: + - Grid-forming (GFM) voltage source with series impedance and VSM control + - Grid-following (GFL) current source + + Notes + ----- + - Current implementation has PLACEHOLDER sections for: + - Voltage and frequency limiters + - Current limiters with PQ priority + - Deadband for GFL voltage error + - Complete current limiting logic + - Full GFM branch current calculations + + - Initialization: + - GFM branch: Pref_GFM = 0, reactive power = 0 + - GFL branch: Pcmd_GFL = p0, Qcmd_GFL = q0 + - EVSM = V, dVSM = bus angle + """ + + def __init__(self, system, config): + REGFMC1Data.__init__(self) + REGFMC1Model.__init__(self, system, config) diff --git a/andes/models/renewable/repcgfmc1.py b/andes/models/renewable/repcgfmc1.py new file mode 100644 index 000000000..14bf2e225 --- /dev/null +++ b/andes/models/renewable/repcgfmc1.py @@ -0,0 +1,970 @@ +""" +REPCGFMC1 - Plant Controller for REGFMC1 (Hybrid Grid-Forming Converter). + +This model implements the plant controller that provides reference signals to REGFMC1: +- GFM Branch: Voltage reference (Vref_GFM) and frequency reference (fref_GFM) +- GFL Branch: Active power command (Pcmd_GFL) and reactive power command (Qcmd_GFL) +""" + +from collections import OrderedDict + +from andes.core import (Algeb, ConstService, ExtAlgeb, ExtParam, ExtService, + IdxParam, Lag, Limiter, Model, ModelData, NumParam, + Piecewise, State, Switcher) +from andes.core.block import DeadBand1, GainLimiter, IntegratorAntiWindup, PIController, Washout +from andes.core.service import NumSelect, VarService + + +class REPCGFMC1Data(ModelData): + """ + REPCGFMC1 plant controller data. + """ + + def __init__(self): + ModelData.__init__(self) + + self.reg = IdxParam(model='RenGen', + info='REGFMC1 device idx', + mandatory=True, + ) + + self.busr = IdxParam(model='Bus', + info='Optional remote bus for measurements', + default=None, + ) + + self.busf = IdxParam(model='BusFreq', + info='Optional BusFreq device (if None, auto-created)', + default=None, + ) + self.busrocof = IdxParam(model='BusROCOF', + info='Optional BusROCOF device (if None, auto-created)', + default=None, + ) + + # --- Site Measurement Parameters --- + self.Tmeas = NumParam(default=0.02, + tex_name='T_{meas}', + info='Site voltage measurement time constant', + unit='s', + ) + + self.Tfrq = NumParam(default=0.02, + tex_name='T_{frq}', + info='Site frequency measurement time constant', + unit='s', + ) + + # --- GFM Frequency Reference Parameters --- + self.frmax = NumParam(default=1.05, + tex_name='f_{rmax}', + info='Maximum frequency reference', + unit='p.u.', + ) + + self.frmin = NumParam(default=0.95, + tex_name='f_{rmin}', + info='Minimum frequency reference', + unit='p.u.', + ) + + self.Vfth = NumParam(default=0, + tex_name='V_{fth}', + info='Voltage threshold for frequency reference switching', + unit='p.u.', + ) + + self.Tfref = NumParam(default=0.3, + tex_name='T_{fref}', + info='Frequency reference filter time constant', + unit='s', + ) + + # --- GFM Voltage Reference Parameters --- + self.Ptarget = NumParam(default=0.0, + tex_name='P_{target}', + info='Target active power', + unit='p.u.', + ) + + self.Qtarget = NumParam(default=0.0, + tex_name='Q_{target}', + info='Target reactive power', + unit='p.u.', + ) + + self.Rloss = NumParam(default=0.0, # modified 0-->0.01 assumption + tex_name='R_{loss}', + info='Loss compensation resistance', + unit='p.u.', + ) + + self.Xloss = NumParam(default=0.0, # modified 0-->0.05assumption + tex_name='X_{loss}', + info='Loss compensation reactance', + unit='p.u.', + ) + + self.TVmeas = NumParam(default=0.01, + tex_name='T_{Vmeas}', + info='Voltage measurement time constant for GFM', + unit='s', + ) + + self.TVlag = NumParam(default=0.2, + tex_name='T_{Vlag}', + info='Voltage lag filter time constant', + unit='s', + ) + + self.VrefFlag = NumParam(default=1.0, + tex_name='V_{refFlag}', + info='Voltage reference flag (0 or 1)', + unit='bool', + ) + + self.Vrefmax = NumParam(default=1.1, + tex_name='V_{refmax}', + info='Maximum voltage reference', + unit='p.u.', + ) + + self.Vrefmin = NumParam(default=0.85, + tex_name='V_{refmin}', + info='Minimum voltage reference', + unit='p.u.', + ) + + self.TVref = NumParam(default=0.3, + tex_name='T_{Vref}', + info='Voltage reference filter time constant', + unit='s', + ) + + # --- GFL Active Power Path Parameters --- + self.dbJLI = NumParam(default=-0.0005, + tex_name='db_{JLI}', + info='Frequency deadband lower limit', + unit='p.u.', + ) + + self.dbJHI = NumParam(default=0.0005, + tex_name='db_{JHI}', + info='Frequency deadband upper limit', + unit='p.u.', + ) + + self.Ddn = NumParam(default=20.0, + tex_name='D_{dn}', + info='Droop for frequency above deadband', + ) + + self.Dup = NumParam(default=20.0, + tex_name='D_{up}', + info='Droop for frequency below deadband', + ) + + self.Pfreq_max = NumParam(default=1, + tex_name='P_{freq,max}', + info='Maximum frequency droop output', + unit='p.u.', + ) + + self.Pfreq_min = NumParam(default=-1, + tex_name='P_{freq,min}', + info='Minimum frequency droop output', + unit='p.u.', + ) + + self.Pref_max = NumParam(default=1.0, + tex_name='P_{ref,max}', + info='Maximum site power reference', + unit='p.u.', + ) + + self.Pref_min = NumParam(default=-1.0, + tex_name='P_{ref,min}', + info='Minimum site power reference', + unit='p.u.', + ) + + self.Perr_rmax = NumParam(default=0.1, + tex_name='P_{err,rmax}', + info='Maximum power error for rate limiter', + unit='p.u.', + ) + + self.Perr_rmin = NumParam(default=-0.1, + tex_name='P_{err,rmin}', + info='Minimum power error for rate limiter', + unit='p.u.', + ) + + self.Perr_max = NumParam(default=0.1, + tex_name='P_{err,max}', + info='Maximum power error', + unit='p.u.', + ) + + self.Perr_min = NumParam(default=-0.1, + tex_name='P_{err,min}', + info='Minimum power error', + unit='p.u.', + ) + + self.Kip = NumParam(default=0.1, #0 + tex_name='K_{ip}', + info='Proportional gain for active power PI controller', + ) + + self.Kip_Perr = NumParam(default=0.1, + tex_name='K_{ip_Perr}', + info='Integral gain for active power PI controller', + ) + + self.Tplag = NumParam(default=0.04, + tex_name='T_{plag}', + info='Active power command lag time constant', + unit='s', + ) + + self.FFRFlag = NumParam(default=0.0, + tex_name='FFR_{Flag}', + info='FFR flag (0 or 1)', + unit='bool', + ) + + self.Pcmd_GFL_max = NumParam(default=1.2, + tex_name='P_{cmd,GFL,max}', + info='Maximum active power command for GFL', + unit='p.u.', + ) + + self.Pcmd_GFL_min = NumParam(default=-1.0, + tex_name='P_{cmd,GFL,min}', + info='Minimum active power command for GFL', + unit='p.u.', + ) + + # --- GFL Reactive Power Path Parameters --- + self.Qref_max = NumParam(default=0.3122, + tex_name='Q_{ref,max}', + info='Maximum reactive power reference', + unit='p.u.', + ) + + self.Qref_min = NumParam(default=-0.3122, + tex_name='Q_{ref,min}', + info='Minimum reactive power reference', + unit='p.u.', + ) + + self.Kiq = NumParam(default=0.1, + tex_name='K_{iq}', + info='Reactive power gain', + ) + + + self.Tqlag = NumParam(default=0.04, + tex_name='T_{qlag}', + info='Reactive power lag time constant', + unit='s', + ) + + self.Verr_max = NumParam(default=0.3, + tex_name='V_{err,max}', + info='Maximum voltage error', + unit='p.u.', + ) + + self.Verr_min = NumParam(default=-0.3, + tex_name='V_{err,min}', + info='Minimum voltage error', + unit='p.u.', + ) + + self.dbVLI = NumParam(default=-0.001, + tex_name='db_{VLI}', + info='Voltage deadband lower limit', + unit='p.u.', + ) + + self.dbVHI = NumParam(default=0.001, + tex_name='db_{VHI}', + info='Voltage deadband upper limit', + unit='p.u.', + ) + + self.Kp_vc = NumParam(default=2, # 40 + tex_name='K_{p,vc}', + info='Voltage control proportional gain', + ) + + self.Ki_vc = NumParam(default=6, # 2 + tex_name='K_{i,vc}', + info='Voltage control integral gain', + ) + + self.Tvc = NumParam(default=0.02, + tex_name='T_{vc}', + info='Voltage control time constant', + unit='s', + ) + + self.Qvc_max = NumParam(default=0.31, + tex_name='Q_{vc,max}', + info='Maximum voltage control output', + unit='p.u.', + ) + + self.Qvc_min = NumParam(default=-0.31, + tex_name='Q_{vc,min}', + info='Minimum voltage control output', + unit='p.u.', + ) + + self.Qcmd_GFL_max = NumParam(default=0.8, + tex_name='Q_{cmd,GFL,max}', + info='Maximum reactive power command for GFL', + unit='p.u.', + ) + + self.Qcmd_GFL_min = NumParam(default=-0.8, + tex_name='Q_{cmd,GFL,min}', + info='Minimum reactive power command for GFL', + unit='p.u.', + ) + + self.VFlag = NumParam(default=1.0, + tex_name='V_{Flag}', + info='Voltage control flag (1-enable, 0-disable)', + unit='bool', + ) + + self.Kl_xc = NumParam(default=1.0, + tex_name='K_{l,xc}', + info='Cross-coupling gain', + ) + + self.Qerr_max = NumParam(default=0.1, # modified + tex_name='Q_{err,max}', + info='Maximum reactive power error limit', + unit='p.u.', + ) + self.Qerr_min = NumParam(default=-0.1, # modified + tex_name='Q_{err,min}', + info='Minimum reactive power error limit', + unit='p.u.', + ) + + self.k_FFR = NumParam(default=25, # modified + tex_name='P_{FFR}', + info='active power FFR offer', + unit='p.u.', + ) + + +class REPCGFMC1Model(Model): + """ + REPCGFMC1 plant controller model implementation. + """ + + def __init__(self, system, config): + Model.__init__(self, system, config) + + self.group = 'RenPlant' + self.flags.tds = True + + # --- External Parameters from REGFMC1 --- + self.bus = ExtParam(model='RenGen', src='bus', indexer=self.reg, export=False, + info='Retrieved bus idx', vtype=str, default=None, + ) + # self.busf = IdxParam(model='BusFreq', info='Bus for frequency measurement') + + # Select bus for measurements (remote bus if provided, otherwise converter bus) + from andes.core.service import DataSelect,DeviceFinder + self.buss = DataSelect(self.busr, self.bus, info='Selected bus for measurements') + + + # self.busfreq = DeviceFinder(self.busr, link=self.buss, idx_name='bus') + self.busfreq = DeviceFinder(self.busf, link=self.buss, idx_name='bus', default_model='BusFreq')# modified + self.busRocof= DeviceFinder(self.busrocof, link=self.buss, idx_name='bus', default_model='BusROCOF') # modified + # --- External Variables from Bus --- + self.v = ExtAlgeb(model='Bus', src='v', indexer=self.buss, tex_name='V', + info='Bus (or busr, if given) terminal voltage', + ) + + self.a = ExtAlgeb(model='Bus', src='a', indexer=self.buss, tex_name=r'\theta', + info='Bus (or busr, if given) phase angle', + ) + + self.v0 = ExtService(model='Bus', src='v', indexer=self.buss, tex_name="V_0", + info='Initial bus voltage', + ) + + self.f = ExtAlgeb(model='BusFreq', src='f', indexer=self.busfreq, + tex_name="f", info='bus frequency (p.u.)') # f - dynamic + + self.rocof = ExtService(model='BusROCOF', src='Wf_y', indexer=self.busRocof, tex_name="ROCOF", + info='bus ROCOF', + ) + + # self.f = ExtAlgeb(model='BusFreq', src='f', indexer=self.buss, tex_name="F", + # info='bus frequency (Hz)', + # ) # modified + + # --- External Variables from REGFMC1 --- + self.Vref_GFM = ExtAlgeb(model='RenGen', src='Vref_GFM', indexer=self.reg, + tex_name='V_{ref,GFM}', + info='Voltage reference for GFM branch', + ) + + self.fref_GFM = ExtAlgeb(model='RenGen', src='fref_GFM', indexer=self.reg, + tex_name='f_{ref,GFM}', + info='Frequency reference for GFM branch', + ) + + self.Pcmd_GFL = ExtAlgeb(model='RenGen', src='Pcmd_GFL', indexer=self.reg, + tex_name='P_{cmd,GFL}', + info='Active power command for GFL branch', + ) + + self.Qcmd_GFL = ExtAlgeb(model='RenGen', src='Qcmd_GFL', indexer=self.reg, + tex_name='Q_{cmd,GFL}', + info='Reactive power command for GFL branch', + ) + + + self.Pe = ExtAlgeb(model='RenGen', src='Pe', indexer=self.reg, export=False, + info='Active power output of REGFMC1', + ) + + self.Qe = ExtAlgeb(model='RenGen', src='Qe', indexer=self.reg, export=False, + info='Reactive power output of REGFMC1', + ) + self.Vref0= ExtService(model='RenGen', src='Vref0', indexer=self.reg, tex_name='V_{ref0}', # MODIFIED + info='Vref0 of REGFMC1', + ) + self.p0 = ExtService(model='RenGen', src='p0', indexer=self.reg, tex_name='P_0', + info='Initial active power of REGFMC1', + ) + + self.q0 = ExtService(model='RenGen', src='q0', indexer=self.reg, tex_name='Q_0', + info='Initial reactive power of REGFMC1', + ) + + # Internal reference values from power flow + self.Pref_site_0 = ConstService(v_str='p0', + tex_name='P_{ref,site,0}', + info='Initial site active power reference from power flow', + ) + + self.Qref_site_0 = ConstService(v_str='q0', + tex_name='Q_{ref,site,0}', + info='Initial site reactive power reference from power flow', + ) + + self.fref_site_0 = ConstService(v_str='1.0', + tex_name='f_{ref,site,0}', + info='Initial site frequency reference (nominal)', + ) + + self.Vref_site_0 = ConstService(v_str='v', + tex_name='V_{ref,site,0}', + info='Initial site voltage reference from power flow', + ) + + self.Ptarget_0 = NumSelect(self.Ptarget, self.p0, + tex_name='P_{target,0}', + info='Actual Ptarget (defaults to p0 if Ptarget=0)', + ) + + self.Qtarget_0 = NumSelect(self.Qtarget, self.q0, + tex_name='Q_{target,0}', + info='Actual Qtarget (defaults to q0 if Qtarget=0)', + ) + + # --- Internal reference Algeb variables (can be modified by external controllers) --- + self.Pref_site = Algeb(v_str='Pref_site_0', + e_str='Pref_site_0 - Pref_site', + tex_name='P_{ref,site}', + info='Site active power reference (internal variable)', + ) + + self.Qref_site = Algeb(v_str='Qref_site_0', + e_str='Qref_site_0 - Qref_site', + tex_name='Q_{ref,site}', + info='Site reactive power reference (internal variable)', + ) + + self.fref_site = Algeb(v_str='fref_site_0', + e_str='fref_site_0 - fref_site', + tex_name='f_{ref,site}', + info='Site frequency reference (internal variable)', + ) + + self.Vref_site = Algeb(v_str='Vref_site_0', + e_str='Vref_site_0 - Vref_site', + tex_name='V_{ref,site}', + info='Site voltage reference (internal variable)', + ) + + # --- Site Measurements --- + # Site voltage measurement + self.Vsite = Lag(u='v', T=self.Tmeas, K=1, + info='Site voltage measurement', + tex_name='V_{site}', + ) + + # Site frequency measurement (PLACEHOLDER - simplified to 1.0) + self.fsite = Lag(u='f', T=self.Tfrq, K=1, + info='Site frequency measurement', + tex_name='f_{site}', # mistake: u shouldnt be 1.0 ,should be the measured freq + ) + + # --- GFM Frequency Reference Generator (Image 3) --- + # Frequency reference filter (using internal fref_site Algeb) + + + + # modified: add logic + self.Vsite_gate = Limiter( + self.Vsite_y, + lower=self.Vfth, + upper=1.5, + name='Vsite_gate' + ) + + self.fsite_meas = Algeb( + name='fsite_meas', + tex_name='f_{site,meas}', + info='Site frequency after voltage-threshold selection', + v_str='1.0', + e_str='(fsite_y) * (Vsite_gate_zi + Vsite_gate_zu) + (fref_site) * (Vsite_gate_zl) - fsite_meas' + ) + self.frefLag = Lag(u='fsite_meas', T=self.Tfref, K=1, + info='Frequency reference filter', + tex_name='f_{ref}', + ) + # Output to REGFMC1 frequency reference + fref_out = 'frefLag_y' + self.fref_GFM.e_str = f'{fref_out} -1' # modified + + # --- GFM Voltage Reference Generator (Image 3) --- + # Site voltage measurement (already defined above as Vsite) + + # Loss compensation calculation + # Vdrop = (Rloss + jXloss) * (Ptarget - jQtarget) / Vsite_meas + # Note: This is a simplified version; full implementation needs complex calculations + self.Vsite_meas2 = Lag(u='v', T=self.TVmeas, K=1, + info='Voltage measurement for loss compensation', + tex_name='V_{site,meas}', + ) + + # Loss compensation (simplified - real part only for now) + # ΔV_loss ≈ (Rloss * Ptarget + Xloss * Qtarget) / Vsite_meas + # self.dVloss = VarService(v_str='(Rloss * Ptarget_1 + Xloss * Qtarget_0) / (Vsite_meas2_y + 1e-8)', + # tex_name=r'\Delta V_{loss}', + # info='Voltage drop due to losses', + # ) + + self.dVloss = VarService(v_str='( (Vsite_meas2_y+1e-8+Rloss * Ptarget_1 + Xloss * Qtarget_1)**2 + (Ptarget_1*Xloss-Qtarget_1*Rloss)**2 )**0.5', + tex_name=r'\Delta V_{loss}', + info='Voltage drop due to losses', + ) # modified + + # Voltage calculation with loss compensation + self.Vcalc = Algeb(tex_name='V_{calc}', + info='Calculated voltage with loss compensation', + v_str='( (v0+Rloss * Pref_site_0 + Xloss * Qref_site_0)**2 + (Pref_site_0*Xloss-Qref_site_0*Rloss)**2 )**0.5', + e_str='dVloss - Vcalc', + ) + + # Inverter voltage measurement (another filter) + self.Vinv_meas = Lag(u='v', T=self.TVlag, K=1, + info='Inverter voltage measurement', + tex_name='V_{inv,meas}', + ) + + # Voltage reference selector based on VrefFlag + self.VrefSW = Switcher(u=self.VrefFlag, options=(0, 1), tex_name='V_{refSW}') + + # When VFlag=1, use V_GFM_ref (complex calculation); when 0, use initial voltage + self.VGFM_ref = Algeb(tex_name='V_{GFM,ref}', + info='GFM voltage reference before filter', + v_str='( (v0+Rloss * Pref_site_0 + Xloss * Qref_site_0)**2 + (Pref_site_0*Xloss-Qref_site_0*Rloss)**2 )**0.5', + e_str='VrefSW_s1 * Vcalc + VrefSW_s0 * Vinv_meas_y - VGFM_ref', # modified + ) + + # Apply limits + self.VrefLim = Limiter(self.VGFM_ref, lower=self.Vrefmin, upper=self.Vrefmax, + tex_name='V_{refLim}', + ) + + self.VGFM_ref_lim = Algeb(tex_name='V_{GFM,ref,lim}', + info='Limited GFM voltage reference', + v_str='( (v0+Rloss * Pref_site_0 + Xloss * Qref_site_0)**2 + (Pref_site_0*Xloss-Qref_site_0*Rloss)**2 )**0.5', + e_str='VGFM_ref * VrefLim_zi + Vrefmax * VrefLim_zu + Vrefmin * VrefLim_zl - VGFM_ref_lim', + ) + + # Voltage reference filter + self.VrefGFMLag = Lag(u='VGFM_ref_lim', T=self.TVref, K=1, + info='GFM voltage reference filter', + tex_name='V_{ref,GFM,lag}', + ) + + # Output to REGFMC1 voltage reference + Vref_out = 'VrefGFMLag_y' + # self.Vref_GFM.e_str = f'{Vref_out} - Vref_site_0' + self.Vref_GFM.e_str = f'{Vref_out} - (Vref0)' + # self.Vref_GFM.e_str = f'{Vref_out} -Vref_GFM' + + # --- GFL Active Power Path (Image 5) --- + # Frequency deadband + + + self.fsite_err = Algeb(tex_name='f_{site,err}', + info='Site frequency error', + v_str='0', + e_str='fref_site - fsite_meas - fsite_err', + ) + + self.fdbd = DeadBand1(u=self.fsite_err, center=0.0, + lower=self.dbJLI, upper=self.dbJHI, + tex_name='f_{dbd}', + info='Frequency deadband', + ) + + # Frequency droop: use Ddn when freq is low (error > 0), Dup when freq is high (error < 0) + # Pfreq_droop = Ddn * fdbd_y (when fdbd_y > 0) or Dup * fdbd_y (when fdbd_y < 0) + self.fdbd_sign = VarService(v_str='Indicator(fdbd_y >= 0)', + tex_name='f_{dbd,sign}', + ) + + self.Pfreq_droop = Algeb(tex_name='P_{freq,droop}', + info='Frequency droop output', + v_str='0', + e_str='fdbd_sign * Ddn * fdbd_y + (1 - fdbd_sign) * Dup * fdbd_y - Pfreq_droop', + ) + + + # Apply frequency droop limits + self.Pfreq_lim = Limiter(self.Pfreq_droop, lower=self.Pfreq_min, upper=self.Pfreq_max, + tex_name='P_{freq,lim}', + ) + + self.Pfreq_droop_lim = Algeb(tex_name='P_{freq,droop,lim}', + info='Limited frequency droop output', + v_str='0', + e_str='Pfreq_droop * Pfreq_lim_zi + Pfreq_max * Pfreq_lim_zu + Pfreq_min * Pfreq_lim_zl - Pfreq_droop_lim', + ) + # FFR + self.FFRCSW = Switcher(u=self.FFRFlag, options=(0, 1), tex_name='FFR_{SW}') + + + self.P_FFR = Algeb(tex_name='P_{ffr}', + info='Limited frequency droop output', + v_str='0', + e_str='(Indicator(rocof > 0.002) + Indicator(rocof < -0.002))' + '*(fsite_err * k_FFR) - P_FFR', + ) + + # p target + self.Ptarget_1_initial = Algeb(tex_name='P_{target1_initial}', # modified + info='active power P target_initial', + v_str='Pe ', + e_str='Pfreq_droop_lim+ Pref_site+ FFRCSW_s1*P_FFR - Ptarget_1_initial', + ) + self.Ptarget_1_lim = Limiter(self.Ptarget_1_initial, lower=self.Pref_min, upper=self.Pref_max, # modified + tex_name='P_{target1_limit}', + ) + self.Ptarget_1 = Algeb(tex_name='P_{target1}', # modified + info='active power P target', + v_str='Pe', + e_str='Ptarget_1_initial * Ptarget_1_lim_zi + Pref_max * Ptarget_1_lim_zu + Pref_min * Ptarget_1_lim_zl - Ptarget_1', + ) + # Site power measurement + self.Psite = Lag(u='Pe', T=self.Tfrq, K=1, + info='Site power measurement', + tex_name='P_{site}', + ) + + + + # Site power reference limits + # self.Pref_site_lim = Limiter(u=self.Pref_site, lower=self.Pref_min, upper=self.Pref_max, + # tex_name='P_{ref,site,lim}', + # ) + # + # self.Pref_site_lim_val = Algeb(tex_name='P_{ref,site,lim}', + # info='Limited site power reference', + # v_str='Pref_site_0', + # e_str='Pref_site * Pref_site_lim_zi + Pref_max * Pref_site_lim_zu + Pref_min * Pref_site_lim_zl - Pref_site_lim_val', + # ) + + # Active power reference with frequency droop + # Ptarget is a parameter, so Pref = Ptarget + + # self.Ptarget_val = ConstService(v_str='Ptarget', + # tex_name='P_{target,val}', + # ) + + # Power error calculation + + + self.Perr = Algeb(tex_name='P_{err}', + info='Site power error', + v_str='0', + e_str='Ptarget_1 - Psite_y - Perr', # mistake + ) + + # Apply error limits (for rate limiter and PI) + self.Perr_lim = Limiter(self.Perr, lower=self.Perr_min, upper=self.Perr_max, + tex_name='P_{err,lim}', + ) + + self.Perr_lim_val = Algeb(tex_name='P_{err,lim}', + info='Limited power error', + v_str='0', + e_str='Perr * Perr_lim_zi + Perr_max * Perr_lim_zu + Perr_min * Perr_lim_zl - Perr_lim_val', + ) + + # Integrator state for PI controller [mistake]:theres no pi actually + self.xpwr = State(tex_name='x_{pwr}', + info='Integrator state for active power PI', + v_str='0', + e_str='Kip_Perr * Perr_lim_val', + ) + + + # Ploss modified + self.Ploss = Algeb(tex_name='P_{loss,GFL}', # modified + info='active power loss value', + v_str='(Pref_site_0**2+Qref_site_0**2)*Rloss', + e_str='(Ptarget_1**2+ Qtarget_1**2)*Rloss - Ploss', + ) + + + # Active power command with lag filter + + self.Pcmd_sum = Algeb(tex_name='P_{cmd,sum}', # modified + info='Sum for active power command before lag', + v_str='(Pref_site_0**2+Qref_site_0**2)*Rloss + Pref_site_0 ', + e_str='Ploss + xpwr + Ptarget_1 - Pcmd_sum') + + self.Pcmd_GFL_lag = Lag(u='Pcmd_sum', + T=self.Tplag, K=1, + info='Active power command lag filter', + tex_name='P_{cmd,GFL,lag}') + + # self.Pcmd_GFL_lag = Lag(u=' Ploss + xpwr + Ptarget_1', # modified + # T=self.Tplag, K=1, + # info='Active power command lag filter', + # tex_name='P_{cmd,GFL,lag}', + # ) + + # Apply Pcmd limits + self.Pcmd_lim = Limiter(self.Pcmd_GFL_lag_y, lower=self.Pcmd_GFL_min, upper=self.Pcmd_GFL_max, + tex_name='P_{cmd,lim}', + ) + + # Output to REGFMC1 active power command + Pcmd_out = 'Pcmd_GFL_lag_y * Pcmd_lim_zi +Pcmd_GFL_max * Pcmd_lim_zu + Pcmd_GFL_min * Pcmd_lim_zl' + self.Pcmd_GFL.e_str = f'{Pcmd_out} - (p0)' + # self.Pcmd_GFL.e_str = f'{Pcmd_out} - Pcmd_GFL' + + # --- GFL Reactive Power Path --- + # Voltage control path (using internal Vref_site Algeb) + self.Verr_site = Algeb(tex_name='V_{err,site}', + info='Site voltage error', + v_str='0', + e_str='Vref_site - Vsite_y - Verr_site', + ) + + # Voltage deadband + self.Vdbd = DeadBand1(u=self.Verr_site, center=0.0, + lower=self.dbVLI, upper=self.dbVHI, + tex_name='V_{dbd}', + info='Voltage deadband', + ) + + # Apply voltage error limits + self.Verr_lim = Limiter(self.Vdbd_y, lower=self.Verr_min, upper=self.Verr_max, + tex_name='V_{err,lim}', + ) + + self.Verr_lim_val = Algeb(tex_name='V_{err,lim}', + info='Limited voltage error', + v_str='0', + e_str='Vdbd_y * Verr_lim_zi + Verr_max * Verr_lim_zu + Verr_min * Verr_lim_zl - Verr_lim_val', + ) + + # Integral path with anti-windup + self.Qvc_int = IntegratorAntiWindup(u=self.Verr_lim_val, + T=1.0, + K=self.Ki_vc, + y0=0, + lower=self.Qvc_min, + upper=self.Qvc_max, + name='Qvc_int', + tex_name='Q_{vc,int}', + info='Voltage control integrator with anti-windup', + ) + + # Voltage control PI output (proportional + integral) + self.Qvc = Algeb(tex_name='Q_{vc}', + info='Voltage control PI output', + v_str='0', + e_str='Kp_vc * Verr_lim_val + Qvc_int_y - Qvc', + ) + + # Apply voltage control limits + self.Qvc_lim = Limiter(self.Qvc, lower=self.Qvc_min, upper=self.Qvc_max, + tex_name='Q_{vc,lim}', + ) + + self.Qvc_lim_val = Algeb(tex_name='Q_{vc,lim}', + info='Limited voltage control output', + v_str='0', + e_str='Qvc * Qvc_lim_zi + Qvc_max * Qvc_lim_zu + Qvc_min * Qvc_lim_zl - Qvc_lim_val', + ) + + # Voltage control filter + self.Qvc_lag = Lag(u='Qvc_lim_val', T=self.Tvc, K=1, + info='Voltage control lag filter', + tex_name='Q_{vc,lag}', + ) + + # Reactive power reference path (simplified) + # self.Qref_site_lim = Limiter(self.Qref_site, lower=self.Qref_min, upper=self.Qref_max, + # tex_name='Q_{ref,site,lim}', + # ) + + # self.Qref_site_lim_val = Algeb(tex_name='Q_{ref,site,lim}', + # info='Limited reactive power reference', + # v_str='Qref_site_0', + # e_str='Qref_site * Qref_site_lim_zi + Qref_max * Qref_site_lim_zu + Qref_min * Qref_site_lim_zl - Qref_site_lim_val', + # ) + + # Reactive power control with lag + self.Qsite_filt = Lag(u='Qe', T=self.Tqlag, K=1, + info='Site reactive power measurement', + tex_name='Q_{site,filt}', + ) + + # VFlag selector + self.VFlagSW = Switcher(u=self.VFlag, options=(0, 1), tex_name='V_{FlagSW}') + + # When VFlag=1, use Qvc_lag (voltage control); when 0, use Q reference + # self.Qerr = Algeb(tex_name='Q_{err}', + # info='Reactive power error', + # v_str='0', + # e_str='Qref_site_lim_val - Qsite_filt_y + VFlagSW_s1 * Kiq * Qvc_lag_y - Qerr', + # ) + + self.Qtarget_1_initial = Algeb(tex_name='Q_{target1_initial}', # modified + info='Reactive power Q target_initial', + v_str='Qref_site_0 ', + e_str='VFlagSW_s1*(Qref_site+Qvc_lag_y) +VFlagSW_s0*(Qref_site)-Qtarget_1_initial ', + ) + self.Qtarget_1_lim = Limiter(self.Qtarget_1_initial, lower=self.Qref_min, upper=self.Qref_max, # modified + tex_name='Q_{target1_limit}', + ) + self.Qtarget_1= Algeb(tex_name='Q_{target1}', # modified + info='Reactive power Q target', + v_str='Qref_site_0', + e_str='Qtarget_1_lim_zi * Qtarget_1_initial + Qref_max * Qtarget_1_lim_zu + Qref_min * Qtarget_1_lim_zl - Qtarget_1', + ) + self.Qerr0 = Algeb(tex_name='Q_{err0}', # modified + info='Reactive power error_0', + v_str='0', + e_str='Qtarget_1 - Qsite_filt_y - Qerr0', + ) + # self.Qerr_int= State(tex_name='Q_{err_integral}', # modified + # info='Integrator state for Qerror', + # v_str='0', + # e_str='Kiq *Qerr0', + # ) + + # Integral path with anti-windup + self.Qerr_int = IntegratorAntiWindup(u=self.Qerr0, + T=1.0, + K=self.Kiq, + y0=0, + lower=self.Qerr_min, + upper=self.Qerr_max, + name='Qerr_int', + tex_name='Q_{err_integral}', + info='Integrator state for Qerror', + ) + + # Voltage control PI output (proportional + integral) + self.Qerr_pi = Algeb(tex_name='Q_{vc}', + info='Qerror PI output', + v_str='0', + e_str='Qerr_int_y - Qerr_pi', + ) + + + + + + self.Qerr_lim = Limiter(self.Qerr_pi, lower=self.Qerr_min, upper=self.Qerr_max, # modified + tex_name='Q_{err,lim}', + ) + + self.Qerr_lim_val = Algeb(tex_name='Q_{vc,lim}', # modified + info='Limited voltage control output', + v_str='0', + e_str='Qerr_pi * Qerr_lim_zi + Qerr_max * Qerr_lim_zu + Qerr_min * Qerr_lim_zl - Qerr_lim_val', + ) + self.Qloss = Algeb(tex_name='Q_{loss,GFL}', # modified + info='Reactive power loss value', + v_str='(Pref_site_0**2+Qref_site_0**2)*Xloss', + e_str='(Ptarget_1**2+ Qtarget_1**2)*Xloss - Qloss', + ) + self.Qcmd_GFL_0 = Algeb(tex_name='Q_{cmd,GFL,0}', # modified + info='Reactive power command value(no lag)', + v_str='(Pref_site_0**2+Qref_site_0**2)*Xloss+Qref_site_0', + e_str='Qloss + Qerr_lim_val + Qtarget_1 - Qcmd_GFL_0', + ) + + + self.Qcmd_GFLLag = Lag(u='Qcmd_GFL_0', T=self.Tqlag , K=1, # modified + info='Reactive power command value', + tex_name='Q_{cmd,GFL}', + ) + + self.Qcmd_lim = Limiter(self.Qcmd_GFLLag_y, lower=self.Qcmd_GFL_min, upper=self.Qcmd_GFL_max, + tex_name='Q_{cmd,lim}', + ) + + Qcmd_out = 'Qcmd_GFLLag_y * Qcmd_lim_zi + Qcmd_GFL_max * Qcmd_lim_zu + Qcmd_GFL_min * Qcmd_lim_zl' + # Output to REGFMC1 reactive power command + # Qcmd_out = 'Qcmd_GFLLag_y' # modified + self.Qcmd_GFL.e_str = f'{Qcmd_out} - (q0)' + # self.Qcmd_GFL.e_str = f'{Qcmd_out} - Qcmd_GFL' + +class REPCGFMC1(REPCGFMC1Data, REPCGFMC1Model): + """ + REPCGFMC1: Plant controller for REGFMC1 (hybrid grid-forming converter). + + This model provides reference signals to REGFMC1: + - GFM Branch: Voltage reference (Vref_GFM) and frequency reference (fref_GFM) + - GFL Branch: Active power command (Pcmd_GFL) and reactive power command (Qcmd_GFL) + + The controller implements: + 1. GFM frequency reference generator with voltage-based switching + 2. GFM voltage reference generator with loss compensation + 3. GFL active power path with frequency droop and FFR + 4. GFL reactive power and voltage control + + Notes: + - Site measurements are taken from the converter bus or optional remote bus + - Frequency measurement is simplified (uses constant 1.0 pu) + - Loss compensation uses simplified real-part calculation + """ + + def __init__(self, system, config): + REPCGFMC1Data.__init__(self) + REPCGFMC1Model.__init__(self, system, config) From e595f2fc8990a748cec8c6619f89fa8e4b68a7c0 Mon Sep 17 00:00:00 2001 From: Zilin Zhuang Date: Wed, 15 Apr 2026 10:54:29 -0400 Subject: [PATCH 2/6] fix: update eig routine eig.py and renewable model files regfmc1.py & repcgfmc1.py change the case parameter of npcc.xlsx --- andes/cases/npcc/npcc.xlsx | Bin 70386 -> 70507 bytes andes/models/group.py | 2 +- andes/models/renewable/regfmc1.py | 356 +++++++++++++++++++--------- andes/models/renewable/repcgfmc1.py | 207 ++++++++++++++-- andes/routines/eig.py | 42 +--- 5 files changed, 443 insertions(+), 164 deletions(-) diff --git a/andes/cases/npcc/npcc.xlsx b/andes/cases/npcc/npcc.xlsx index a398a8f379de7c9ef2873eae28b2f6af4f6e60f5..680fedbcb5b2ccb700b36ced68e0dcacc33db5ce 100644 GIT binary patch delta 63259 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 d1O8>Qsso(;+%;UMMSABsc2e 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

azduiuZlB{lU`rT4oJy#Q!O1P%{8-TS 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 bewCmk2M7Jx007`XKf-VT06rQtn*ja~ZRWKn delta 63103 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 fixed output -> hold TFFR -> ramp back with DFFR -> re-arm + + + # self.z_ffr_low = VarService(v_str='Indicator(fsite_meas < fFFR_low)') + # self.z_ffr_high = VarService(v_str='Indicator(fsite_meas > fFFR_high)') + # self.z_ffr_thr = VarService(v_str='Indicator(z_ffr_low + z_ffr_high > 0)') + + # # busy / armed + # self.z_ffr_busy = VarService(v_str='Indicator(Abs(P_FFR) > 1e-6)') + # self.z_ffr_armed = VarService(v_str='FFRCSW_s1 * (1 - z_ffr_busy)') + + # # only armed condition can fire 0 or 1 + # self.z_ffr_pick = VarService(v_str='z_ffr_armed * z_ffr_thr') + + # # convert conditionz_ffr_armed * z_ffr_thr') + + # # convert condition into event 0 or 1 + # self.ffr_evt = EventFlag(self.z_ffr_pick) + + # # hold window for TFFR seconds only trig from 0-->1 + # self.ffr_hold = ExtendedEvent(self.ffr_evt, t_ext=self.TFFR, trig="rise") + + # # choose fixed step value determine the P_FFR + # self.Pffr_pick = VarService( + # v_str='z_ffr_low * PFFR_low + z_ffr_high * PFFR_high' + # ) + + # # hold the chosen value + # self.Pffr_hold = VarHold(self.Pffr_pick, hold=self.ffr_hold) + + # algebraic output placeholder, final value still updated in g_numeric() + self.P_FFR = Algeb( + tex_name='P_{FFR}', + info='FFR contribution added to active power reference', + v_str='0.0', + e_str='0.0 - P_FFR', + diag_eps=True + ) + + + + # p target self.Ptarget_1_initial = Algeb(tex_name='P_{target1_initial}', # modified @@ -944,6 +1040,85 @@ def __init__(self, system, config): # Qcmd_out = 'Qcmd_GFLLag_y' # modified self.Qcmd_GFL.e_str = f'{Qcmd_out} - (q0)' # self.Qcmd_GFL.e_str = f'{Qcmd_out} - Qcmd_GFL' + + + # # FFR update function + def v_numeric(self, **kwargs): + self._ffr_cmd = 0.0 # ffr output now + self._ffr_hold_remain = 0.0 # remaining time + self._ffr_busy = 0 # 0=armed, 1=busy + self._ffr_last_t = None + self._ffr_eps = 1e-8 + + def g_numeric(self, **kwargs): + dae = self.system.dae + t_now = float(getattr(dae, 't', 0.0)) + + if not hasattr(self, '_ffr_cmd'): + self._ffr_cmd = 0.0 + self._ffr_hold_remain = 0.0 + self._ffr_busy = 0 + self._ffr_last_t = None + self._ffr_eps = 1e-8 + + if self._ffr_last_t is None: + dt = 0.0 + else: + dt = max(0.0, t_now - self._ffr_last_t) + + enabled = bool(self.FFRFlag.v[0] > 0.5) + f_meas = float(self.fsite_meas.v[0]) + + f_low = float(self.fFFR_low.v[0]) + f_high = float(self.fFFR_high.v[0]) + + p_low = float(self.PFFR_low.v[0]) + p_high = float(self.PFFR_high.v[0]) + + dffr = float(self.DFFR.v[0]) + tffr = float(self.TFFR.v[0]) + + if (self._ffr_last_t is None) or (dt > 0.0): + + if not enabled: + self._ffr_cmd = 0.0 + self._ffr_hold_remain = 0.0 + self._ffr_busy = 0 + + else: + # 1) armed: check trigger only when not busy + if self._ffr_busy == 0: + if f_meas < f_low: + self._ffr_cmd = p_low + self._ffr_hold_remain = tffr + self._ffr_busy = 1 + elif f_meas > f_high: + self._ffr_cmd = p_high + self._ffr_hold_remain = tffr + self._ffr_busy = 1 + + # 2) busy: hold stage + else: + if self._ffr_hold_remain > 0.0: + self._ffr_hold_remain = max(0.0, self._ffr_hold_remain - dt) + + # 3) ramp-back stage + else: + if self._ffr_cmd > 0.0: + self._ffr_cmd = max(0.0, self._ffr_cmd - dffr * dt) + elif self._ffr_cmd < 0.0: + self._ffr_cmd = min(0.0, self._ffr_cmd + dffr * dt) + + # 4) re-arm only after returning to zero + if abs(self._ffr_cmd) <= self._ffr_eps: + self._ffr_cmd = 0.0 + self._ffr_busy = 0 + + self._ffr_last_t = t_now + + # algebraic residual: force P_FFR = _ffr_cmd + self.P_FFR.e[:] = np.array([self._ffr_cmd]) - self.P_FFR.v + class REPCGFMC1(REPCGFMC1Data, REPCGFMC1Model): """ diff --git a/andes/routines/eig.py b/andes/routines/eig.py index 43c706a4b..fdadfa594 100644 --- a/andes/routines/eig.py +++ b/andes/routines/eig.py @@ -29,18 +29,16 @@ class EIG(BaseRoutine): def __init__(self, system, config): super().__init__(system=system, config=config) - self.config.add(plot=0, tol=1e-6, gy_tol=1e-6, gy_tol=1e-6) + self.config.add(plot=0, tol=1e-6, gy_tol=1e-6) self.config.add_extra("_help", plot="show plot after computation", tol="numerical tolerance to treat eigenvalues as zeros", - gy_tol="row norm threshold for eliminating dead algebraic variables", gy_tol="row norm threshold for eliminating dead algebraic variables") self.config.add_extra("_alt", plot=(0, 1)) # internal flags and storage self.As = None # reduced state matrix - self.As = None # reduced state matrix self.mu = None # eigenvalues self.N = None # right eigenvectors self.W = None # left eigenvectors @@ -52,9 +50,6 @@ def __init__(self, system, config): # --- related to dead algebraic variables --- self.dead_algeb_idx = np.array([], dtype=int) - # --- related to dead algebraic variables --- - self.dead_algeb_idx = np.array([], dtype=int) - # --- statistics -- self.n_positive = 0 self.n_zeros = 0 @@ -62,7 +57,6 @@ def __init__(self, system, config): self.x_name = [] self.x_tex_name = [] - self.x_tex_name = [] def calc_As(self, dense=True): r""" @@ -89,12 +83,6 @@ def calc_As(self, dense=True): complement handles both original algebraic variables and zero-Tf states uniformly. - States with zero time constants satisfy :math:`0 = f(x, y)` and - are treated as algebraic equations. They are folded into the - algebraic system before reduction so that a single Schur - complement handles both original algebraic variables and - zero-Tf states uniformly. - Returns ------- kvxopt.matrix @@ -105,14 +93,10 @@ def calc_As(self, dense=True): self.find_zero_states() self.x_name = np.array(dae.x_name) self.x_tex_name = np.array(dae.x_tex_name) - self.x_tex_name = np.array(dae.x_tex_name) - fx, fy, gx, gy = dae.fx, dae.fy, dae.gx, dae.gy - Tf = dae.Tf fx, fy, gx, gy = dae.fx, dae.fy, dae.gx, dae.gy Tf = dae.Tf - # Fold zero-Tf states into the algebraic system # Fold zero-Tf states into the algebraic system if len(self.zstate_idx) > 0: fx, fy, gx, gy, Tf = self._fold_zstates(fx, fy, gx, gy, Tf) @@ -131,29 +115,6 @@ def calc_As(self, dense=True): self.As = self._reduce(fx, fy, gx, gy, Tf, dense=dense) - # Use state constraints to eliminate dependent states - if C is not None: - self.As = self._apply_state_constraints(self.As, C) - - if len(self.x_name) < dae.n: - n_as = len(self.x_name) - logger.info("State matrix is %d x %d (reduced from %d states).", n_as, n_as, dae.n) - fx, fy, gx, gy, Tf = self._fold_zstates(fx, fy, gx, gy, Tf) - - # Find and eliminate dead algebraic variables (including dead zero-Tf states) - self.find_dead_algebs(gy, gx) - - # Extract state constraints (0 = C δx) before dead rows are removed - C = self._extract_state_constraints(gx) - - if len(self.dead_algeb_idx) > 0: - fx, fy, gx, gy = self._eliminate_algebs(fx, fy, gx, gy) - - # Regularize dead columns (variables absent from all equations) - self._regularize_dead_columns(gy) - - self.As = self._reduce(fx, fy, gx, gy, Tf, dense=dense) - # Use state constraints to eliminate dependent states if C is not None: self.As = self._apply_state_constraints(self.As, C) @@ -186,7 +147,6 @@ def _reduce(self, fx, fy, gx, gy, Tf, dense=True): else: return sparse(iTf * self.fxy) - def _fold_zstates(self, fx, fy, gx, gy, Tf): def _fold_zstates(self, fx, fy, gx, gy, Tf): """ Fold zero-Tf states into the algebraic system. From 1c038a85f1b0dda43598abbf5513f0f654bc4b17 Mon Sep 17 00:00:00 2001 From: Zilin Zhuang Date: Tue, 21 Apr 2026 15:21:47 -0400 Subject: [PATCH 3/6] =?UTF-8?q?fix:=20Further=20refinements=20were=20made?= =?UTF-8?q?=20to=20the=20model,=20standardizing=20the=20per-unit=20values?= =?UTF-8?q?=20=E2=80=8B=E2=80=8Bboth=20internal=20and=20external=20to=20th?= =?UTF-8?q?e=20model,=20and=20resolving=20issues=20related=20to=20initiali?= =?UTF-8?q?zation=20current=20limiting=20and=20slow=20initialization.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- andes/models/renewable/regfmc1.py | 492 ++++++++++++++-------------- andes/models/renewable/repcgfmc1.py | 46 ++- 2 files changed, 272 insertions(+), 266 deletions(-) diff --git a/andes/models/renewable/regfmc1.py b/andes/models/renewable/regfmc1.py index b418ae82e..afb69a3ba 100644 --- a/andes/models/renewable/regfmc1.py +++ b/andes/models/renewable/regfmc1.py @@ -13,7 +13,7 @@ ) from andes.core.service import NumSelect, VarService -from andes.core.block import PIController, Washout +from andes.core.block import DeadBand1, PIController, Washout class REGFMC1Data(ModelData): @@ -193,22 +193,6 @@ def __init__(self): tex_name='db_{VHI}', info='Voltage deadband upper limit (PLACEHOLDER)', ) - self.Pcmd_GFL_max = NumParam(default=0.8, - tex_name='P_{cmd,GFL,max}', - info='Maximum active power command for GFL (PLACEHOLDER)', - ) - self.Pcmd_GFL_min = NumParam(default=-0.8, - tex_name='P_{cmd,GFL,min}', - info='Minimum active power command for GFL (PLACEHOLDER)', - ) - self.Qcmd_GFL_max = NumParam(default=0.6, - tex_name='Q_{cmd,GFL,max}', - info='Maximum reactive power command for GFL (PLACEHOLDER)', - ) - self.Qcmd_GFL_min = NumParam(default=-0.6, - tex_name='Q_{cmd,GFL,min}', - info='Minimum reactive power command for GFL (PLACEHOLDER)', - ) # --- Current Limiting Parameters --- @@ -230,7 +214,30 @@ def __init__(self): tex_name='V_{min}', info='Minimum voltage for current limiting (PLACEHOLDER)', ) + + + self.Pcmd_GFL_max = NumParam(default=1.2, + tex_name='P_{cmd,GFL,max}', + info='Maximum active power command for GFL', + unit='p.u.', + ) + self.Pcmd_GFL_min = NumParam(default=-1.0, + tex_name='P_{cmd,GFL,min}', + info='Minimum active power command for GFL', + unit='p.u.', + ) + self.Qcmd_GFL_max = NumParam(default=0.6, + tex_name='Q_{cmd,GFL,max}', + info='Maximum reactive power command for GFL', + unit='p.u.', + ) + + self.Qcmd_GFL_min = NumParam(default=-0.6, + tex_name='Q_{cmd,GFL,min}', + info='Minimum reactive power command for GFL', + unit='p.u.', + ) class REGFMC1Model(Model): """ @@ -273,22 +280,45 @@ def __init__(self, system, config): ) # --- Initialization Services --- - self.p0 = ConstService(v_str='gammap * p0s', - tex_name='P_0', - info='Initial P for this device', - ) - self.q0 = ConstService(v_str='gammaq * q0s', - tex_name='Q_0', - info='Initial Q for this device', - ) - - # Initial current calculations (for both branches) - self.Id0_GFL = ConstService(tex_name=r'I_{d0,GFL}', - v_str='u * p0 / v', - ) - self.Iq0_GFL = ConstService(tex_name=r'I_{q0,GFL}', - v_str='u * q0 / v', - ) + # Keep both system-base and device-base copies so the network-facing + # power balance remains on system base while the controller uses + # device-base references internally. + self.SbSn = ConstService(v_str='sys_mva / Sn', + tex_name=r'S_b/S_n', + info='System-base to device-base power factor', + ) + self.SnSb = ConstService(v_str='Sn / sys_mva', + tex_name=r'S_n/S_b', + info='Device-base to system-base power factor', + ) + self.p0_sys = ConstService(v_str='gammap * p0s', + tex_name=r'P_{0,sys}', + info='Initial P for this device on system base', + ) + self.q0_sys = ConstService(v_str='gammaq * q0s', + tex_name=r'Q_{0,sys}', + info='Initial Q for this device on system base', + ) + self.p0 = ConstService(v_str='p0_sys * SbSn', + tex_name='P_0', + info='Initial P for this device on device base', + ) + self.q0 = ConstService(v_str='q0_sys * SbSn', + tex_name='Q_0', + info='Initial Q for this device on device base', + ) + self.Imax_dev = ConstService(v_str='Imax * SbSn', + tex_name=r'I_{max,dev}', + info='Maximum total output current on device base', + ) + + # Initial current calculations (for both branches) + self.Id0_GFL = ConstService(tex_name=r'I_{d0,GFL}', + v_str='u * p0 / v', + ) + self.Iq0_GFL = ConstService(tex_name=r'I_{q0,GFL}', + v_str='u * q0 / v', + ) # Damping washout time constant (1 / omegaD) self.Tdamp = ConstService(tex_name=r'T_{damp}', @@ -303,10 +333,22 @@ def __init__(self, system, config): ) # GFM branch impedance squared (for current calculation) - self.Zs2 = ConstService(v_str='Rs**2 + Xs**2', - tex_name='Z_s^2', - info='GFM series impedance magnitude squared', - ) + self.Zs2 = ConstService(v_str='Rs**2 + Xs**2', + tex_name='Z_s^2', + info='GFM series impedance magnitude squared', + ) + self.Rs_dev = ConstService(v_str='Rs * SnSb', + tex_name=r'R_{s,dev}', + info='GFM series resistance on device base', + ) + self.Xs_dev = ConstService(v_str='Xs * SnSb', + tex_name=r'X_{s,dev}', + info='GFM series reactance on device base', + ) + self.Zs2_dev = ConstService(v_str='Rs_dev**2 + Xs_dev**2', + tex_name=r'Z_{s,dev}^2', + info='GFM series impedance magnitude squared on device base', + ) # --- External reference variables (to be controlled by plant controller) --- # GFM voltage reference (external input to voltage control) # mistake @@ -569,13 +611,21 @@ def __init__(self, system, config): name='VinvGFLLag', ) # same as VinvLag_y - # Voltage error for GFL (TODO: add deadband) + # Voltage error for GFL self.Verr_GFL = Algeb(tex_name='V_{err,GFL}', info='Voltage error for GFL', v_str='Vref0 - v', e_str='Vref0 - VinvGFLLag_y - Verr_GFL', # Vref0 or VrefLag_y? shouldbe Vref0! ) + # Offset piecewise-linear deadband for GFL voltage error: + # y = x-dbVHI (x > dbVHI), 0 (dbVLI <= x <= dbVHI), x-dbVLI (x < dbVLI) + self.Verr_GFL_dbd = DeadBand1(u=self.Verr_GFL, center=0.0, + lower=self.dbVLI, upper=self.dbVHI, + name='Verr_GFL_dbd', + tex_name='V_{err,GFL,dbd}', + info='Deadbanded voltage error for GFL') + # Active and reactive power commands (controlled by plant controller) # Default equations lock to p0/q0, but can be overridden externally self.Pcmd_GFL = Algeb(tex_name='P_{cmd,GFL}', @@ -591,17 +641,17 @@ def __init__(self, system, config): ) # Current commands (PLACEHOLDER - TODO: add limiters and PQ priority) - self.Ipcmd_GFL = Algeb(tex_name='I_{pcmd,GFL}', - info='Active current command for GFL', - v_str='Id0_GFL', - e_str='Pcmd_GFL / v - Ipcmd_GFL', - ) - - self.Iqcmd_GFL = Algeb(tex_name='I_{qcmd,GFL}', - info='Reactive current command for GFL', - v_str='kqv * (Vref0 - v) + Iq0_GFL', - e_str='kqv * Verr_GFL + Qcmd_GFL / v - Iqcmd_GFL', - ) + self.Ipcmd_GFL = Algeb(tex_name='I_{pcmd,GFL}', + info='Active current command for GFL', + v_str='Id0_GFL', + e_str='Pcmd_GFL / v - Ipcmd_GFL', + ) + + self.Iqcmd_GFL = Algeb(tex_name='I_{qcmd,GFL}', + info='Reactive current command for GFL', + v_str='kqv * Verr_GFL_dbd_y + Iq0_GFL', + e_str='kqv * Verr_GFL_dbd_y + Qcmd_GFL / v - Iqcmd_GFL', + ) # GFL PQ Priority Current Limiting @@ -631,48 +681,48 @@ def __init__(self, system, config): # e_str='PQFlag * Imax + (1 - PQFlag) * sqrt(0.5 * ((Imax**2 - Ipcmd_sat_val**2) + sqrt( Imax**2 - Ipcmd_sat_val**2)**2 )) - Iqmax_GFL1' # ) - self.Ipmaxsq0_GFL1 = ConstService( - v_str='Piecewise((0, Le(Imax**2 - (kqv * (Vref0 - v) + Iq0_GFL) **2, 0.0)), (Imax**2 - (kqv * (Vref0 - v) + Iq0_GFL)**2 , True), evaluate=False)' - ) - - self.Iqmaxsq0_GFL1 = ConstService( - v_str='Piecewise((0, Le(Imax**2 - Id0_GFL**2, 0.0)), (Imax**2 - Id0_GFL**2, True), evaluate=False)' - ) - - self.Ipmaxsq_GFL1 = VarService( - v_str='Piecewise((0, Le(Imax**2 - Iqcmd_sat_val**2, 0.0)), (Imax**2 - Iqcmd_sat_val**2, True), evaluate=False)', - tex_name='I_{p,max,GFL}^2', - ) - - self.Iqmaxsq_GFL1 = VarService( - v_str='Piecewise((0, Le(Imax**2 - Ipcmd_sat_val**2, 0.0)), (Imax**2 - Ipcmd_sat_val**2, True), evaluate=False)', - tex_name='I_{q,max,GFL}^2', - ) - - self.Ipmax_GFL1 = Algeb( - tex_name='I_{p,max,GFL}', - info='Ipmax_GFL for Current Limiting Algorithm', - v_str='PQFlag * Imax + (1 - PQFlag) * sqrt(Ipmaxsq0_GFL1)', - e_str='PQFlag * Imax + (1 - PQFlag) * sqrt(Ipmaxsq_GFL1) - Ipmax_GFL1' - ) - - self.Iqmax_GFL1 = Algeb( - tex_name='I_{q,max,GFL}', - info='Iqmax_GFL for Current Limiting Algorithm', - v_str='(1 - PQFlag) * Imax + PQFlag * sqrt(Iqmaxsq0_GFL1)', - e_str='(1 - PQFlag) * Imax + PQFlag * sqrt(Iqmaxsq_GFL1) - Iqmax_GFL1' - ) + self.Ipmaxsq0_GFL1 = ConstService( + v_str='Piecewise((0, Le(Imax_dev**2 - (kqv * (Vref0 - v) + Iq0_GFL) **2, 0.0)), (Imax_dev**2 - (kqv * (Vref0 - v) + Iq0_GFL)**2 , True), evaluate=False)' + ) + + self.Iqmaxsq0_GFL1 = ConstService( + v_str='Piecewise((0, Le(Imax_dev**2 - Id0_GFL**2, 0.0)), (Imax_dev**2 - Id0_GFL**2, True), evaluate=False)' + ) + + self.Ipmaxsq_GFL1 = VarService( + v_str='Piecewise((0, Le(Imax_dev**2 - Iqcmd_sat_val**2, 0.0)), (Imax_dev**2 - Iqcmd_sat_val**2, True), evaluate=False)', + tex_name='I_{p,max,GFL}^2', + ) + + self.Iqmaxsq_GFL1 = VarService( + v_str='Piecewise((0, Le(Imax_dev**2 - Ipcmd_sat_val**2, 0.0)), (Imax_dev**2 - Ipcmd_sat_val**2, True), evaluate=False)', + tex_name='I_{q,max,GFL}^2', + ) + + self.Ipmax_GFL1 = Algeb( + tex_name='I_{p,max,GFL}', + info='Ipmax_GFL for Current Limiting Algorithm', + v_str='PQFlag * Imax_dev + (1 - PQFlag) * sqrt(Ipmaxsq0_GFL1)', + e_str='PQFlag * Imax_dev + (1 - PQFlag) * sqrt(Ipmaxsq_GFL1) - Ipmax_GFL1' + ) + + self.Iqmax_GFL1 = Algeb( + tex_name='I_{q,max,GFL}', + info='Iqmax_GFL for Current Limiting Algorithm', + v_str='(1 - PQFlag) * Imax_dev + PQFlag * sqrt(Iqmaxsq0_GFL1)', + e_str='(1 - PQFlag) * Imax_dev + PQFlag * sqrt(Iqmaxsq_GFL1) - Iqmax_GFL1' + ) self.Ipmin_GFL1 = Algeb( tex_name='I_{p,min,GFL}', - info='Ipmin_GFL for Current Limiting Algorithm', + info='Active-current lower limit with P/Q priority selection', v_str='-Ipmax_GFL1', e_str='-Ipmax_GFL1 - Ipmin_GFL1' ) self.Iqmin_GFL1 = Algeb( tex_name='I_{q,min,GFL}', - info='Iqmin_GFL for Current Limiting Algorithm', + info='Reactive-current lower limit with P/Q priority selection', v_str='-Iqmax_GFL1', e_str='-Iqmax_GFL1 - Iqmin_GFL1' ) @@ -736,120 +786,21 @@ def __init__(self, system, config): e_str='Iqcmd_GFL * Iqcmd_sat_zi + Iqmax_GFL1 * Iqcmd_sat_zu + Iqmin_GFL1 * Iqcmd_sat_zl - Iqcmd_sat_val', ) - # # choose priority 1=P ,0=Q - # self.PQsel = VarService(v_str='Indicator(PQFlag >= 0.5)', tex_name='PQ_{sel}', - # info='1=P priority, 0=Q priority') - # # A:P prior, first limit Ip,then Iq - # self.Irem_p = VarService( - # v_str='sqrt( (Imax**2 - Ipcmd_sat_val**2) * Indicator(Imax >= abs(Ipcmd_sat_val)) )', - # tex_name='I_{rem}^{(P)}', - # info='Remaining current radius after Ip saturation' - # ) - - # # step1:Irem_p boundary - # self.Iq_step1_p = VarService( - # v_str='Iqcmd_sat_val * Indicator(abs(Iqcmd_sat_val) <= Irem_p) + ' - # 'Irem_p * Indicator(Iqcmd_sat_val > Irem_p) - ' - # 'Irem_p * Indicator(Iqcmd_sat_val < -Irem_p)', - # tex_name='I_{q,step1}^{(P)}', - # info='Iq after Irem bounds' - # ) - - # # step2:apply Iqmin/Iqmax boundary - # self.Iq_lim_p = Algeb( - # name='Iq_lim_p', v_str='(kqv * (Vref0 - v) + Iq0_GFL) ', - # e_str='Iq_step1_p * Indicator((Iq_step1_p >= Iqmin_GFL) & (Iq_step1_p <= Iqmax_GFL)) + ' - # 'Iqmax_GFL * Indicator(Iq_step1_p > Iqmax_GFL) + ' - # 'Iqmin_GFL * Indicator(Iq_step1_p < Iqmin_GFL) - Iq_lim_p', - # tex_name='I_{q}^{(P)}', info='Iq limited with P priority' - # ) - - # # B:Q prior, first limit Iq,then Ip - # self.Irem_q = VarService( - # v_str='sqrt( (Imax**2 - Iqcmd_sat_val**2) * Indicator(Imax >= abs(Iqcmd_sat_val)) )', - # tex_name='I_{rem}^{(Q)}', - # info='Remaining current radius after Iq saturation' - # ) - - # # step1: apply Irem_q - # self.Ip_step1_q = VarService( - # v_str='Ipcmd_sat_val * Indicator(abs(Ipcmd_sat_val) <= Irem_q) + ' - # 'Irem_q * Indicator(Ipcmd_sat_val > Irem_q) - ' - # 'Irem_q * Indicator(Ipcmd_sat_val < -Irem_q)', - # tex_name='I_{p,step1}^{(Q)}', - # info='Ip after Irem bounds' - # ) - - # # step2:apply Ipmin/Ipmax boundary - # self.Ip_lim_q = Algeb( - # name='Ip_lim_q', v_str='Id0_GFL', - # e_str='Ip_step1_q * Indicator((Ip_step1_q >= Ipmin_GFL) & (Ip_step1_q <= Ipmax_GFL)) + ' - # 'Ipmax_GFL * Indicator(Ip_step1_q > Ipmax_GFL) + ' - # 'Ipmin_GFL * Indicator(Ip_step1_q < Ipmin_GFL) - Ip_lim_q', - # tex_name='I_{p}^{(Q)}', info='Ip limited with Q priority' - # ) - - - # # Current outputs (PLACEHOLDER - limiting yet) - # # P prior - # self.Iq_upper_p = VarService( - # v_str='Iqmax_GFL * Indicator(Iqmax_GFL <= Irem_p) + ' - # 'Irem_p * Indicator(Iqmax_GFL > Irem_p)', - # tex_name=r'\overline{I_q}^{(P)}', - # info='Upper bound of Iq for P priority' - # ) - # self.Iq_lower_p = VarService( - # v_str='Iqmin_GFL * Indicator(Iqmin_GFL >= -Irem_p) + ' - # '(-Irem_p) * Indicator(Iqmin_GFL < -Irem_p)', - # tex_name=r'\underline{I_q}^{(P)}', - # info='Lower bound of Iq for P priority' - # ) - - # # q prior - # self.Ip_upper_q = VarService( - # v_str='Ipmax_GFL * Indicator(Ipmax_GFL <= Irem_q) + ' - # 'Irem_q * Indicator(Ipmax_GFL > Irem_q)', - # tex_name=r'\overline{I_p}^{(Q)}', - # info='Upper bound of Ip for Q priority' - # ) - # self.Ip_lower_q = VarService( - # v_str='Ipmin_GFL * Indicator(Ipmin_GFL >= -Irem_q) + ' - # '(-Irem_q) * Indicator(Ipmin_GFL < -Irem_q)', - # tex_name=r'\underline{I_p}^{(Q)}', - # info='Lower bound of Ip for Q priority' - # ) - - # # final Ip, Iq for gfm gfl - # self.Ip_GFL = Algeb(tex_name='I_{p,GFL}', - # info='Active current output for GFL', - # v_str='Id0_GFL', - # e_str='PQsel * Ipcmd_sat_val + (1 - PQsel) * Ip_lim_q - Ip_GFL', - # ) - - # self.Iq_GFL = Algeb(tex_name='I_{q,GFL}', - # info='Reactive current output for GFL', - # v_str='kqv * (Vref0 - v) + Iq0_GFL', - # e_str='PQsel * Iq_lim_p + (1 - PQsel) * Iqcmd_sat_val - Iq_GFL', - # ) - - - # --- Current Calculation (PLACEHOLDER - simplified) --- # GFM branch current magnitude (simplified) - self.IVSM_mag = Algeb(tex_name='I_{VSM,mag}', - info='GFM branch current magnitude (PLACEHOLDER)', - v_str='1e-4', - e_str='sqrt(Id_VSM_lim**2 + Iq_VSM_lim**2+1e-8) - IVSM_mag', # To be calculated - ) + self.IVSM_mag = VarService(tex_name='I_{VSM,mag}', + info='GFM branch current magnitude', + v_str='sqrt(Id_VSM_lim**2 + Iq_VSM_lim**2)', + ) # GFM branch current angle (simplified) - self.IVSM_ang = Algeb(tex_name=r'\phi_{VSM}', - info='GFM branch current angle (PLACEHOLDER)', - v_str='a', - e_str='a - atan2(Iq_VSM_lim, Id_VSM_lim + 1e-8) - IVSM_ang', # To be calculated - ) - + self.IVSM_ang = VarService(tex_name=r'\phi_{VSM}', + info='GFM branch current angle', + v_str='a - atan2(Iq_VSM_lim, Id_VSM_lim + 1e-8)', + ) + + # GFL branch current magnitude # ---------- dq -> xy(αβ) rotation for GFL current ---------- self.deltaV = VarService(v_str='a', tex_name=r'\delta_V', info='dq to xy rotation angle') # relative angle? @@ -883,11 +834,11 @@ def __init__(self, system, config): e_str='sqrt((Id_VSM + Ipcmd_sat_val)**2 + (Iq_VSM + Iqcmd_sat_val)**2 ) - Itotal', # Simplified, should be vector sum ) - self.k_scale = VarService( - v_str='1.0 + Indicator(Itotal > Imax) * (Itotal / (Imax) - 1.0)', - tex_name='k', - info='Scaling factor (>=1) for total current limiting' - ) + self.k_scale = VarService( + v_str='1.0 + Indicator(Itotal > Imax_dev) * (Itotal / (Imax_dev) - 1.0)', + tex_name='k', + info='Scaling factor (>=1) for total current limiting' + ) self.k_scale_out = Algeb(tex_name='k_{scale}', @@ -913,21 +864,21 @@ def __init__(self, system, config): ) - self.Id_VSM_lim = Algeb( - name='Id_VSM_lim', - v_str='0.0', - e_str='((Ed_VSM_lim - v) * Rs + Eq_VSM_lim * Xs) / Zs2 - Id_VSM_lim', - tex_name='I_{d,VSM}^{lim}', - info='Limited GFM d-axis current' - ) + self.Id_VSM_lim = Algeb( + name='Id_VSM_lim', + v_str='0.0', + e_str='((Ed_VSM_lim - v) * Rs_dev + Eq_VSM_lim * Xs_dev) / Zs2_dev - Id_VSM_lim', + tex_name='I_{d,VSM}^{lim}', + info='Limited GFM d-axis current' + ) - self.Iq_VSM_lim = Algeb( - name='Iq_VSM_lim', - v_str='0.0', - e_str='((Ed_VSM_lim - v) * Xs - Eq_VSM_lim * Rs) / Zs2 - Iq_VSM_lim', - tex_name='I_{q,VSM}^{lim}', - info='Limited GFM q-axis current' - ) + self.Iq_VSM_lim = Algeb( + name='Iq_VSM_lim', + v_str='0.0', + e_str='((Ed_VSM_lim - v) * Xs_dev - Eq_VSM_lim * Rs_dev) / Zs2_dev - Iq_VSM_lim', + tex_name='I_{q,VSM}^{lim}', + info='Limited GFM q-axis current' + ) self.EVSM_lim = Algeb( tex_name='E_{VSM}^{lim}', @@ -1011,18 +962,18 @@ def __init__(self, system, config): e_str='EVSM * sin(dVSM - a) - Eq_VSM', ) - self.Id_VSM = Algeb(tex_name='I_{d,VSM}', - info='GFM d-axis current', - v_str='0', - e_str='((EVSM * cos(dVSM - a) - v) * Rs + EVSM * sin(dVSM - a) * Xs) / Zs2 - Id_VSM', - ) + self.Id_VSM = Algeb(tex_name='I_{d,VSM}', + info='GFM d-axis current', + v_str='0', + e_str='((EVSM * cos(dVSM - a) - v) * Rs_dev + EVSM * sin(dVSM - a) * Xs_dev) / Zs2_dev - Id_VSM', + ) # should be negative? - self.Iq_VSM = Algeb(tex_name='I_{q,VSM}', - info='GFM q-axis current', - v_str='0', - e_str='-(EVSM * sin(dVSM - a) * Rs - (EVSM * cos(dVSM - a) - v) * Xs) / Zs2 - Iq_VSM', - ) + self.Iq_VSM = Algeb(tex_name='I_{q,VSM}', + info='GFM q-axis current', + v_str='0', + e_str='-(EVSM * sin(dVSM - a) * Rs_dev - (EVSM * cos(dVSM - a) - v) * Xs_dev) / Zs2_dev - Iq_VSM', + ) # self.Id_VSM = Algeb(tex_name='I_{d,VSM}', # info='GFM d-axis current', @@ -1045,17 +996,17 @@ def __init__(self, system, config): self.Vd = VarService(v_str='v*cos(a)') self.Vq = VarService(v_str='v*sin(a)') - self.PGFM = Algeb(tex_name='P_{GFM}', - info='GFM branch active power at bus', - v_str='0', - e_str='v * Id_VSM_lim - PGFM', - ) + self.PGFM = Algeb(tex_name='P_{GFM}', + info='GFM branch active power at bus', + v_str='0', + e_str='v * Id_VSM_lim * SnSb - PGFM', + ) - self.QGFM = Algeb(tex_name='Q_{GFM}', - info='GFM branch reactive power at bus', - v_str='0', - e_str='v * Iq_VSM_lim - QGFM', - ) + self.QGFM = Algeb(tex_name='Q_{GFM}', + info='GFM branch reactive power at bus', + v_str='0', + e_str='v * Iq_VSM_lim * SnSb - QGFM', + ) # self.PGFM = Algeb(tex_name='P_{GFM}', # info='GFM branch active power at bus', # v_str='0', @@ -1081,36 +1032,73 @@ def __init__(self, system, config): # v_str='v*sin(a)*(Id0_GFL)- v*cos(a)*(kqv * (Vref0 - v) + Iq0_GFL)', # e_str='Vq * Ip_GFL_lim - Vd *Iq_GFL_lim - QGFL', # ) - self.PGFL = Algeb(tex_name='P_{GFL}', - info='GFL branch active power', - v_str='v* Id0_GFL', - e_str='v * Ip_GFL_lim - PGFL', - ) + self.PGFL = Algeb(tex_name='P_{GFL}', + info='GFL branch active power', + v_str='v * Id0_GFL * SnSb', + e_str='v * Ip_GFL_lim * SnSb - PGFL', + ) + + self.QGFL = Algeb(tex_name='Q_{GFL}', + info='GFL branch reactive power', + v_str='v * (kqv * (Vref0 - v) + Iq0_GFL) * SnSb', + e_str='v * Iq_GFL_lim * SnSb - QGFL', + ) + + # Device-base branch powers for internal monitoring/debugging. + # The original PGFM/QGFM/PGFL/QGFL stay on system base for network balance. + self.PGFM_dev = Algeb(tex_name=r'P_{GFM,dev}', + info='GFM branch active power on device base', + v_str='PGFM * SbSn', + e_str='PGFM * SbSn - PGFM_dev', + ) - self.QGFL = Algeb(tex_name='Q_{GFL}', - info='GFL branch reactive power', - v_str='v*(kqv * (Vref0 - v) + Iq0_GFL)', - e_str='v *Iq_GFL_lim - QGFL', - ) - + self.QGFM_dev = Algeb(tex_name=r'Q_{GFM,dev}', + info='GFM branch reactive power on device base', + v_str='QGFM * SbSn', + e_str='QGFM * SbSn - QGFM_dev', + ) + + self.PGFL_dev = Algeb(tex_name=r'P_{GFL,dev}', + info='GFL branch active power on device base', + v_str='PGFL * SbSn', + e_str='PGFL * SbSn - PGFL_dev', + ) + + self.QGFL_dev = Algeb(tex_name=r'Q_{GFL,dev}', + info='GFL branch reactive power on device base', + v_str='QGFL * SbSn', + e_str='QGFL * SbSn - QGFL_dev', + ) # Total power injection self.Pe = Algeb(tex_name='P_e', info='Total active power injection', - v_str='p0', + v_str='p0_sys', e_str='PGFM + PGFL - Pe', ) - self.Qe = Algeb(tex_name='Q_e', - info='Total reactive power injection', - v_str='q0', - e_str='QGFM + QGFL - Qe', - ) - - def v_numeric(self, **kwargs): - """ - Disable the corresponding StaticGen. - """ + self.Qe = Algeb(tex_name='Q_e', + info='Total reactive power injection', + v_str='q0_sys', + e_str='QGFM + QGFL - Qe', + ) + + self.Pe_dev = Algeb(tex_name=r'P_{e,dev}', + info='Total active power injection on device base', + v_str='p0', + e_str='Pe * SbSn - Pe_dev', + ) + + self.Qe_dev = Algeb(tex_name=r'Q_{e,dev}', + info='Total reactive power injection on device base', + v_str='q0', + e_str='Qe * SbSn - Qe_dev', + ) + + def v_numeric(self, **kwargs): + """ + Disable the corresponding StaticGen. + """ self.system.groups['StaticGen'].set(src='u', idx=self.gen.v, attr='v', value=0) diff --git a/andes/models/renewable/repcgfmc1.py b/andes/models/renewable/repcgfmc1.py index 6516fc92b..568f7aa9b 100644 --- a/andes/models/renewable/repcgfmc1.py +++ b/andes/models/renewable/repcgfmc1.py @@ -33,7 +33,10 @@ def __init__(self): info='Optional remote bus for measurements', default=None, ) - + self.Sn = NumParam(default=100.0, tex_name='S_n', + info='Model MVA base', + unit='MVA', + ) self.busf = IdxParam(model='BusFreq', info='Optional BusFreq device (if None, auto-created)', default=None, @@ -229,7 +232,7 @@ def __init__(self): unit='s', ) - self.FFRFlag = NumParam(default=0.0, + self.FFRFlag = NumParam(default=1.0, tex_name='FFR_{Flag}', info='FFR flag (0 or 1)', unit='bool', @@ -248,13 +251,13 @@ def __init__(self): ) # --- GFL Reactive Power Path Parameters --- - self.Qref_max = NumParam(default=0.3122, + self.Qref_max = NumParam(default=0.6, # sometimes need to adjust tex_name='Q_{ref,max}', info='Maximum reactive power reference', unit='p.u.', ) - self.Qref_min = NumParam(default=-0.3122, + self.Qref_min = NumParam(default=-0.6, tex_name='Q_{ref,min}', info='Minimum reactive power reference', unit='p.u.', @@ -284,13 +287,13 @@ def __init__(self): unit='p.u.', ) - self.dbVLI = NumParam(default=-0.001, + self.dbVLI = NumParam(default=-0.01, tex_name='db_{VLI}', info='Voltage deadband lower limit', unit='p.u.', ) - self.dbVHI = NumParam(default=0.001, + self.dbVHI = NumParam(default=0.01, tex_name='db_{VHI}', info='Voltage deadband upper limit', unit='p.u.', @@ -312,25 +315,25 @@ def __init__(self): unit='s', ) - self.Qvc_max = NumParam(default=0.31, + self.Qvc_max = NumParam(default=0.6, # sometimes need to adjust(same with Qref_max) tex_name='Q_{vc,max}', info='Maximum voltage control output', unit='p.u.', ) - self.Qvc_min = NumParam(default=-0.31, + self.Qvc_min = NumParam(default=-0.6, tex_name='Q_{vc,min}', info='Minimum voltage control output', unit='p.u.', ) - self.Qcmd_GFL_max = NumParam(default=0.8, + self.Qcmd_GFL_max = NumParam(default=0.6, tex_name='Q_{cmd,GFL,max}', info='Maximum reactive power command for GFL', unit='p.u.', ) - self.Qcmd_GFL_min = NumParam(default=-0.8, + self.Qcmd_GFL_min = NumParam(default=-0.6, tex_name='Q_{cmd,GFL,min}', info='Minimum reactive power command for GFL', unit='p.u.', @@ -487,6 +490,9 @@ def __init__(self, system, config): self.Vref0= ExtService(model='RenGen', src='Vref0', indexer=self.reg, tex_name='V_{ref0}', # MODIFIED info='Vref0 of REGFMC1', ) + self.regSn = ExtParam(model='RenGen', src='Sn', indexer=self.reg, export=False, + info='REGFMC1 model base MVA', + ) self.p0 = ExtService(model='RenGen', src='p0', indexer=self.reg, tex_name='P_0', info='Initial active power of REGFMC1', ) @@ -494,6 +500,18 @@ def __init__(self, system, config): self.q0 = ExtService(model='RenGen', src='q0', indexer=self.reg, tex_name='Q_0', info='Initial reactive power of REGFMC1', ) + self.SbSn = ConstService(v_str='sys_mva / regSn', + tex_name=r'S_b/S_n', + info='System-base to device-base power factor', + ) + self.Pe_dev = VarService(v_str='Pe * SbSn', + tex_name=r'P_{e,dev}', + info='Active power output on device base', + ) + self.Qe_dev = VarService(v_str='Qe * SbSn', + tex_name=r'Q_{e,dev}', + info='Reactive power output on device base', + ) # Internal reference values from power flow self.Pref_site_0 = ConstService(v_str='p0', @@ -768,7 +786,7 @@ def __init__(self, system, config): # p target self.Ptarget_1_initial = Algeb(tex_name='P_{target1_initial}', # modified info='active power P target_initial', - v_str='Pe ', + v_str='Pe_dev', e_str='Pfreq_droop_lim+ Pref_site+ FFRCSW_s1*P_FFR - Ptarget_1_initial', ) self.Ptarget_1_lim = Limiter(self.Ptarget_1_initial, lower=self.Pref_min, upper=self.Pref_max, # modified @@ -776,11 +794,11 @@ def __init__(self, system, config): ) self.Ptarget_1 = Algeb(tex_name='P_{target1}', # modified info='active power P target', - v_str='Pe', + v_str='Pe_dev', e_str='Ptarget_1_initial * Ptarget_1_lim_zi + Pref_max * Ptarget_1_lim_zu + Pref_min * Ptarget_1_lim_zl - Ptarget_1', ) # Site power measurement - self.Psite = Lag(u='Pe', T=self.Tfrq, K=1, + self.Psite = Lag(u='Pe_dev', T=self.Tfrq, K=1, info='Site power measurement', tex_name='P_{site}', ) @@ -943,7 +961,7 @@ def __init__(self, system, config): # ) # Reactive power control with lag - self.Qsite_filt = Lag(u='Qe', T=self.Tqlag, K=1, + self.Qsite_filt = Lag(u='Qe_dev', T=self.Tqlag, K=1, info='Site reactive power measurement', tex_name='Q_{site,filt}', ) From eac2cd4e75dea8f010bedee841bbcdcb7949fddd Mon Sep 17 00:00:00 2001 From: Zilin Zhuang Date: Tue, 28 Apr 2026 15:29:17 -0400 Subject: [PATCH 4/6] hybrid converter model V3.2, fixing some model bugs --- andes/models/renewable/regfmc1.py | 331 ++++++++++++---------------- andes/models/renewable/repcgfmc1.py | 54 +++-- 2 files changed, 171 insertions(+), 214 deletions(-) diff --git a/andes/models/renewable/regfmc1.py b/andes/models/renewable/regfmc1.py index afb69a3ba..6740fcd3e 100644 --- a/andes/models/renewable/regfmc1.py +++ b/andes/models/renewable/regfmc1.py @@ -69,22 +69,25 @@ def __init__(self): unit='s', ) - self.kq = NumParam(default=1, - tex_name='k_q', - info='Reactive power gain in voltage control', - ) + self.kq = NumParam(default=1, + tex_name='k_q', + info='Reactive power gain in voltage control', + current=True, + ) self.mq = NumParam(default=0.4, tex_name='m_q', info='Reactive power measurement gain', ) - self.kpE = NumParam(default=0.333, - tex_name='k_{pE}', - info='Proportional gain for voltage magnitude error', - ) - self.kiE = NumParam(default=3.333, - tex_name='k_{iE}', - info='Integral gain for voltage magnitude error', - ) + self.kpE = NumParam(default=0.333, + tex_name='k_{pE}', + info='Proportional gain for voltage magnitude error', + ipower=True, + ) + self.kiE = NumParam(default=3.333, + tex_name='k_{iE}', + info='Integral gain for voltage magnitude error', + ipower=True, + ) self.Tvsm = NumParam(default=0.318, tex_name='T_{vsm}', info='Time constant for voltage controller output filter', @@ -116,10 +119,11 @@ def __init__(self): unit='s', ) - self.mp = NumParam(default=0.041667, - tex_name='m_p', - info='Active power droop gain', - ) + self.mp = NumParam(default=0.041667, + tex_name='m_p', + info='Active power droop gain', + ipower=True, + ) self.Tomegacmd = NumParam(default=0.02, tex_name=r'T_{\omega cmd}', info='Time constant for omega command filter', @@ -130,19 +134,22 @@ def __init__(self): info='Time constant for power measurement filter', unit='s', ) - self.Hs = NumParam(default=1.6, - tex_name='H_s', - info='Inertia constant (2H)', - unit='s', - ) - self.D1 = NumParam(default=5, - tex_name='D_1', - info='Primary damping coefficient', - ) - self.D2 = NumParam(default=90/3.14, - tex_name='D_2', - info='Secondary damping coefficient', - ) + self.Hs = NumParam(default=1.6, + tex_name='H_s', + info='Inertia constant (2H)', + unit='s', + power=True, + ) + self.D1 = NumParam(default=5, + tex_name='D_1', + info='Primary damping coefficient', + power=True, + ) + self.D2 = NumParam(default=90/3.14, + tex_name='D_2', + info='Secondary damping coefficient', + power=True, + ) self.omegaD = NumParam(default=3.14, tex_name=r'\omega_D', info='Damping filter frequency', @@ -156,14 +163,16 @@ def __init__(self): tex_name=r'\Delta\omega_{min}', info='Minimum frequency deviation (PLACEHOLDER)', ) - self.dPGFMmax = NumParam(default=1.36, - tex_name=r'\Delta P_{GFM,max}', - info='Maximum active power deviation for GFM (PLACEHOLDER)', - ) - self.dPGFMmin = NumParam(default=-1.36, - tex_name=r'\Delta P_{GFM,min}', - info='Minimum active power deviation for GFM (PLACEHOLDER)', - ) + self.dPGFMmax = NumParam(default=1.36, + tex_name=r'\Delta P_{GFM,max}', + info='Maximum active power deviation for GFM (PLACEHOLDER)', + power=True, + ) + self.dPGFMmin = NumParam(default=-1.36, + tex_name=r'\Delta P_{GFM,min}', + info='Minimum active power deviation for GFM (PLACEHOLDER)', + power=True, + ) self.Tpf = NumParam(default=0.02, tex_name='T_{pf}', info='Time constant for frequency flag filter', @@ -181,10 +190,11 @@ def __init__(self): info='Time constant for voltage filter in GFL', unit='s', ) - self.kqv = NumParam(default=2, # modified - tex_name='k_{qv}', - info='Voltage error gain in GFL', - ) + self.kqv = NumParam(default=2, # modified + tex_name='k_{qv}', + info='Voltage error gain in GFL', + current=True, + ) self.dbVLI = NumParam(default=-0.12, tex_name='db_{VLI}', info='Voltage deadband lower limit (PLACEHOLDER)', @@ -215,29 +225,32 @@ def __init__(self): info='Minimum voltage for current limiting (PLACEHOLDER)', ) - - self.Pcmd_GFL_max = NumParam(default=1.2, - tex_name='P_{cmd,GFL,max}', - info='Maximum active power command for GFL', - unit='p.u.', - ) - - self.Pcmd_GFL_min = NumParam(default=-1.0, - tex_name='P_{cmd,GFL,min}', - info='Minimum active power command for GFL', - unit='p.u.', - ) - self.Qcmd_GFL_max = NumParam(default=0.6, - tex_name='Q_{cmd,GFL,max}', - info='Maximum reactive power command for GFL', - unit='p.u.', - ) - - self.Qcmd_GFL_min = NumParam(default=-0.6, - tex_name='Q_{cmd,GFL,min}', - info='Minimum reactive power command for GFL', - unit='p.u.', - ) + self.Pcmd_GFL_max = NumParam(default=1.2, + tex_name='P_{cmd,GFL,max}', + info='Maximum active power command for GFL', + unit='p.u.', + power=True, + ) + + self.Pcmd_GFL_min = NumParam(default=-1.0, + tex_name='P_{cmd,GFL,min}', + info='Minimum active power command for GFL', + unit='p.u.', + power=True, + ) + self.Qcmd_GFL_max = NumParam(default=0.6, + tex_name='Q_{cmd,GFL,max}', + info='Maximum reactive power command for GFL', + unit='p.u.', + power=True, + ) + + self.Qcmd_GFL_min = NumParam(default=-0.6, + tex_name='Q_{cmd,GFL,min}', + info='Minimum reactive power command for GFL', + unit='p.u.', + power=True, + ) class REGFMC1Model(Model): """ @@ -279,46 +292,26 @@ def __init__(self, system, config): info='Total Q of the static gen', ) - # --- Initialization Services --- - # Keep both system-base and device-base copies so the network-facing - # power balance remains on system base while the controller uses - # device-base references internally. - self.SbSn = ConstService(v_str='sys_mva / Sn', - tex_name=r'S_b/S_n', - info='System-base to device-base power factor', - ) - self.SnSb = ConstService(v_str='Sn / sys_mva', - tex_name=r'S_n/S_b', - info='Device-base to system-base power factor', - ) - self.p0_sys = ConstService(v_str='gammap * p0s', - tex_name=r'P_{0,sys}', - info='Initial P for this device on system base', - ) - self.q0_sys = ConstService(v_str='gammaq * q0s', - tex_name=r'Q_{0,sys}', - info='Initial Q for this device on system base', - ) - self.p0 = ConstService(v_str='p0_sys * SbSn', + # --- Initialization Services --- + # Power, current, and impedance parameters marked with ``power=True``, + # ``current=True``, and ``z=True`` are converted by ANDES to system base. + # Keep the internal controller equations on system base as well. + self.p0 = ConstService(v_str='gammap * p0s', tex_name='P_0', - info='Initial P for this device on device base', + info='Initial P for this device on system base', ) - self.q0 = ConstService(v_str='q0_sys * SbSn', + self.q0 = ConstService(v_str='gammaq * q0s', tex_name='Q_0', - info='Initial Q for this device on device base', + info='Initial Q for this device on system base', ) - self.Imax_dev = ConstService(v_str='Imax * SbSn', - tex_name=r'I_{max,dev}', - info='Maximum total output current on device base', - ) # Initial current calculations (for both branches) self.Id0_GFL = ConstService(tex_name=r'I_{d0,GFL}', - v_str='u * p0 / v', - ) - self.Iq0_GFL = ConstService(tex_name=r'I_{q0,GFL}', - v_str='u * q0 / v', - ) + v_str='u * p0 / v', + ) + self.Iq0_GFL = ConstService(tex_name=r'I_{q0,GFL}', + v_str='u * q0 / v', + ) # Damping washout time constant (1 / omegaD) self.Tdamp = ConstService(tex_name=r'T_{damp}', @@ -335,20 +328,8 @@ def __init__(self, system, config): # GFM branch impedance squared (for current calculation) self.Zs2 = ConstService(v_str='Rs**2 + Xs**2', tex_name='Z_s^2', - info='GFM series impedance magnitude squared', + info='GFM series impedance magnitude squared on system base', ) - self.Rs_dev = ConstService(v_str='Rs * SnSb', - tex_name=r'R_{s,dev}', - info='GFM series resistance on device base', - ) - self.Xs_dev = ConstService(v_str='Xs * SnSb', - tex_name=r'X_{s,dev}', - info='GFM series reactance on device base', - ) - self.Zs2_dev = ConstService(v_str='Rs_dev**2 + Xs_dev**2', - tex_name=r'Z_{s,dev}^2', - info='GFM series impedance magnitude squared on device base', - ) # --- External reference variables (to be controlled by plant controller) --- # GFM voltage reference (external input to voltage control) # mistake @@ -641,17 +622,17 @@ def __init__(self, system, config): ) # Current commands (PLACEHOLDER - TODO: add limiters and PQ priority) - self.Ipcmd_GFL = Algeb(tex_name='I_{pcmd,GFL}', - info='Active current command for GFL', - v_str='Id0_GFL', - e_str='Pcmd_GFL / v - Ipcmd_GFL', - ) - - self.Iqcmd_GFL = Algeb(tex_name='I_{qcmd,GFL}', - info='Reactive current command for GFL', - v_str='kqv * Verr_GFL_dbd_y + Iq0_GFL', - e_str='kqv * Verr_GFL_dbd_y + Qcmd_GFL / v - Iqcmd_GFL', - ) + self.Ipcmd_GFL = Algeb(tex_name='I_{pcmd,GFL}', + info='Active current command for GFL', + v_str='Id0_GFL', + e_str='Pcmd_GFL / v - Ipcmd_GFL', + ) + + self.Iqcmd_GFL = Algeb(tex_name='I_{qcmd,GFL}', + info='Reactive current command for GFL', + v_str='kqv * Verr_GFL_dbd_y + Iq0_GFL', + e_str='kqv * Verr_GFL_dbd_y + Qcmd_GFL / v - Iqcmd_GFL', + ) # GFL PQ Priority Current Limiting @@ -682,35 +663,35 @@ def __init__(self, system, config): # ) self.Ipmaxsq0_GFL1 = ConstService( - v_str='Piecewise((0, Le(Imax_dev**2 - (kqv * (Vref0 - v) + Iq0_GFL) **2, 0.0)), (Imax_dev**2 - (kqv * (Vref0 - v) + Iq0_GFL)**2 , True), evaluate=False)' + v_str='Piecewise((0, Le(Imax**2 - (kqv * (Vref0 - v) + Iq0_GFL) **2, 0.0)), (Imax**2 - (kqv * (Vref0 - v) + Iq0_GFL)**2 , True), evaluate=False)' ) self.Iqmaxsq0_GFL1 = ConstService( - v_str='Piecewise((0, Le(Imax_dev**2 - Id0_GFL**2, 0.0)), (Imax_dev**2 - Id0_GFL**2, True), evaluate=False)' + v_str='Piecewise((0, Le(Imax**2 - Id0_GFL**2, 0.0)), (Imax**2 - Id0_GFL**2, True), evaluate=False)' ) self.Ipmaxsq_GFL1 = VarService( - v_str='Piecewise((0, Le(Imax_dev**2 - Iqcmd_sat_val**2, 0.0)), (Imax_dev**2 - Iqcmd_sat_val**2, True), evaluate=False)', + v_str='Piecewise((0, Le(Imax**2 - Iqcmd_sat_val**2, 0.0)), (Imax**2 - Iqcmd_sat_val**2, True), evaluate=False)', tex_name='I_{p,max,GFL}^2', ) self.Iqmaxsq_GFL1 = VarService( - v_str='Piecewise((0, Le(Imax_dev**2 - Ipcmd_sat_val**2, 0.0)), (Imax_dev**2 - Ipcmd_sat_val**2, True), evaluate=False)', + v_str='Piecewise((0, Le(Imax**2 - Ipcmd_sat_val**2, 0.0)), (Imax**2 - Ipcmd_sat_val**2, True), evaluate=False)', tex_name='I_{q,max,GFL}^2', ) - + self.Ipmax_GFL1 = Algeb( tex_name='I_{p,max,GFL}', info='Ipmax_GFL for Current Limiting Algorithm', - v_str='PQFlag * Imax_dev + (1 - PQFlag) * sqrt(Ipmaxsq0_GFL1)', - e_str='PQFlag * Imax_dev + (1 - PQFlag) * sqrt(Ipmaxsq_GFL1) - Ipmax_GFL1' + v_str='PQFlag * Imax + (1 - PQFlag) * sqrt(Ipmaxsq0_GFL1)', + e_str='PQFlag * Imax + (1 - PQFlag) * sqrt(Ipmaxsq_GFL1) - Ipmax_GFL1' ) self.Iqmax_GFL1 = Algeb( tex_name='I_{q,max,GFL}', info='Iqmax_GFL for Current Limiting Algorithm', - v_str='(1 - PQFlag) * Imax_dev + PQFlag * sqrt(Iqmaxsq0_GFL1)', - e_str='(1 - PQFlag) * Imax_dev + PQFlag * sqrt(Iqmaxsq_GFL1) - Iqmax_GFL1' + v_str='(1 - PQFlag) * Imax + PQFlag * sqrt(Iqmaxsq0_GFL1)', + e_str='(1 - PQFlag) * Imax + PQFlag * sqrt(Iqmaxsq_GFL1) - Iqmax_GFL1' ) self.Ipmin_GFL1 = Algeb( @@ -835,7 +816,7 @@ def __init__(self, system, config): ) self.k_scale = VarService( - v_str='1.0 + Indicator(Itotal > Imax_dev) * (Itotal / (Imax_dev) - 1.0)', + v_str='1.0 + Indicator(Itotal > Imax) * (Itotal / Imax - 1.0)', tex_name='k', info='Scaling factor (>=1) for total current limiting' ) @@ -867,7 +848,7 @@ def __init__(self, system, config): self.Id_VSM_lim = Algeb( name='Id_VSM_lim', v_str='0.0', - e_str='((Ed_VSM_lim - v) * Rs_dev + Eq_VSM_lim * Xs_dev) / Zs2_dev - Id_VSM_lim', + e_str='((Ed_VSM_lim - v) * Rs + Eq_VSM_lim * Xs) / Zs2 - Id_VSM_lim', tex_name='I_{d,VSM}^{lim}', info='Limited GFM d-axis current' ) @@ -875,7 +856,7 @@ def __init__(self, system, config): self.Iq_VSM_lim = Algeb( name='Iq_VSM_lim', v_str='0.0', - e_str='((Ed_VSM_lim - v) * Xs_dev - Eq_VSM_lim * Rs_dev) / Zs2_dev - Iq_VSM_lim', + e_str='((Ed_VSM_lim - v) * Xs - Eq_VSM_lim * Rs) / Zs2 - Iq_VSM_lim', tex_name='I_{q,VSM}^{lim}', info='Limited GFM q-axis current' ) @@ -965,14 +946,14 @@ def __init__(self, system, config): self.Id_VSM = Algeb(tex_name='I_{d,VSM}', info='GFM d-axis current', v_str='0', - e_str='((EVSM * cos(dVSM - a) - v) * Rs_dev + EVSM * sin(dVSM - a) * Xs_dev) / Zs2_dev - Id_VSM', + e_str='((EVSM * cos(dVSM - a) - v) * Rs + EVSM * sin(dVSM - a) * Xs) / Zs2 - Id_VSM', ) # should be negative? self.Iq_VSM = Algeb(tex_name='I_{q,VSM}', info='GFM q-axis current', v_str='0', - e_str='-(EVSM * sin(dVSM - a) * Rs_dev - (EVSM * cos(dVSM - a) - v) * Xs_dev) / Zs2_dev - Iq_VSM', + e_str='-(EVSM * sin(dVSM - a) * Rs - (EVSM * cos(dVSM - a) - v) * Xs) / Zs2 - Iq_VSM', ) # self.Id_VSM = Algeb(tex_name='I_{d,VSM}', @@ -999,13 +980,13 @@ def __init__(self, system, config): self.PGFM = Algeb(tex_name='P_{GFM}', info='GFM branch active power at bus', v_str='0', - e_str='v * Id_VSM_lim * SnSb - PGFM', + e_str='v * Id_VSM_lim - PGFM', ) - + self.QGFM = Algeb(tex_name='Q_{GFM}', info='GFM branch reactive power at bus', v_str='0', - e_str='v * Iq_VSM_lim * SnSb - QGFM', + e_str='v * Iq_VSM_lim - QGFM', ) # self.PGFM = Algeb(tex_name='P_{GFM}', # info='GFM branch active power at bus', @@ -1034,71 +1015,33 @@ def __init__(self, system, config): # ) self.PGFL = Algeb(tex_name='P_{GFL}', info='GFL branch active power', - v_str='v * Id0_GFL * SnSb', - e_str='v * Ip_GFL_lim * SnSb - PGFL', + v_str='v * Id0_GFL', + e_str='v * Ip_GFL_lim - PGFL', ) self.QGFL = Algeb(tex_name='Q_{GFL}', info='GFL branch reactive power', - v_str='v * (kqv * (Vref0 - v) + Iq0_GFL) * SnSb', - e_str='v * Iq_GFL_lim * SnSb - QGFL', + v_str='v * (kqv * (Vref0 - v) + Iq0_GFL)', + e_str='v * Iq_GFL_lim - QGFL', ) - - # Device-base branch powers for internal monitoring/debugging. - # The original PGFM/QGFM/PGFL/QGFL stay on system base for network balance. - self.PGFM_dev = Algeb(tex_name=r'P_{GFM,dev}', - info='GFM branch active power on device base', - v_str='PGFM * SbSn', - e_str='PGFM * SbSn - PGFM_dev', - ) - - self.QGFM_dev = Algeb(tex_name=r'Q_{GFM,dev}', - info='GFM branch reactive power on device base', - v_str='QGFM * SbSn', - e_str='QGFM * SbSn - QGFM_dev', - ) - - self.PGFL_dev = Algeb(tex_name=r'P_{GFL,dev}', - info='GFL branch active power on device base', - v_str='PGFL * SbSn', - e_str='PGFL * SbSn - PGFL_dev', - ) - - self.QGFL_dev = Algeb(tex_name=r'Q_{GFL,dev}', - info='GFL branch reactive power on device base', - v_str='QGFL * SbSn', - e_str='QGFL * SbSn - QGFL_dev', - ) - - # Total power injection - self.Pe = Algeb(tex_name='P_e', - info='Total active power injection', - v_str='p0_sys', - e_str='PGFM + PGFL - Pe', - ) - + + # Total power injection + self.Pe = Algeb(tex_name='P_e', + info='Total active power injection', + v_str='p0', + e_str='PGFM + PGFL - Pe', + ) + self.Qe = Algeb(tex_name='Q_e', info='Total reactive power injection', - v_str='q0_sys', + v_str='q0', e_str='QGFM + QGFL - Qe', ) - - self.Pe_dev = Algeb(tex_name=r'P_{e,dev}', - info='Total active power injection on device base', - v_str='p0', - e_str='Pe * SbSn - Pe_dev', - ) - - self.Qe_dev = Algeb(tex_name=r'Q_{e,dev}', - info='Total reactive power injection on device base', - v_str='q0', - e_str='Qe * SbSn - Qe_dev', - ) - - def v_numeric(self, **kwargs): - """ - Disable the corresponding StaticGen. - """ + + def v_numeric(self, **kwargs): + """ + Disable the corresponding StaticGen. + """ self.system.groups['StaticGen'].set(src='u', idx=self.gen.v, attr='v', value=0) diff --git a/andes/models/renewable/repcgfmc1.py b/andes/models/renewable/repcgfmc1.py index 568f7aa9b..341e1d42a 100644 --- a/andes/models/renewable/repcgfmc1.py +++ b/andes/models/renewable/repcgfmc1.py @@ -89,24 +89,28 @@ def __init__(self): tex_name='P_{target}', info='Target active power', unit='p.u.', + power=True, ) self.Qtarget = NumParam(default=0.0, tex_name='Q_{target}', info='Target reactive power', unit='p.u.', + power=True, ) self.Rloss = NumParam(default=0.0, # modified 0-->0.01 assumption tex_name='R_{loss}', info='Loss compensation resistance', unit='p.u.', + z=True, ) self.Xloss = NumParam(default=0.0, # modified 0-->0.05assumption tex_name='X_{loss}', info='Loss compensation reactance', unit='p.u.', + z=True, ) self.TVmeas = NumParam(default=0.01, @@ -161,59 +165,69 @@ def __init__(self): self.Ddn = NumParam(default=20.0, tex_name='D_{dn}', info='Droop for frequency above deadband', + power=True, ) self.Dup = NumParam(default=20.0, tex_name='D_{up}', info='Droop for frequency below deadband', + power=True, ) self.Pfreq_max = NumParam(default=1, tex_name='P_{freq,max}', info='Maximum frequency droop output', unit='p.u.', + power=True, ) self.Pfreq_min = NumParam(default=-1, tex_name='P_{freq,min}', info='Minimum frequency droop output', unit='p.u.', + power=True, ) self.Pref_max = NumParam(default=1.0, tex_name='P_{ref,max}', info='Maximum site power reference', unit='p.u.', + power=True, ) self.Pref_min = NumParam(default=-1.0, tex_name='P_{ref,min}', info='Minimum site power reference', unit='p.u.', + power=True, ) self.Perr_rmax = NumParam(default=0.1, tex_name='P_{err,rmax}', info='Maximum power error for rate limiter', unit='p.u.', + power=True, ) self.Perr_rmin = NumParam(default=-0.1, tex_name='P_{err,rmin}', info='Minimum power error for rate limiter', unit='p.u.', + power=True, ) self.Perr_max = NumParam(default=0.1, tex_name='P_{err,max}', info='Maximum power error', unit='p.u.', + power=True, ) self.Perr_min = NumParam(default=-0.1, tex_name='P_{err,min}', info='Minimum power error', unit='p.u.', + power=True, ) self.Kip = NumParam(default=0.1, #0 @@ -242,25 +256,29 @@ def __init__(self): tex_name='P_{cmd,GFL,max}', info='Maximum active power command for GFL', unit='p.u.', + power=True, ) self.Pcmd_GFL_min = NumParam(default=-1.0, tex_name='P_{cmd,GFL,min}', info='Minimum active power command for GFL', unit='p.u.', + power=True, ) # --- GFL Reactive Power Path Parameters --- - self.Qref_max = NumParam(default=0.6, # sometimes need to adjust + self.Qref_max = NumParam(default=0.6, # sometimes need to adjust tex_name='Q_{ref,max}', info='Maximum reactive power reference', unit='p.u.', + power=True, ) self.Qref_min = NumParam(default=-0.6, tex_name='Q_{ref,min}', info='Minimum reactive power reference', unit='p.u.', + power=True, ) self.Kiq = NumParam(default=0.1, @@ -302,11 +320,13 @@ def __init__(self): self.Kp_vc = NumParam(default=2, # 40 tex_name='K_{p,vc}', info='Voltage control proportional gain', + power=True, ) self.Ki_vc = NumParam(default=6, # 2 tex_name='K_{i,vc}', info='Voltage control integral gain', + power=True, ) self.Tvc = NumParam(default=0.02, @@ -319,24 +339,28 @@ def __init__(self): tex_name='Q_{vc,max}', info='Maximum voltage control output', unit='p.u.', + power=True, ) self.Qvc_min = NumParam(default=-0.6, tex_name='Q_{vc,min}', info='Minimum voltage control output', unit='p.u.', + power=True, ) self.Qcmd_GFL_max = NumParam(default=0.6, tex_name='Q_{cmd,GFL,max}', info='Maximum reactive power command for GFL', unit='p.u.', + power=True, ) self.Qcmd_GFL_min = NumParam(default=-0.6, tex_name='Q_{cmd,GFL,min}', info='Minimum reactive power command for GFL', unit='p.u.', + power=True, ) self.VFlag = NumParam(default=1.0, @@ -354,11 +378,13 @@ def __init__(self): tex_name='Q_{err,max}', info='Maximum reactive power error limit', unit='p.u.', + power=True, ) self.Qerr_min = NumParam(default=-0.1, # modified tex_name='Q_{err,min}', info='Minimum reactive power error limit', unit='p.u.', + power=True, ) @@ -379,18 +405,21 @@ def __init__(self): tex_name='P_{FFR,low}', info='FFR power command when frequency is below fFFR_low', unit='p.u.', + power=True, ) self.PFFR_high = NumParam(default=-0.05, tex_name='P_{FFR,high}', info='FFR power command when frequency is above fFFR_high', unit='p.u.', + power=True, ) self.DFFR = NumParam(default=0.01, tex_name='D_{FFR}', info='Ramp rate for FFR to quit operation', unit='p.u./s', + power=True, ) self.TFFR = NumParam(default=20, # 300 @@ -490,9 +519,6 @@ def __init__(self, system, config): self.Vref0= ExtService(model='RenGen', src='Vref0', indexer=self.reg, tex_name='V_{ref0}', # MODIFIED info='Vref0 of REGFMC1', ) - self.regSn = ExtParam(model='RenGen', src='Sn', indexer=self.reg, export=False, - info='REGFMC1 model base MVA', - ) self.p0 = ExtService(model='RenGen', src='p0', indexer=self.reg, tex_name='P_0', info='Initial active power of REGFMC1', ) @@ -500,18 +526,6 @@ def __init__(self, system, config): self.q0 = ExtService(model='RenGen', src='q0', indexer=self.reg, tex_name='Q_0', info='Initial reactive power of REGFMC1', ) - self.SbSn = ConstService(v_str='sys_mva / regSn', - tex_name=r'S_b/S_n', - info='System-base to device-base power factor', - ) - self.Pe_dev = VarService(v_str='Pe * SbSn', - tex_name=r'P_{e,dev}', - info='Active power output on device base', - ) - self.Qe_dev = VarService(v_str='Qe * SbSn', - tex_name=r'Q_{e,dev}', - info='Reactive power output on device base', - ) # Internal reference values from power flow self.Pref_site_0 = ConstService(v_str='p0', @@ -786,7 +800,7 @@ def __init__(self, system, config): # p target self.Ptarget_1_initial = Algeb(tex_name='P_{target1_initial}', # modified info='active power P target_initial', - v_str='Pe_dev', + v_str='Pe', e_str='Pfreq_droop_lim+ Pref_site+ FFRCSW_s1*P_FFR - Ptarget_1_initial', ) self.Ptarget_1_lim = Limiter(self.Ptarget_1_initial, lower=self.Pref_min, upper=self.Pref_max, # modified @@ -794,11 +808,11 @@ def __init__(self, system, config): ) self.Ptarget_1 = Algeb(tex_name='P_{target1}', # modified info='active power P target', - v_str='Pe_dev', + v_str='Pe', e_str='Ptarget_1_initial * Ptarget_1_lim_zi + Pref_max * Ptarget_1_lim_zu + Pref_min * Ptarget_1_lim_zl - Ptarget_1', ) # Site power measurement - self.Psite = Lag(u='Pe_dev', T=self.Tfrq, K=1, + self.Psite = Lag(u='Pe', T=self.Tfrq, K=1, info='Site power measurement', tex_name='P_{site}', ) @@ -961,7 +975,7 @@ def __init__(self, system, config): # ) # Reactive power control with lag - self.Qsite_filt = Lag(u='Qe_dev', T=self.Tqlag, K=1, + self.Qsite_filt = Lag(u='Qe', T=self.Tqlag, K=1, info='Site reactive power measurement', tex_name='Q_{site,filt}', ) From b26e754a7e480e1d50dea04835fd91ffa684c886 Mon Sep 17 00:00:00 2001 From: Zilin Zhuang Date: Mon, 1 Jun 2026 10:19:10 -0400 Subject: [PATCH 5/6] complete the model --- .codex | 0 andes/cases/smib/regfmc1_smib_with_plant.xlsx | Bin 7855 -> 13589 bytes andes/models/renewable/regfmc1.py | 601 ++++++------------ andes/models/renewable/repcgfmc1.py | 496 +++++++++------ .../renewable/test_regfmc1_repcgfmc1.py | 111 ++++ 5 files changed, 612 insertions(+), 596 deletions(-) create mode 100644 .codex create mode 100644 tests/models/renewable/test_regfmc1_repcgfmc1.py diff --git a/.codex b/.codex new file mode 100644 index 000000000..e69de29bb diff --git a/andes/cases/smib/regfmc1_smib_with_plant.xlsx b/andes/cases/smib/regfmc1_smib_with_plant.xlsx index 85e251a0d71457714104120ca4a2bebdc2c897ff..6d08fa4c943c38ab46934cbe477e118360d9ef5f 100644 GIT binary patch literal 13589 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 literal 7855 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 diff --git a/andes/models/renewable/regfmc1.py b/andes/models/renewable/regfmc1.py index 6740fcd3e..7e044e7bc 100644 --- a/andes/models/renewable/regfmc1.py +++ b/andes/models/renewable/regfmc1.py @@ -26,7 +26,7 @@ def __init__(self): # --- General Parameters --- self.bus = IdxParam(model='Bus', - info="Interface bus id", + info="Inverter terminal/interface bus id", mandatory=True, ) self.gen = IdxParam(info="Static generator index", @@ -69,25 +69,25 @@ def __init__(self): unit='s', ) - self.kq = NumParam(default=1, - tex_name='k_q', - info='Reactive power gain in voltage control', - current=True, - ) + self.kq = NumParam(default=1, + tex_name='k_q', + info='Reactive power gain in voltage control', + current=True, + ) self.mq = NumParam(default=0.4, tex_name='m_q', info='Reactive power measurement gain', ) - self.kpE = NumParam(default=0.333, - tex_name='k_{pE}', - info='Proportional gain for voltage magnitude error', - ipower=True, - ) - self.kiE = NumParam(default=3.333, - tex_name='k_{iE}', - info='Integral gain for voltage magnitude error', - ipower=True, - ) + self.kpE = NumParam(default=0.333, + tex_name='k_{pE}', + info='Proportional gain for voltage magnitude error', + ipower=True, + ) + self.kiE = NumParam(default=3.333, + tex_name='k_{iE}', + info='Integral gain for voltage magnitude error', + ipower=True, + ) self.Tvsm = NumParam(default=0.318, tex_name='T_{vsm}', info='Time constant for voltage controller output filter', @@ -119,11 +119,11 @@ def __init__(self): unit='s', ) - self.mp = NumParam(default=0.041667, - tex_name='m_p', - info='Active power droop gain', - ipower=True, - ) + self.mp = NumParam(default=0.041667, + tex_name='m_p', + info='Active power droop gain', + ipower=True, + ) self.Tomegacmd = NumParam(default=0.02, tex_name=r'T_{\omega cmd}', info='Time constant for omega command filter', @@ -134,22 +134,22 @@ def __init__(self): info='Time constant for power measurement filter', unit='s', ) - self.Hs = NumParam(default=1.6, - tex_name='H_s', - info='Inertia constant (2H)', - unit='s', - power=True, - ) - self.D1 = NumParam(default=5, - tex_name='D_1', - info='Primary damping coefficient', - power=True, - ) - self.D2 = NumParam(default=90/3.14, - tex_name='D_2', - info='Secondary damping coefficient', - power=True, - ) + self.Hs = NumParam(default=1.6, + tex_name='H_s', + info='Inertia constant (2H)', + unit='s', + power=True, + ) + self.D1 = NumParam(default=5, + tex_name='D_1', + info='Primary damping coefficient', + power=True, + ) + self.D2 = NumParam(default=90/3.14, + tex_name='D_2', + info='Secondary damping coefficient', + power=True, + ) self.omegaD = NumParam(default=3.14, tex_name=r'\omega_D', info='Damping filter frequency', @@ -163,24 +163,24 @@ def __init__(self): tex_name=r'\Delta\omega_{min}', info='Minimum frequency deviation (PLACEHOLDER)', ) - self.dPGFMmax = NumParam(default=1.36, - tex_name=r'\Delta P_{GFM,max}', - info='Maximum active power deviation for GFM (PLACEHOLDER)', - power=True, - ) - self.dPGFMmin = NumParam(default=-1.36, - tex_name=r'\Delta P_{GFM,min}', - info='Minimum active power deviation for GFM (PLACEHOLDER)', - power=True, - ) + self.dPGFMmax = NumParam(default=1.36, + tex_name=r'\Delta P_{GFM,max}', + info='Maximum active power deviation for GFM (PLACEHOLDER)', + power=True, + ) + self.dPGFMmin = NumParam(default=-1.36, + tex_name=r'\Delta P_{GFM,min}', + info='Minimum active power deviation for GFM (PLACEHOLDER)', + power=True, + ) self.Tpf = NumParam(default=0.02, tex_name='T_{pf}', info='Time constant for frequency flag filter', unit='s', ) - self.FFFlag = NumParam(default=0.0, + self.FFFlag = NumParam(default=1.0, tex_name='FF_{Flag}', - info='Frequency flag (0 or 1)', + info='Frequency droop flag; 0-disable, 1-enable', unit='bool', ) @@ -190,11 +190,11 @@ def __init__(self): info='Time constant for voltage filter in GFL', unit='s', ) - self.kqv = NumParam(default=2, # modified - tex_name='k_{qv}', - info='Voltage error gain in GFL', - current=True, - ) + self.kqv = NumParam(default=2, # modified + tex_name='k_{qv}', + info='Voltage error gain in GFL', + current=True, + ) self.dbVLI = NumParam(default=-0.12, tex_name='db_{VLI}', info='Voltage deadband lower limit (PLACEHOLDER)', @@ -225,32 +225,32 @@ def __init__(self): info='Minimum voltage for current limiting (PLACEHOLDER)', ) - self.Pcmd_GFL_max = NumParam(default=1.2, - tex_name='P_{cmd,GFL,max}', - info='Maximum active power command for GFL', - unit='p.u.', - power=True, - ) - - self.Pcmd_GFL_min = NumParam(default=-1.0, - tex_name='P_{cmd,GFL,min}', - info='Minimum active power command for GFL', - unit='p.u.', - power=True, - ) - self.Qcmd_GFL_max = NumParam(default=0.6, - tex_name='Q_{cmd,GFL,max}', - info='Maximum reactive power command for GFL', - unit='p.u.', - power=True, - ) - - self.Qcmd_GFL_min = NumParam(default=-0.6, - tex_name='Q_{cmd,GFL,min}', - info='Minimum reactive power command for GFL', - unit='p.u.', - power=True, - ) + self.Pcmd_GFL_max = NumParam(default=1.2, + tex_name='P_{cmd,GFL,max}', + info='Maximum active power command for GFL', + unit='p.u.', + power=True, + ) + + self.Pcmd_GFL_min = NumParam(default=-1.0, + tex_name='P_{cmd,GFL,min}', + info='Minimum active power command for GFL', + unit='p.u.', + power=True, + ) + self.Qcmd_GFL_max = NumParam(default=0.6, + tex_name='Q_{cmd,GFL,max}', + info='Maximum reactive power command for GFL', + unit='p.u.', + power=True, + ) + + self.Qcmd_GFL_min = NumParam(default=-0.6, + tex_name='Q_{cmd,GFL,min}', + info='Minimum reactive power command for GFL', + unit='p.u.', + power=True, + ) class REGFMC1Model(Model): """ @@ -268,14 +268,14 @@ def __init__(self, system, config): src='a', indexer=self.bus, tex_name=r'\theta', - info='Bus voltage angle', + info='Inverter terminal/interface bus voltage angle', e_str='-u * Pe', ) self.v = ExtAlgeb(model='Bus', src='v', indexer=self.bus, tex_name='V', - info='Bus voltage magnitude', + info='Inverter terminal/interface bus voltage magnitude', e_str='-u * Qe', ) @@ -292,21 +292,21 @@ def __init__(self, system, config): info='Total Q of the static gen', ) - # --- Initialization Services --- - # Power, current, and impedance parameters marked with ``power=True``, - # ``current=True``, and ``z=True`` are converted by ANDES to system base. - # Keep the internal controller equations on system base as well. - self.p0 = ConstService(v_str='gammap * p0s', - tex_name='P_0', - info='Initial P for this device on system base', - ) - self.q0 = ConstService(v_str='gammaq * q0s', - tex_name='Q_0', - info='Initial Q for this device on system base', - ) - - # Initial current calculations (for both branches) - self.Id0_GFL = ConstService(tex_name=r'I_{d0,GFL}', + # --- Initialization Services --- + # Power, current, and impedance parameters marked with ``power=True``, + # ``current=True``, and ``z=True`` are converted by ANDES to system base. + # Keep the internal controller equations on system base as well. + self.p0 = ConstService(v_str='gammap * p0s', + tex_name='P_0', + info='Initial P for this device on system base', + ) + self.q0 = ConstService(v_str='gammaq * q0s', + tex_name='Q_0', + info='Initial Q for this device on system base', + ) + + # Initial current calculations (for both branches) + self.Id0_GFL = ConstService(tex_name=r'I_{d0,GFL}', v_str='u * p0 / v', ) self.Iq0_GFL = ConstService(tex_name=r'I_{q0,GFL}', @@ -326,20 +326,20 @@ def __init__(self, system, config): ) # GFM branch impedance squared (for current calculation) - self.Zs2 = ConstService(v_str='Rs**2 + Xs**2', - tex_name='Z_s^2', - info='GFM series impedance magnitude squared on system base', - ) + self.Zs2 = ConstService(v_str='Rs**2 + Xs**2', + tex_name='Z_s^2', + info='GFM series impedance magnitude squared on system base', + ) # --- External reference variables (to be controlled by plant controller) --- - # GFM voltage reference (external input to voltage control) # mistake + # GFM voltage reference (external input to voltage control) self.Vref_GFM = Algeb(tex_name='V_{ref,GFM}', info='Voltage reference for GFM branch (from plant controller)', v_str='Vref0', # modified e_str='Vref0 - Vref_GFM', # Default: maintain initial bus voltage ) - # GFM frequency reference (external input to VSM control) # mistake + # GFM frequency reference (external input to VSM control) self.fref_GFM = Algeb(tex_name='f_{ref,GFM}', info='Frequency reference for GFM branch (from plant controller)', v_str='1.0', @@ -363,18 +363,6 @@ def __init__(self, system, config): # Reactive power measurement path (per diagram: Iq_GFM filtered through 1/(Tif*s+1)) self.Iq_VSMLag = Lag(u='Iq_VSM', T=self.TIf, K=1, info='Filter for I_q GFM', name='Iq_VSMLag') - # Voltage magnitude error - TESTING POSITIVE SIGN - # Testing: Verr = (Vref - Vinv) * kq/mq + Iq_VSMLag_y (or -Iq_VSMLag_y ??) - - - # PI controller for voltage magnitude - # self.VmagPI = PIController(u=self.Verr, - # kp=self.kpE, - # ki=self.kiE, - # x0='0', - # info='Voltage magnitude PI controller', - # name='VmagPI', - # ) # Proportional part # Voltage magnitude error @@ -424,7 +412,7 @@ def __init__(self, system, config): e_str='VmagP + VmagI_lim_val - VmagPI_raw', ) - # Optional: also limit the final PI output to [dEmin, dEmax] + # Also limit the final PI output to [dEmin, dEmax] self.VmagPI_lim = Limiter( u=self.VmagPI_raw, lower=self.dEmin, @@ -447,7 +435,7 @@ def __init__(self, system, config): e_str='VmagPI_lim_val + VrefLag_y - EVSM_initial', ) - # Output filter for EVSM (TODO: add limiters for dEmax, dEmin) + # Output filter for EVSM self.EVSMLag = Lag(u='EVSM_initial', T=self.Tvsm, K=1, @@ -478,17 +466,8 @@ def __init__(self, system, config): info='Active power reference for GFM', ) - # TODO: USE `fref_GFM` as the input terminal for frequency reference - - # # Plant controller changes omega_ref (PLACEHOLDER - use constant 1 for now) - # self.omega_ref = Algeb(tex_name=r'\omega_{ref}', - # info='Omega reference', - # v_str='1.0', - # e_str='OmegarefLag_y - omega_ref', - # ) - # GFM branch power measurement (for droop feedback) - self.Pinv_GFM = Algeb( # after limit + self.Pinv_GFM = Algeb( tex_name='P_{inv,GFM}', info='GFM inverter active power at limited internal voltage source', v_str='0', @@ -497,16 +476,14 @@ def __init__(self, system, config): # Frequency droop: converts frequency error to power command # dP_GFM_droop = (omegarefLag_y - omegamLag_y) / mp + self.SWFF = Switcher(u=self.FFFlag, options=(0, 1), tex_name='SW_{FF}', cache=True) + self.dP_GFM_droop = Algeb(tex_name=r'\Delta P_{GFM,droop}', info='Power command from frequency droop', v_str='0', - e_str='(OmegarefLag_y - omegamLag_y) / mp - dP_GFM_droop', + e_str='SWFF_s1 * (OmegarefLag_y - omegamLag_y) / mp - dP_GFM_droop', ) - - # TODO: CONSIDER `FFlag` to allow turning off the droop control - # FFlag = 0: `dP_GFM_droop` = 0 -- this is currently missing - # Power command for GFM branch # Pcmd_GFM = Pref_GFM + dP_GFM_droop (reference + droop correction) self.Pcmd_GFM = Algeb(tex_name='P_{cmd,GFM}', @@ -516,8 +493,8 @@ def __init__(self, system, config): ) # Damping filter: sD2/(s+omegaD) using Washout - # Washout implements sK/(1+sT), so we need K=D2, T=1/omegaD - self.domegam = Algeb( # modified-11.6 + # Washout implements sK/(1+sT), so K=D2, T=1/omegaD + self.domegam = Algeb( name='domegam', tex_name=r'\Delta\omega', info='Frequency deviation (pu)', @@ -527,34 +504,20 @@ def __init__(self, system, config): self.DampWash = Washout(u='domegam', T=self.Tdamp, - K=self.D2, # should be self.D2* self.Tdamp??-11.6 + K=self.D2, info='Damping washout filter', name='DampWash', ) - # Inverter active power for GFM - measured at voltage source (for swing equation) - - - + # Inverter active power for GFM - measured at voltage source self.PGFM_pre = Algeb( tex_name='P_{GFM,pre}', info='Pre-limited GFM branch active power at bus', v_str='0', e_str='v * Id_VSM - PGFM_pre', ) - - - - # self.Pinv_GFM = Algeb(tex_name='P_{inv,GFM}', - # info='GFM inverter active power at voltage source', - # v_str='0', - # e_str='(EVSM * cos(dVSM - a) * Id_VSM_lim + EVSM * sin(dVSM - a) * Iq_VSM_lim) - Pinv_GFM', - # ) - - - - - self.PGFM_pre_Lag = Lag(u='PGFM_pre', # modified-11.6 + + self.PGFM_pre_Lag = Lag(u='PGFM_pre', T=self.Tpf, K=1, info='Pre-limited GFM branch active power at bus', @@ -621,7 +584,7 @@ def __init__(self, system, config): e_str='q0 - Qcmd_GFL', # Defaults to q0, can be overridden externally ) - # Current commands (PLACEHOLDER - TODO: add limiters and PQ priority) + # Current commands self.Ipcmd_GFL = Algeb(tex_name='I_{pcmd,GFL}', info='Active current command for GFL', v_str='Id0_GFL', @@ -633,66 +596,38 @@ def __init__(self, system, config): v_str='kqv * Verr_GFL_dbd_y + Iq0_GFL', e_str='kqv * Verr_GFL_dbd_y + Qcmd_GFL / v - Iqcmd_GFL', ) - - # GFL PQ Priority Current Limiting - - # dynamic max,min boundary (PQsel choice) - # a new version of PQ priority current limiting algorithm - # choose priority 1=P ,0=Q - - - # self.PQsel = VarService(v_str='Indicator(PQFlag >= 0.5)', tex_name='PQ_{sel}', - # info='1=P priority, 0=Q priority') - - # get Ipmax_GFL, Ipmin_GFL, Iqmax_GFL, Iqmin_GFL + self.Ipmaxsq0_GFL1 = ConstService( + v_str='Piecewise((0, Le(Imax**2 - (kqv * (Vref0 - v) + Iq0_GFL) **2, 0.0)), (Imax**2 - (kqv * (Vref0 - v) + Iq0_GFL)**2 , True), evaluate=False)' + ) + self.Iqmaxsq0_GFL1 = ConstService( + v_str='Piecewise((0, Le(Imax**2 - Id0_GFL**2, 0.0)), (Imax**2 - Id0_GFL**2, True), evaluate=False)' + ) + + self.Ipmaxsq_GFL1 = VarService( + v_str='Piecewise((0, Le(Imax**2 - Iqcmd_sat_val**2, 0.0)), (Imax**2 - Iqcmd_sat_val**2, True), evaluate=False)', + tex_name='I_{p,max,GFL}^2', + ) - # self.Ipmax_GFL1 = Algeb( - # tex_name='I_{p,max,GFL}', - # info='Ipmax_GFL for Current Limiting Algorithm', - # v_str='PQFlag * Imax + (1 - PQFlag) * sqrt(0.5 * ((Imax**2 - Iqcmd_sat_val**2) + sqrt( Imax**2 - Iqcmd_sat_val**2)**2 ))', - # e_str='PQFlag * Imax + (1 - PQFlag) * sqrt(0.5 * ((Imax**2 - Iqcmd_sat_val**2) + sqrt( Imax**2 - Iqcmd_sat_val**2)**2 )) - Ipmax_GFL1' - # ) + self.Iqmaxsq_GFL1 = VarService( + v_str='Piecewise((0, Le(Imax**2 - Ipcmd_sat_val**2, 0.0)), (Imax**2 - Ipcmd_sat_val**2, True), evaluate=False)', + tex_name='I_{q,max,GFL}^2', + ) - # self.Iqmax_GFL1 = Algeb( - # tex_name='I_{q,max,GFL}', - # info='Iqmax_GFL for Current Limiting Algorithm', - # v_str='PQFlag * Imax + (1 - PQFlag) * sqrt(0.5 * ((Imax**2 - Ipcmd_sat_val**2) + sqrt( Imax**2 - Ipcmd_sat_val**2)**2 ))', - # e_str='PQFlag * Imax + (1 - PQFlag) * sqrt(0.5 * ((Imax**2 - Ipcmd_sat_val**2) + sqrt( Imax**2 - Ipcmd_sat_val**2)**2 )) - Iqmax_GFL1' - # ) - - self.Ipmaxsq0_GFL1 = ConstService( - v_str='Piecewise((0, Le(Imax**2 - (kqv * (Vref0 - v) + Iq0_GFL) **2, 0.0)), (Imax**2 - (kqv * (Vref0 - v) + Iq0_GFL)**2 , True), evaluate=False)' - ) - - self.Iqmaxsq0_GFL1 = ConstService( - v_str='Piecewise((0, Le(Imax**2 - Id0_GFL**2, 0.0)), (Imax**2 - Id0_GFL**2, True), evaluate=False)' - ) - - self.Ipmaxsq_GFL1 = VarService( - v_str='Piecewise((0, Le(Imax**2 - Iqcmd_sat_val**2, 0.0)), (Imax**2 - Iqcmd_sat_val**2, True), evaluate=False)', - tex_name='I_{p,max,GFL}^2', - ) - - self.Iqmaxsq_GFL1 = VarService( - v_str='Piecewise((0, Le(Imax**2 - Ipcmd_sat_val**2, 0.0)), (Imax**2 - Ipcmd_sat_val**2, True), evaluate=False)', - tex_name='I_{q,max,GFL}^2', - ) - - self.Ipmax_GFL1 = Algeb( - tex_name='I_{p,max,GFL}', - info='Ipmax_GFL for Current Limiting Algorithm', - v_str='PQFlag * Imax + (1 - PQFlag) * sqrt(Ipmaxsq0_GFL1)', - e_str='PQFlag * Imax + (1 - PQFlag) * sqrt(Ipmaxsq_GFL1) - Ipmax_GFL1' - ) - - self.Iqmax_GFL1 = Algeb( - tex_name='I_{q,max,GFL}', - info='Iqmax_GFL for Current Limiting Algorithm', - v_str='(1 - PQFlag) * Imax + PQFlag * sqrt(Iqmaxsq0_GFL1)', - e_str='(1 - PQFlag) * Imax + PQFlag * sqrt(Iqmaxsq_GFL1) - Iqmax_GFL1' - ) + self.Ipmax_GFL1 = Algeb( + tex_name='I_{p,max,GFL}', + info='Ipmax_GFL for Current Limiting Algorithm', + v_str='PQFlag * Imax + (1 - PQFlag) * sqrt(Ipmaxsq0_GFL1)', + e_str='PQFlag * Imax + (1 - PQFlag) * sqrt(Ipmaxsq_GFL1) - Ipmax_GFL1' + ) + + self.Iqmax_GFL1 = Algeb( + tex_name='I_{q,max,GFL}', + info='Iqmax_GFL for Current Limiting Algorithm', + v_str='(1 - PQFlag) * Imax + PQFlag * sqrt(Iqmaxsq0_GFL1)', + e_str='(1 - PQFlag) * Imax + PQFlag * sqrt(Iqmaxsq_GFL1) - Iqmax_GFL1' + ) self.Ipmin_GFL1 = Algeb( tex_name='I_{p,min,GFL}', @@ -709,48 +644,6 @@ def __init__(self, system, config): ) - - - - # Ipmax_GFL, Ipmin_GFL, Iqmax_GFL, Iqmin_GFL (Algeb) - - - - # self.Ipmax_GFL1 = Algeb( - # tex_name='I_{p,max,GFL}', - # info='Ipmax_GFL for Current Limiting Algorithm', - # v_str='PQFlag*Imax + (1-PQFlag)*sqrt( Imax**2 - (kqv * (Vref0 - v) + Iq0_GFL)**2)', - # e_str='(PQFlag*Imax + (1-PQFlag)*sqrt(0.5*((Imax**2 - Iqcmd_sat_val**2) + Abs(Imax**2 - Iqcmd_sat_val**2)))) - Ipmax_GFL1', - # ) - - - # self.Ipmin_GFL1 = Algeb( - # tex_name='I_{p,min,GFL}', - # info='Ipmin_GFL for Current Limiting Algorithm ', - # v_str='-PQFlag*Imax - (1-PQFlag)*sqrt( Imax**2 - (kqv * (Vref0 - v) + Iq0_GFL)**2)', - # e_str='(-Ipmax_GFL1) - Ipmin_GFL1', - # ) - - # self.Iqmax_GFL1 = Algeb( - # tex_name='I_{q,max,GFL}', - # info='Iqmax_GFL for Current Limiting Algorithm', - # v_str='(1-PQFlag)*Imax + (PQFlag)*sqrt( Imax**2 - Id0_GFL**2)', - # e_str='((1-PQFlag)*Imax + PQFlag*sqrt(0.5*((Imax**2 - Ipcmd_sat_val**2) + Abs(Imax**2 - Ipcmd_sat_val**2)))) - Iqmax_GFL1', - # ) - - # self.Iqmin_GFL1 = Algeb( - # tex_name='I_{q,min,GFL}', - # info='Iqmin_GFL for Current Limiting Algorithm', - # v_str='-(1-PQFlag)*Imax - (PQFlag)*sqrt( Imax**2 - Id0_GFL**2)', - # e_str='(-Iqmax_GFL1) - Iqmin_GFL1', - # ) - - - - - - - self.Ipcmd_sat = Limiter(u=self.Ipcmd_GFL, lower=self.Ipmin_GFL1, upper=self.Ipmax_GFL1, name='Ipcmd_sat') self.Iqcmd_sat = Limiter(u=self.Iqcmd_GFL, lower=self.Iqmin_GFL1, upper=self.Iqmax_GFL1, name='Iqcmd_sat') @@ -815,11 +708,11 @@ def __init__(self, system, config): e_str='sqrt((Id_VSM + Ipcmd_sat_val)**2 + (Iq_VSM + Iqcmd_sat_val)**2 ) - Itotal', # Simplified, should be vector sum ) - self.k_scale = VarService( - v_str='1.0 + Indicator(Itotal > Imax) * (Itotal / Imax - 1.0)', - tex_name='k', - info='Scaling factor (>=1) for total current limiting' - ) + self.k_scale = VarService( + v_str='1.0 + Indicator(Itotal > Imax) * (Itotal / Imax - 1.0)', + tex_name='k', + info='Scaling factor (>=1) for total current limiting' + ) self.k_scale_out = Algeb(tex_name='k_{scale}', @@ -829,7 +722,6 @@ def __init__(self, system, config): ) # --- Limited branch currents (both branches divided by k_scale) --- - self.Ed_VSM_lim = Algeb( tex_name='E_{d,VSM}^{lim}', info='Limited GFM internal voltage d-axis component', @@ -845,21 +737,21 @@ def __init__(self, system, config): ) - self.Id_VSM_lim = Algeb( - name='Id_VSM_lim', - v_str='0.0', - e_str='((Ed_VSM_lim - v) * Rs + Eq_VSM_lim * Xs) / Zs2 - Id_VSM_lim', - tex_name='I_{d,VSM}^{lim}', - info='Limited GFM d-axis current' - ) + self.Id_VSM_lim = Algeb( + name='Id_VSM_lim', + v_str='0.0', + e_str='((Ed_VSM_lim - v) * Rs + Eq_VSM_lim * Xs) / Zs2 - Id_VSM_lim', + tex_name='I_{d,VSM}^{lim}', + info='Limited GFM d-axis current' + ) - self.Iq_VSM_lim = Algeb( - name='Iq_VSM_lim', - v_str='0.0', - e_str='((Ed_VSM_lim - v) * Xs - Eq_VSM_lim * Rs) / Zs2 - Iq_VSM_lim', - tex_name='I_{q,VSM}^{lim}', - info='Limited GFM q-axis current' - ) + self.Iq_VSM_lim = Algeb( + name='Iq_VSM_lim', + v_str='0.0', + e_str='((Ed_VSM_lim - v) * Xs - Eq_VSM_lim * Rs) / Zs2 - Iq_VSM_lim', + tex_name='I_{q,VSM}^{lim}', + info='Limited GFM q-axis current' + ) self.EVSM_lim = Algeb( tex_name='E_{VSM}^{lim}', @@ -875,19 +767,6 @@ def __init__(self, system, config): e_str='a + atan2(Eq_VSM_lim, Ed_VSM_lim + 1e-8) - dVSM_lim', ) - - - # self.Id_VSM_lim = Algeb( - # name='Id_VSM_lim', v_str='0.0', - # e_str='Id_VSM / (k_scale+1e-8) - Id_VSM_lim', - # tex_name='I_{d,VSM}^{lim}', info='Limited GFM d-axis current' - # ) - # self.Iq_VSM_lim = Algeb( - # name='Iq_VSM_lim', v_str='0.0', - # e_str='Iq_VSM / (k_scale+1e-8) - Iq_VSM_lim', - # tex_name='I_{q,VSM}^{lim}', info='Limited GFM q-axis current' - # ) - self.Ip_GFL_lim = Algeb( name='Ip_GFL_lim', v_str='Id0_GFL', e_str='Ipcmd_sat_val / (k_scale) - Ip_GFL_lim', @@ -906,30 +785,6 @@ def __init__(self, system, config): tex_name='I_{total}^{lim}', info='Total output current after limiting' ) - # Scaling factor for current limiting (PLACEHOLDER) - # self.k_factor = Algeb(tex_name='k_{factor}', - # info='Current scaling factor (PLACEHOLDER)', - # v_str='1.0', - # e_str='1.0 - k_factor', # No limiting initially - # ) - - # self.k_factor = Algeb( - # tex_name='k_{factor}', - # info='Current scaling factor (PLACEHOLDER)', - # v_str='Itotal/Imax', - # e_str='Itotal/Imax - k_factor', - # ) - - - - # --- Power Calculations --- - # GFM branch current in dq-frame - # Voltage source EVSM∠dVSM behind impedance Rs+jXs to bus V∠a - # In dq-frame (d-axis aligned with bus voltage V∠a): whe - # delta = dVSM - a (angle difference) - # Ed_VSM = EVSM * cos(delta), Eq_VSM = EVSM * sin(delta) - # Id_VSM = ((Ed_VSM - v)*Rs + Eq_VSM*Xs) / Zs2 - # Iq_VSM = (Eq_VSM*Rs - (Ed_VSM - v)*Xs) / Zs2 self.Ed_VSM = Algeb(tex_name='E_{d,VSM}', info='GFM d-axis voltage', @@ -943,100 +798,66 @@ def __init__(self, system, config): e_str='EVSM * sin(dVSM - a) - Eq_VSM', ) - self.Id_VSM = Algeb(tex_name='I_{d,VSM}', - info='GFM d-axis current', - v_str='0', - e_str='((EVSM * cos(dVSM - a) - v) * Rs + EVSM * sin(dVSM - a) * Xs) / Zs2 - Id_VSM', - ) + self.Id_VSM = Algeb(tex_name='I_{d,VSM}', + info='GFM d-axis current', + v_str='0', + e_str='((EVSM * cos(dVSM - a) - v) * Rs + EVSM * sin(dVSM - a) * Xs) / Zs2 - Id_VSM', + ) # should be negative? - self.Iq_VSM = Algeb(tex_name='I_{q,VSM}', - info='GFM q-axis current', - v_str='0', - e_str='-(EVSM * sin(dVSM - a) * Rs - (EVSM * cos(dVSM - a) - v) * Xs) / Zs2 - Iq_VSM', - ) - - # self.Id_VSM = Algeb(tex_name='I_{d,VSM}', - # info='GFM d-axis current', - # v_str='0', - # e_str='( (EVSM- v * cos(dVSM - a)) * Rs + v * sin(dVSM - a) * Xs )/ Zs2 - Id_VSM', - # ) - - # self.Iq_VSM = Algeb(tex_name='I_{q,VSM}', - # info='GFM q-axis current', - # v_str='0', - # e_str='( v * sin(dVSM - a) * Rs - (EVSM - v * cos(dVSM - a)) * Xs ) / Zs2 - Iq_VSM', - # ) + self.Iq_VSM = Algeb(tex_name='I_{q,VSM}', + info='GFM q-axis current', + v_str='0', + e_str='-(EVSM * sin(dVSM - a) * Rs - (EVSM * cos(dVSM - a) - v) * Xs) / Zs2 - Iq_VSM', + ) # GFM branch power at bus terminals (for bus injection) # In dq frame aligned with bus: Vd=v, Vq=0 # P = Vd*Id + Vq*Iq = v*Id_VSM - # Q = Vq*Id - Vd*Iq = -v*Iq_VSM (negative sign for generator convention) or postive? + # Q = Vq*Id - Vd*Iq = v*Iq_VSM self.Vd = VarService(v_str='v*cos(a)') self.Vq = VarService(v_str='v*sin(a)') - self.PGFM = Algeb(tex_name='P_{GFM}', - info='GFM branch active power at bus', - v_str='0', - e_str='v * Id_VSM_lim - PGFM', - ) - - self.QGFM = Algeb(tex_name='Q_{GFM}', - info='GFM branch reactive power at bus', - v_str='0', - e_str='v * Iq_VSM_lim - QGFM', - ) - # self.PGFM = Algeb(tex_name='P_{GFM}', - # info='GFM branch active power at bus', - # v_str='0', - # e_str='Ed_VSM * Id_VSM_lim + Eq_VSM * Iq_VSM_lim - PGFM', - # ) - - # self.QGFM = Algeb(tex_name='Q_{GFM}', - # info='GFM branch reactive power at bus', - # v_str='0', - # e_str='-Ed_VSM * Iq_VSM_lim + Eq_VSM * Id_VSM_lim - QGFM', # -Ed_VSM * Iq_VSM+ Eq_VSM*Id_VSM - QGFM - # ) - - # GFL branch power + self.PGFM = Algeb(tex_name='P_{GFM}', + info='GFM branch active power at bus', + v_str='0', + e_str='v * Id_VSM_lim - PGFM', + ) - # self.PGFL = Algeb(tex_name='P_{GFL}', - # info='GFL branch active power', - # v_str='v*cos(a)*(Id0_GFL) + v*sin(a)*(kqv * (Vref0 - v) + Iq0_GFL)', - # e_str='Vd * Ip_GFL_lim + Vq *Iq_GFL_lim - PGFL', - # ) - - # self.QGFL = Algeb(tex_name='Q_{GFL}', - # info='GFL branch reactive power', - # v_str='v*sin(a)*(Id0_GFL)- v*cos(a)*(kqv * (Vref0 - v) + Iq0_GFL)', - # e_str='Vq * Ip_GFL_lim - Vd *Iq_GFL_lim - QGFL', - # ) - self.PGFL = Algeb(tex_name='P_{GFL}', - info='GFL branch active power', - v_str='v * Id0_GFL', - e_str='v * Ip_GFL_lim - PGFL', - ) - - self.QGFL = Algeb(tex_name='Q_{GFL}', - info='GFL branch reactive power', - v_str='v * (kqv * (Vref0 - v) + Iq0_GFL)', - e_str='v * Iq_GFL_lim - QGFL', - ) - - # Total power injection - self.Pe = Algeb(tex_name='P_e', - info='Total active power injection', - v_str='p0', - e_str='PGFM + PGFL - Pe', - ) - - self.Qe = Algeb(tex_name='Q_e', - info='Total reactive power injection', - v_str='q0', - e_str='QGFM + QGFL - Qe', - ) + self.QGFM = Algeb(tex_name='Q_{GFM}', + info='GFM branch reactive power at bus', + v_str='0', + e_str='v * Iq_VSM_lim - QGFM', + ) + + # GFL branch power + + self.PGFL = Algeb(tex_name='P_{GFL}', + info='GFL branch active power', + v_str='v * Id0_GFL', + e_str='v * Ip_GFL_lim - PGFL', + ) + + self.QGFL = Algeb(tex_name='Q_{GFL}', + info='GFL branch reactive power', + v_str='v * (kqv * (Vref0 - v) + Iq0_GFL)', + e_str='v * Iq_GFL_lim - QGFL', + ) + + # Total power injection + self.Pe = Algeb(tex_name='P_e', + info='Total active power injection', + v_str='p0', + e_str='PGFM + PGFL - Pe', + ) + + self.Qe = Algeb(tex_name='Q_e', + info='Total reactive power injection', + v_str='q0', + e_str='QGFM + QGFL - Qe', + ) def v_numeric(self, **kwargs): """ diff --git a/andes/models/renewable/repcgfmc1.py b/andes/models/renewable/repcgfmc1.py index 341e1d42a..8acf8a087 100644 --- a/andes/models/renewable/repcgfmc1.py +++ b/andes/models/renewable/repcgfmc1.py @@ -14,7 +14,8 @@ from andes.core.block import DeadBand1, GainLimiter, IntegratorAntiWindup, PIController, Washout -from andes.core.service import NumSelect, VarService, EventFlag, ExtendedEvent, VarHold +from andes.core.service import (CurrentSign, NumSelect, VarService, + EventFlag, ExtendedEvent, VarHold) class REPCGFMC1Data(ModelData): """ @@ -30,8 +31,13 @@ def __init__(self): ) self.busr = IdxParam(model='Bus', - info='Optional remote bus for measurements', - default=None, + info='Plant/PCC measurement bus', + mandatory=True, + ) + + self.line = IdxParam(info='Monitored branch between inverter terminal bus and plant/PCC measurement bus', + model='ACLine', + mandatory=True, ) self.Sn = NumParam(default=100.0, tex_name='S_n', info='Model MVA base', @@ -99,14 +105,14 @@ def __init__(self): power=True, ) - self.Rloss = NumParam(default=0.0, # modified 0-->0.01 assumption + self.Rloss = NumParam(default=0.0, tex_name='R_{loss}', info='Loss compensation resistance', unit='p.u.', z=True, ) - self.Xloss = NumParam(default=0.0, # modified 0-->0.05assumption + self.Xloss = NumParam(default=0.0, tex_name='X_{loss}', info='Loss compensation reactance', unit='p.u.', @@ -267,7 +273,7 @@ def __init__(self): ) # --- GFL Reactive Power Path Parameters --- - self.Qref_max = NumParam(default=0.6, # sometimes need to adjust + self.Qref_max = NumParam(default=0.6, tex_name='Q_{ref,max}', info='Maximum reactive power reference', unit='p.u.', @@ -335,7 +341,7 @@ def __init__(self): unit='s', ) - self.Qvc_max = NumParam(default=0.6, # sometimes need to adjust(same with Qref_max) + self.Qvc_max = NumParam(default=0.6, tex_name='Q_{vc,max}', info='Maximum voltage control output', unit='p.u.', @@ -374,13 +380,13 @@ def __init__(self): info='Cross-coupling gain', ) - self.Qerr_max = NumParam(default=0.1, # modified + self.Qerr_max = NumParam(default=0.1, tex_name='Q_{err,max}', info='Maximum reactive power error limit', unit='p.u.', power=True, ) - self.Qerr_min = NumParam(default=-0.1, # modified + self.Qerr_min = NumParam(default=-0.1, tex_name='Q_{err,min}', info='Minimum reactive power error limit', unit='p.u.', @@ -451,41 +457,167 @@ def __init__(self, system, config): self.bus = ExtParam(model='RenGen', src='bus', indexer=self.reg, export=False, info='Retrieved bus idx', vtype=str, default=None, ) - # self.busf = IdxParam(model='BusFreq', info='Bus for frequency measurement') - - # Select bus for measurements (remote bus if provided, otherwise converter bus) - from andes.core.service import DataSelect,DeviceFinder - self.buss = DataSelect(self.busr, self.bus, info='Selected bus for measurements') + # Plant/PCC measurement bus. Frequency is measured at this same bus. + from andes.core.service import DeviceFinder - # self.busfreq = DeviceFinder(self.busr, link=self.buss, idx_name='bus') - self.busfreq = DeviceFinder(self.busf, link=self.buss, idx_name='bus', default_model='BusFreq')# modified - self.busRocof= DeviceFinder(self.busrocof, link=self.buss, idx_name='bus', default_model='BusROCOF') # modified + self.busfreq = DeviceFinder(self.busf, link=self.busr, idx_name='bus', default_model='BusFreq') + self.busRocof = DeviceFinder(self.busrocof, link=self.busr, idx_name='bus', default_model='BusROCOF') # --- External Variables from Bus --- - self.v = ExtAlgeb(model='Bus', src='v', indexer=self.buss, tex_name='V', - info='Bus (or busr, if given) terminal voltage', + self.v = ExtAlgeb(model='Bus', src='v', indexer=self.busr, tex_name='V', + info='Plant/PCC measurement bus voltage', e_str='0', ) - self.a = ExtAlgeb(model='Bus', src='a', indexer=self.buss, tex_name=r'\theta', - info='Bus (or busr, if given) phase angle', + self.vsite = ExtAlgeb(model='Bus', src='v', indexer=self.busr, tex_name='V_{site}', + info='Plant/PCC measurement bus voltage', + e_str='0', + ) + + self.vinv = ExtAlgeb(model='Bus', src='v', indexer=self.bus, tex_name='V_{inv}', + info='Inverter terminal voltage', + e_str='0', + ) + + self.a = ExtAlgeb(model='Bus', src='a', indexer=self.busr, tex_name=r'\theta', + info='Plant/PCC measurement bus phase angle', e_str='0', ) - self.v0 = ExtService(model='Bus', src='v', indexer=self.buss, tex_name="V_0", - info='Initial bus voltage', + self.v0 = ExtService(model='Bus', src='v', indexer=self.busr, tex_name="V_0", + info='Initial plant/PCC measurement bus voltage', ) self.f = ExtAlgeb(model='BusFreq', src='f', indexer=self.busfreq, - tex_name="f", info='bus frequency (p.u.)') # f - dynamic + tex_name="f", info='Plant/PCC measurement bus frequency (p.u.)') self.rocof = ExtService(model='BusROCOF', src='Wf_y', indexer=self.busRocof, tex_name="ROCOF", info='bus ROCOF', ) - # self.f = ExtAlgeb(model='BusFreq', src='f', indexer=self.buss, tex_name="F", - # info='bus frequency (Hz)', - # ) # modified + # --- Monitored branch for plant/PCC power measurement --- + self.bus1 = ExtParam(model='ACLine', src='bus1', indexer=self.line, export=False, + info='Retrieved monitored branch Line.bus1 idx', vtype=str, + ) + + self.bus2 = ExtParam(model='ACLine', src='bus2', indexer=self.line, export=False, + info='Retrieved monitored branch Line.bus2 idx', vtype=str, + ) + + self.line_phi = ExtParam(model='ACLine', src='phi', indexer=self.line, export=False, + info='Retrieved monitored branch phase shift', vtype=float, + ) + + self.v1 = ExtAlgeb(model='ACLine', src='v1', indexer=self.line, tex_name='V_1', + info='Voltage at monitored branch Line.bus1', + ) + + self.v2 = ExtAlgeb(model='ACLine', src='v2', indexer=self.line, tex_name='V_2', + info='Voltage at monitored branch Line.bus2', + ) + + self.a1 = ExtAlgeb(model='ACLine', src='a1', indexer=self.line, tex_name=r'\theta_1', + info='Angle at monitored branch Line.bus1', + ) + + self.a2 = ExtAlgeb(model='ACLine', src='a2', indexer=self.line, tex_name=r'\theta_2', + info='Angle at monitored branch Line.bus2', + ) + + self.gh = ExtService(model='ACLine', src='gh', indexer=self.line, + info='Retrieved monitored branch Line.gh', + ) + + self.bh = ExtService(model='ACLine', src='bh', indexer=self.line, + info='Retrieved monitored branch Line.bh', + ) + + self.gk = ExtService(model='ACLine', src='gk', indexer=self.line, + info='Retrieved monitored branch Line.gk', + ) + + self.bk = ExtService(model='ACLine', src='bk', indexer=self.line, + info='Retrieved monitored branch Line.bk', + ) + + self.ghk = ExtService(model='ACLine', src='ghk', indexer=self.line, + info='Retrieved monitored branch Line.ghk', + ) + + self.bhk = ExtService(model='ACLine', src='bhk', indexer=self.line, + info='Retrieved monitored branch Line.bhk', + ) + + self.itap = ExtService(model='ACLine', src='itap', indexer=self.line, + info='Retrieved monitored branch Line.itap', + ) + + self.itap2 = ExtService(model='ACLine', src='itap2', indexer=self.line, + info='Retrieved monitored branch Line.itap2', + ) + + self.MeasBusSign = CurrentSign(self.busr, self.bus1, self.bus2, + tex_name='I_{site,sign}', + info='Sign of monitored branch current outflow at the plant/PCC measurement bus', + ) + + Pij = ('v1 ** 2 * (gh + ghk) * itap2 - ' + 'v1 * v2 * (ghk * cos(a1 - a2 - line_phi) + ' + 'bhk * sin(a1 - a2 - line_phi)) * itap') + Qij = ('-v1 ** 2 * (bh + bhk) * itap2 - ' + 'v1 * v2 * (ghk * sin(a1 - a2 - line_phi) - ' + 'bhk * cos(a1 - a2 - line_phi)) * itap') + Pji = ('v2 ** 2 * (gk + ghk) - ' + 'v1 * v2 * (ghk * cos(a1 - a2 - line_phi) - ' + 'bhk * sin(a1 - a2 - line_phi)) * itap') + Qji = ('-v2 ** 2 * (bk + bhk) + ' + 'v1 * v2 * (ghk * sin(a1 - a2 - line_phi) + ' + 'bhk * cos(a1 - a2 - line_phi)) * itap') + + self.Pline_bus1 = Algeb(tex_name='P_{ij}', + info='Monitored branch active power out of Line.bus1', + v_str=Pij, + e_str=f'{Pij} - Pline_bus1', + ) + + self.Qline_bus1 = Algeb(tex_name='Q_{ij}', + info='Monitored branch reactive power out of Line.bus1', + v_str=Qij, + e_str=f'{Qij} - Qline_bus1', + ) + + self.Pline_bus2 = Algeb(tex_name='P_{ji}', + info='Monitored branch active power out of Line.bus2', + v_str=Pji, + e_str=f'{Pji} - Pline_bus2', + ) + + self.Qline_bus2 = Algeb(tex_name='Q_{ji}', + info='Monitored branch reactive power out of Line.bus2', + v_str=Qji, + e_str=f'{Qji} - Qline_bus2', + ) + + # Line-side Pij/Pji and Qij/Qji are positive out of the bus into the + # branch. Site injection is power entering the PCC from the plant, so + # the selected PCC-side branch outflow is negated. + Psite_raw = ('-(0.5 * (1 + MeasBusSign) * Pline_bus1 + ' + '0.5 * (1 - MeasBusSign) * Pline_bus2)') + Qsite_raw = ('-(0.5 * (1 + MeasBusSign) * Qline_bus1 + ' + '0.5 * (1 - MeasBusSign) * Qline_bus2)') + + self.Psite_raw = Algeb(tex_name='P_{site,raw}', + info='Plant/PCC active power injection from monitored branch; positive means plant output', + v_str=Psite_raw, + e_str=f'{Psite_raw} - Psite_raw', + ) + + self.Qsite_raw = Algeb(tex_name='Q_{site,raw}', + info='Plant/PCC reactive power injection from monitored branch; positive means plant output', + v_str=Qsite_raw, + e_str=f'{Qsite_raw} - Qsite_raw', + ) + # --- External Variables from REGFMC1 --- self.Vref_GFM = ExtAlgeb(model='RenGen', src='Vref_GFM', indexer=self.reg, @@ -516,7 +648,7 @@ def __init__(self, system, config): self.Qe = ExtAlgeb(model='RenGen', src='Qe', indexer=self.reg, export=False, info='Reactive power output of REGFMC1', ) - self.Vref0= ExtService(model='RenGen', src='Vref0', indexer=self.reg, tex_name='V_{ref0}', # MODIFIED + self.Vref0= ExtService(model='RenGen', src='Vref0', indexer=self.reg, tex_name='V_{ref0}', info='Vref0 of REGFMC1', ) self.p0 = ExtService(model='RenGen', src='p0', indexer=self.reg, tex_name='P_0', @@ -527,13 +659,23 @@ def __init__(self, system, config): info='Initial reactive power of REGFMC1', ) - # Internal reference values from power flow - self.Pref_site_0 = ConstService(v_str='p0', + self.Ploss_0 = ConstService(v_str='(p0**2 + q0**2) * Rloss', + tex_name='P_{loss,0}', + info='Initial active power loss compensation', + ) + + self.Qloss_0 = ConstService(v_str='(p0**2 + q0**2) * Xloss', + tex_name='Q_{loss,0}', + info='Initial reactive power loss compensation', + ) + + # Internal reference values from power flow, expressed at the PCC. + self.Pref_site_0 = ConstService(v_str='p0 - Ploss_0', tex_name='P_{ref,site,0}', info='Initial site active power reference from power flow', ) - self.Qref_site_0 = ConstService(v_str='q0', + self.Qref_site_0 = ConstService(v_str='q0 - Qloss_0', tex_name='Q_{ref,site,0}', info='Initial site reactive power reference from power flow', ) @@ -543,11 +685,40 @@ def __init__(self, system, config): info='Initial site frequency reference (nominal)', ) - self.Vref_site_0 = ConstService(v_str='v', + self.Ki_vc_nonzero = ConstService(v_str='Indicator(Ki_vc > 0) + Indicator(Ki_vc < 0)', + tex_name='z_{Ki,vc}', + info='Voltage-control integral gain is nonzero', + ) + + self.Qref_site_pos = ConstService(v_str='Indicator(Qref_site_0 > 0)', + tex_name='z_{Qref>0}', + info='Initial site reactive power is positive', + ) + + self.Qref_site_neg = ConstService(v_str='Indicator(Qref_site_0 < 0)', + tex_name='z_{Qref<0}', + info='Initial site reactive power is negative', + ) + + self.Vref_site_0 = ConstService(v_str='Ki_vc_nonzero * v + (1 - Ki_vc_nonzero) * ' + '((1 - Qref_site_pos - Qref_site_neg) * v + ' + 'Qref_site_pos * (v + dbVHI + Qref_site_0 / Kp_vc) + ' + 'Qref_site_neg * (v + dbVLI + Qref_site_0 / Kp_vc))', tex_name='V_{ref,site,0}', info='Initial site voltage reference from power flow', ) + self.Vest_0 = ConstService(v_str='sqrt((v0 + Rloss * Pref_site_0 + Xloss * Qref_site_0)**2 + ' + '(Pref_site_0 * Xloss - Qref_site_0 * Rloss)**2)', + tex_name='V_{est,0}', + info='Initial estimated inverter terminal voltage from site quantities', + ) + + self.dVerr = ConstService(v_str='Vref0 - Vest_0', + tex_name=r'\Delta V_{err}', + info='Initialization offset for GFM voltage reference', + ) + self.Ptarget_0 = NumSelect(self.Ptarget, self.p0, tex_name='P_{target,0}', info='Actual Ptarget (defaults to p0 if Ptarget=0)', @@ -590,18 +761,15 @@ def __init__(self, system, config): tex_name='V_{site}', ) - # Site frequency measurement (PLACEHOLDER - simplified to 1.0) + # Site frequency measurement self.fsite = Lag(u='f', T=self.Tfrq, K=1, info='Site frequency measurement', - tex_name='f_{site}', # mistake: u shouldnt be 1.0 ,should be the measured freq + tex_name='f_{site}', ) - # --- GFM Frequency Reference Generator (Image 3) --- + # --- GFM Frequency Reference Generator --- # Frequency reference filter (using internal fref_site Algeb) - - - - # modified: add logic + self.Vsite_gate = Limiter( self.Vsite_y, lower=self.Vfth, @@ -624,39 +792,39 @@ def __init__(self, system, config): fref_out = 'frefLag_y' self.fref_GFM.e_str = f'{fref_out} -1' - # --- GFM Voltage Reference Generator (Image 3) --- + # --- GFM Voltage Reference Generator --- # Site voltage measurement (already defined above as Vsite) # Loss compensation calculation # Vdrop = (Rloss + jXloss) * (Ptarget - jQtarget) / Vsite_meas # Note: This is a simplified version; full implementation needs complex calculations self.Vsite_meas2 = Lag(u='v', T=self.TVmeas, K=1, - info='Voltage measurement for loss compensation', + info='Site voltage measurement for loss compensation', tex_name='V_{site,meas}', ) - # Loss compensation (simplified - real part only for now) - # ΔV_loss ≈ (Rloss * Ptarget + Xloss * Qtarget) / Vsite_meas - # self.dVloss = VarService(v_str='(Rloss * Ptarget_1 + Xloss * Qtarget_0) / (Vsite_meas2_y + 1e-8)', - # tex_name=r'\Delta V_{loss}', - # info='Voltage drop due to losses', - # ) - - self.dVloss = VarService(v_str='( (Vsite_meas2_y+Rloss * Ptarget_1 + Xloss * Qtarget_1)**2 + (Ptarget_1*Xloss-Qtarget_1*Rloss)**2 )**0.5', - tex_name=r'\Delta V_{loss}', - info='Voltage drop due to losses', - ) # modified + self.Vest = Algeb(tex_name='V_{est}', + info='Estimated inverter terminal voltage from site voltage and loss compensation', + v_str='Vest_0', + e_str='sqrt((Vsite_meas2_y + Rloss * Ptarget_1 + Xloss * Qtarget_1)**2 + ' + '(Ptarget_1 * Xloss - Qtarget_1 * Rloss)**2) - Vest', + ) # Voltage calculation with loss compensation self.Vcalc = Algeb(tex_name='V_{calc}', info='Calculated voltage with loss compensation', - v_str='( (v0+Rloss * Pref_site_0 + Xloss * Qref_site_0)**2 + (Pref_site_0*Xloss-Qref_site_0*Rloss)**2 )**0.5', - e_str='dVloss - Vcalc', + v_str='Vref0', + e_str='Vest + dVerr - Vcalc', ) # Inverter voltage measurement (another filter) - self.Vinv_meas = Lag(u='v', T=self.TVlag, K=1, - info='Inverter voltage measurement', + self.Vinv_meas0 = Lag(u='vinv', T=self.TVmeas, K=1, + info='Inverter terminal voltage measurement', + tex_name='V_{inv,meas0}', + ) + + self.Vinv_meas = Lag(u='Vinv_meas0_y', T=self.TVlag, K=1, + info='Delayed inverter terminal voltage measurement', tex_name='V_{inv,meas}', ) @@ -666,8 +834,8 @@ def __init__(self, system, config): # When VFlag=1, use V_GFM_ref (complex calculation); when 0, use initial voltage self.VGFM_ref = Algeb(tex_name='V_{GFM,ref}', info='GFM voltage reference before filter', - v_str='( (v0+Rloss * Pref_site_0 + Xloss * Qref_site_0)**2 + (Pref_site_0*Xloss-Qref_site_0*Rloss)**2 )**0.5', - e_str='VrefSW_s1 * Vcalc + VrefSW_s0 * Vinv_meas_y - VGFM_ref', # modified + v_str='Vref0', + e_str='VrefSW_s1 * Vcalc + VrefSW_s0 * Vinv_meas_y - VGFM_ref', ) # Apply limits @@ -677,7 +845,7 @@ def __init__(self, system, config): self.VGFM_ref_lim = Algeb(tex_name='V_{GFM,ref,lim}', info='Limited GFM voltage reference', - v_str='( (v0+Rloss * Pref_site_0 + Xloss * Qref_site_0)**2 + (Pref_site_0*Xloss-Qref_site_0*Rloss)**2 )**0.5', + v_str='Vref0', e_str='VGFM_ref * VrefLim_zi + Vrefmax * VrefLim_zu + Vrefmin * VrefLim_zl - VGFM_ref_lim', ) @@ -689,14 +857,10 @@ def __init__(self, system, config): # Output to REGFMC1 voltage reference Vref_out = 'VrefGFMLag_y' - # self.Vref_GFM.e_str = f'{Vref_out} - Vref_site_0' self.Vref_GFM.e_str = f'{Vref_out} - (Vref0)' - # self.Vref_GFM.e_str = f'{Vref_out} -Vref_GFM' - # --- GFL Active Power Path (Image 5) --- + # --- GFL Active Power Path --- # Frequency deadband - - self.fsite_err = Algeb(tex_name='f_{site,err}', info='Site frequency error', v_str='0', @@ -732,8 +896,8 @@ def __init__(self, system, config): v_str='0', e_str='Pfreq_droop * Pfreq_lim_zi + Pfreq_max * Pfreq_lim_zu + Pfreq_min * Pfreq_lim_zl - Pfreq_droop_lim', ) - # FFR + # FFR module self.f_rocof = Algeb(v_str='rocof', e_str='rocof - f_rocof', tex_name='f_{rocof,freq}', @@ -744,46 +908,6 @@ def __init__(self, system, config): self.FFRCSW = Switcher(u=self.FFRFlag, options=(0, 1), tex_name='FFR_{SW}') - # self.P_FFR = Algeb(tex_name='P_{ffr}', - # info='Limited frequency droop output', - # v_str='0', - # e_str='(Indicator(rocof > 0.002) + Indicator(rocof < -0.002))' - # '*(fsite_err * k_FFR) - P_FFR', - # ) - - - # FFR output is NOT a simple algebraic function of ROCOF or frequency error. - # It is a latched hybrid logic: - # trigger -> fixed output -> hold TFFR -> ramp back with DFFR -> re-arm - - - # self.z_ffr_low = VarService(v_str='Indicator(fsite_meas < fFFR_low)') - # self.z_ffr_high = VarService(v_str='Indicator(fsite_meas > fFFR_high)') - # self.z_ffr_thr = VarService(v_str='Indicator(z_ffr_low + z_ffr_high > 0)') - - # # busy / armed - # self.z_ffr_busy = VarService(v_str='Indicator(Abs(P_FFR) > 1e-6)') - # self.z_ffr_armed = VarService(v_str='FFRCSW_s1 * (1 - z_ffr_busy)') - - # # only armed condition can fire 0 or 1 - # self.z_ffr_pick = VarService(v_str='z_ffr_armed * z_ffr_thr') - - # # convert conditionz_ffr_armed * z_ffr_thr') - - # # convert condition into event 0 or 1 - # self.ffr_evt = EventFlag(self.z_ffr_pick) - - # # hold window for TFFR seconds only trig from 0-->1 - # self.ffr_hold = ExtendedEvent(self.ffr_evt, t_ext=self.TFFR, trig="rise") - - # # choose fixed step value determine the P_FFR - # self.Pffr_pick = VarService( - # v_str='z_ffr_low * PFFR_low + z_ffr_high * PFFR_high' - # ) - - # # hold the chosen value - # self.Pffr_hold = VarHold(self.Pffr_pick, hold=self.ffr_hold) - # algebraic output placeholder, final value still updated in g_numeric() self.P_FFR = Algeb( tex_name='P_{FFR}', @@ -792,57 +916,44 @@ def __init__(self, system, config): e_str='0.0 - P_FFR', diag_eps=True ) - - - - - # p target + self.Paux = Algeb(tex_name='P_{aux}', + info='Auxiliary active power reference', + v_str='0.0', + e_str='0.0 - Paux', + diag_eps=True, + ) + + # P target self.Ptarget_1_initial = Algeb(tex_name='P_{target1_initial}', # modified info='active power P target_initial', - v_str='Pe', - e_str='Pfreq_droop_lim+ Pref_site+ FFRCSW_s1*P_FFR - Ptarget_1_initial', + v_str='Pref_site_0', + e_str='Pfreq_droop_lim + Pref_site + FFRCSW_s1 * P_FFR + Paux - Ptarget_1_initial', ) self.Ptarget_1_lim = Limiter(self.Ptarget_1_initial, lower=self.Pref_min, upper=self.Pref_max, # modified tex_name='P_{target1_limit}', ) self.Ptarget_1 = Algeb(tex_name='P_{target1}', # modified info='active power P target', - v_str='Pe', + v_str='Pref_site_0', e_str='Ptarget_1_initial * Ptarget_1_lim_zi + Pref_max * Ptarget_1_lim_zu + Pref_min * Ptarget_1_lim_zl - Ptarget_1', ) - # Site power measurement - self.Psite = Lag(u='Pe', T=self.Tfrq, K=1, - info='Site power measurement', + # Site power measurement from the monitored branch at the PCC side. + self.Psite = Lag(u='Psite_raw', T=self.Tfrq, K=1, + info='Site active power measurement from monitored branch', tex_name='P_{site}', ) - - # Site power reference limits - # self.Pref_site_lim = Limiter(u=self.Pref_site, lower=self.Pref_min, upper=self.Pref_max, - # tex_name='P_{ref,site,lim}', - # ) - # - # self.Pref_site_lim_val = Algeb(tex_name='P_{ref,site,lim}', - # info='Limited site power reference', - # v_str='Pref_site_0', - # e_str='Pref_site * Pref_site_lim_zi + Pref_max * Pref_site_lim_zu + Pref_min * Pref_site_lim_zl - Pref_site_lim_val', - # ) - + # Active power reference with frequency droop # Ptarget is a parameter, so Pref = Ptarget - - # self.Ptarget_val = ConstService(v_str='Ptarget', - # tex_name='P_{target,val}', - # ) - # Power error calculation self.Perr = Algeb(tex_name='P_{err}', info='Site power error', - v_str='0', + v_str='Ptarget_1 - Psite_y', e_str='Ptarget_1 - Psite_y - Perr', # mistake ) @@ -853,31 +964,31 @@ def __init__(self, system, config): self.Perr_lim_val = Algeb(tex_name='P_{err,lim}', info='Limited power error', - v_str='0', + v_str='Perr * Perr_lim_zi + Perr_max * Perr_lim_zu + Perr_min * Perr_lim_zl', e_str='Perr * Perr_lim_zi + Perr_max * Perr_lim_zu + Perr_min * Perr_lim_zl - Perr_lim_val', ) - # Integrator state for PI controller [mistake]:theres no pi actually + # Integrator state for PI controller self.xpwr = State(tex_name='x_{pwr}', info='Integrator state for active power PI', - v_str='0', + v_str='p0 - (Pref_site_0**2 + Qref_site_0**2) * Rloss - Pref_site_0', e_str='Kip_Perr * Perr_lim_val', ) - # Ploss modified - self.Ploss = Algeb(tex_name='P_{loss,GFL}', # modified + # Ploss + self.Ploss = Algeb(tex_name='P_{loss,GFL}', info='active power loss value', - v_str='(Pref_site_0**2+Qref_site_0**2)*Rloss', + v_str='(Pref_site_0**2 + Qref_site_0**2) * Rloss', e_str='(Ptarget_1**2+ Qtarget_1**2)*Rloss - Ploss', ) # Active power command with lag filter - self.Pcmd_sum = Algeb(tex_name='P_{cmd,sum}', # modified + self.Pcmd_sum = Algeb(tex_name='P_{cmd,sum}', info='Sum for active power command before lag', - v_str='(Pref_site_0**2+Qref_site_0**2)*Rloss + Pref_site_0 ', + v_str='p0', e_str='Ploss + xpwr + Ptarget_1 - Pcmd_sum') self.Pcmd_GFL_lag = Lag(u='Pcmd_sum', @@ -885,12 +996,6 @@ def __init__(self, system, config): info='Active power command lag filter', tex_name='P_{cmd,GFL,lag}') - # self.Pcmd_GFL_lag = Lag(u=' Ploss + xpwr + Ptarget_1', # modified - # T=self.Tplag, K=1, - # info='Active power command lag filter', - # tex_name='P_{cmd,GFL,lag}', - # ) - # Apply Pcmd limits self.Pcmd_lim = Limiter(self.Pcmd_GFL_lag_y, lower=self.Pcmd_GFL_min, upper=self.Pcmd_GFL_max, tex_name='P_{cmd,lim}', @@ -899,13 +1004,12 @@ def __init__(self, system, config): # Output to REGFMC1 active power command Pcmd_out = 'Pcmd_GFL_lag_y * Pcmd_lim_zi +Pcmd_GFL_max * Pcmd_lim_zu + Pcmd_GFL_min * Pcmd_lim_zl' self.Pcmd_GFL.e_str = f'{Pcmd_out} - (p0)' - # self.Pcmd_GFL.e_str = f'{Pcmd_out} - Pcmd_GFL' # --- GFL Reactive Power Path --- # Voltage control path (using internal Vref_site Algeb) self.Verr_site = Algeb(tex_name='V_{err,site}', info='Site voltage error', - v_str='0', + v_str='Vref_site_0 - v', e_str='Vref_site - Vsite_y - Verr_site', ) @@ -923,7 +1027,7 @@ def __init__(self, system, config): self.Verr_lim_val = Algeb(tex_name='V_{err,lim}', info='Limited voltage error', - v_str='0', + v_str='Vdbd_y * Verr_lim_zi + Verr_max * Verr_lim_zu + Verr_min * Verr_lim_zl', e_str='Vdbd_y * Verr_lim_zi + Verr_max * Verr_lim_zu + Verr_min * Verr_lim_zl - Verr_lim_val', ) @@ -931,7 +1035,7 @@ def __init__(self, system, config): self.Qvc_int = IntegratorAntiWindup(u=self.Verr_lim_val, T=1.0, K=self.Ki_vc, - y0=0, + y0='Ki_vc_nonzero * Qref_site_0', lower=self.Qvc_min, upper=self.Qvc_max, name='Qvc_int', @@ -942,7 +1046,7 @@ def __init__(self, system, config): # Voltage control PI output (proportional + integral) self.Qvc = Algeb(tex_name='Q_{vc}', info='Voltage control PI output', - v_str='0', + v_str='Qref_site_0', e_str='Kp_vc * Verr_lim_val + Qvc_int_y - Qvc', ) @@ -953,7 +1057,7 @@ def __init__(self, system, config): self.Qvc_lim_val = Algeb(tex_name='Q_{vc,lim}', info='Limited voltage control output', - v_str='0', + v_str='Qvc * Qvc_lim_zi + Qvc_max * Qvc_lim_zu + Qvc_min * Qvc_lim_zl', e_str='Qvc * Qvc_lim_zi + Qvc_max * Qvc_lim_zu + Qvc_min * Qvc_lim_zl - Qvc_lim_val', ) @@ -963,62 +1067,47 @@ def __init__(self, system, config): tex_name='Q_{vc,lag}', ) - # Reactive power reference path (simplified) - # self.Qref_site_lim = Limiter(self.Qref_site, lower=self.Qref_min, upper=self.Qref_max, - # tex_name='Q_{ref,site,lim}', - # ) - - # self.Qref_site_lim_val = Algeb(tex_name='Q_{ref,site,lim}', - # info='Limited reactive power reference', - # v_str='Qref_site_0', - # e_str='Qref_site * Qref_site_lim_zi + Qref_max * Qref_site_lim_zu + Qref_min * Qref_site_lim_zl - Qref_site_lim_val', - # ) - # Reactive power control with lag - self.Qsite_filt = Lag(u='Qe', T=self.Tqlag, K=1, - info='Site reactive power measurement', - tex_name='Q_{site,filt}', - ) + self.Qsite = Lag(u='Qsite_raw', T=self.Tqlag, K=1, + info='Site reactive power measurement from monitored branch', + tex_name='Q_{site}', + ) # VFlag selector self.VFlagSW = Switcher(u=self.VFlag, options=(0, 1), tex_name='V_{FlagSW}') - # When VFlag=1, use Qvc_lag (voltage control); when 0, use Q reference - # self.Qerr = Algeb(tex_name='Q_{err}', - # info='Reactive power error', - # v_str='0', - # e_str='Qref_site_lim_val - Qsite_filt_y + VFlagSW_s1 * Kiq * Qvc_lag_y - Qerr', - # ) + self.Qaux = Algeb(tex_name='Q_{aux}', + info='Auxiliary reactive power reference', + v_str='0.0', + e_str='0.0 - Qaux', + diag_eps=True, + ) + - self.Qtarget_1_initial = Algeb(tex_name='Q_{target1_initial}', # modified + self.Qtarget_1_initial = Algeb(tex_name='Q_{target1_initial}', info='Reactive power Q target_initial', v_str='Qref_site_0 ', - e_str='VFlagSW_s1*(Qref_site+Qvc_lag_y) +VFlagSW_s0*(Qref_site)-Qtarget_1_initial ', + e_str='VFlagSW_s1 * Qvc_lag_y + VFlagSW_s0 * Qref_site + Qaux - Qtarget_1_initial ', ) - self.Qtarget_1_lim = Limiter(self.Qtarget_1_initial, lower=self.Qref_min, upper=self.Qref_max, # modified + self.Qtarget_1_lim = Limiter(self.Qtarget_1_initial, lower=self.Qref_min, upper=self.Qref_max, tex_name='Q_{target1_limit}', ) - self.Qtarget_1= Algeb(tex_name='Q_{target1}', # modified + self.Qtarget_1= Algeb(tex_name='Q_{target1}', info='Reactive power Q target', v_str='Qref_site_0', e_str='Qtarget_1_lim_zi * Qtarget_1_initial + Qref_max * Qtarget_1_lim_zu + Qref_min * Qtarget_1_lim_zl - Qtarget_1', ) - self.Qerr0 = Algeb(tex_name='Q_{err0}', # modified + self.Qerr0 = Algeb(tex_name='Q_{err0}', info='Reactive power error_0', - v_str='0', - e_str='Qtarget_1 - Qsite_filt_y - Qerr0', + v_str='Qtarget_1 - Qsite_y', + e_str='Qtarget_1 - Qsite_y - Qerr0', ) - # self.Qerr_int= State(tex_name='Q_{err_integral}', # modified - # info='Integrator state for Qerror', - # v_str='0', - # e_str='Kiq *Qerr0', - # ) # Integral path with anti-windup self.Qerr_int = IntegratorAntiWindup(u=self.Qerr0, T=1.0, K=self.Kiq, - y0=0, + y0='q0 - (Pref_site_0**2 + Qref_site_0**2) * Xloss - Qref_site_0', lower=self.Qerr_min, upper=self.Qerr_max, name='Qerr_int', @@ -1029,36 +1118,32 @@ def __init__(self, system, config): # Voltage control PI output (proportional + integral) self.Qerr_pi = Algeb(tex_name='Q_{vc}', info='Qerror PI output', - v_str='0', + v_str='q0 - (Pref_site_0**2 + Qref_site_0**2) * Xloss - Qref_site_0', e_str='Qerr_int_y - Qerr_pi', ) - - - - - self.Qerr_lim = Limiter(self.Qerr_pi, lower=self.Qerr_min, upper=self.Qerr_max, # modified + self.Qerr_lim = Limiter(self.Qerr_pi, lower=self.Qerr_min, upper=self.Qerr_max, tex_name='Q_{err,lim}', ) - self.Qerr_lim_val = Algeb(tex_name='Q_{vc,lim}', # modified + self.Qerr_lim_val = Algeb(tex_name='Q_{vc,lim}', info='Limited voltage control output', - v_str='0', + v_str='q0 - (Pref_site_0**2 + Qref_site_0**2) * Xloss - Qref_site_0', e_str='Qerr_pi * Qerr_lim_zi + Qerr_max * Qerr_lim_zu + Qerr_min * Qerr_lim_zl - Qerr_lim_val', ) - self.Qloss = Algeb(tex_name='Q_{loss,GFL}', # modified + self.Qloss = Algeb(tex_name='Q_{loss,GFL}', info='Reactive power loss value', - v_str='(Pref_site_0**2+Qref_site_0**2)*Xloss', + v_str='(Pref_site_0**2 + Qref_site_0**2) * Xloss', e_str='(Ptarget_1**2+ Qtarget_1**2)*Xloss - Qloss', ) - self.Qcmd_GFL_0 = Algeb(tex_name='Q_{cmd,GFL,0}', # modified + self.Qcmd_GFL_0 = Algeb(tex_name='Q_{cmd,GFL,0}', info='Reactive power command value(no lag)', - v_str='(Pref_site_0**2+Qref_site_0**2)*Xloss+Qref_site_0', + v_str='q0', e_str='Qloss + Qerr_lim_val + Qtarget_1 - Qcmd_GFL_0', ) - self.Qcmd_GFLLag = Lag(u='Qcmd_GFL_0', T=self.Tqlag , K=1, # modified + self.Qcmd_GFLLag = Lag(u='Qcmd_GFL_0', T=self.Tqlag , K=1, info='Reactive power command value', tex_name='Q_{cmd,GFL}', ) @@ -1068,10 +1153,9 @@ def __init__(self, system, config): ) Qcmd_out = 'Qcmd_GFLLag_y * Qcmd_lim_zi + Qcmd_GFL_max * Qcmd_lim_zu + Qcmd_GFL_min * Qcmd_lim_zl' + # Output to REGFMC1 reactive power command - # Qcmd_out = 'Qcmd_GFLLag_y' # modified self.Qcmd_GFL.e_str = f'{Qcmd_out} - (q0)' - # self.Qcmd_GFL.e_str = f'{Qcmd_out} - Qcmd_GFL' # # FFR update function @@ -1167,9 +1251,9 @@ class REPCGFMC1(REPCGFMC1Data, REPCGFMC1Model): 4. GFL reactive power and voltage control Notes: - - Site measurements are taken from the converter bus or optional remote bus - - Frequency measurement is simplified (uses constant 1.0 pu) - - Loss compensation uses simplified real-part calculation + - Voltage and frequency measurements are taken from ``busr``. + - Site P/Q measurements are taken from the monitored branch at ``busr``. + - ``Rloss`` and ``Xloss`` are user-specified loss-compensation values. """ def __init__(self, system, config): diff --git a/tests/models/renewable/test_regfmc1_repcgfmc1.py b/tests/models/renewable/test_regfmc1_repcgfmc1.py new file mode 100644 index 000000000..3e8050abb --- /dev/null +++ b/tests/models/renewable/test_regfmc1_repcgfmc1.py @@ -0,0 +1,111 @@ +import json +import os +import tempfile +import unittest + +import numpy as np + +import andes +from andes.utils.paths import get_case + + +class TestREGFMC1REPCGFMC1Measurements(unittest.TestCase): + """ + Tests for REGFMC1/REPCGFMC1 measurement wiring. + """ + + @staticmethod + def _write_case(): + with open(get_case('smib/SMIB.json')) as f: + data = json.load(f) + + data['GENCLS'] = [row for row in data['GENCLS'] + if row['idx'] != 'GENCLS_1'] + + data['REGFMC1'] = [{ + 'idx': 'REGFMC1_1', + 'u': 1.0, + 'name': 'REGFMC1_1', + 'bus': 1, + 'gen': 'PV_1', + 'FFFlag': 0.0, + }] + + data['REPCGFMC1'] = [{ + 'idx': 'REPCGFMC1_1', + 'u': 1.0, + 'name': 'REPCGFMC1_1', + 'reg': 'REGFMC1_1', + 'busr': 3, + 'line': 'Line_1', + 'Rloss': 0.0, + 'Xloss': 0.0, + 'FFRFlag': 0.0, + }] + + fd, path = tempfile.mkstemp(suffix='.json') + os.close(fd) + with open(path, 'w') as f: + json.dump(data, f) + return path + + def test_measurements_and_short_tds(self): + path = self._write_case() + + try: + ss = andes.load(path, no_output=True, default_config=True) + ss.PFlow.run() + self.assertTrue(ss.PFlow.converged) + + ss.TDS.config.tf = 0.02 + ss.TDS.config.tstep = 0.01 + ss.TDS.init() + self.assertTrue(ss.TDS.initialized) + + reg = ss.REGFMC1 + rep = ss.REPCGFMC1 + + inv_bus = reg.bus.v[0] + site_bus = rep.busr.v[0] + + np.testing.assert_equal( + rep.vinv.a[0], + ss.Bus.v.a[ss.Bus.idx2uid(inv_bus)], + err_msg='vinv must measure REGFMC1.bus') + np.testing.assert_equal( + rep.vsite.a[0], + ss.Bus.v.a[ss.Bus.idx2uid(site_bus)], + err_msg='vsite must measure REPCGFMC1.busr') + np.testing.assert_equal( + rep.v.a[0], + ss.Bus.v.a[ss.Bus.idx2uid(site_bus)], + err_msg='v must measure REPCGFMC1.busr') + + busfreq_uid = ss.BusFreq.idx2uid(rep.busfreq.v[0]) + self.assertEqual(ss.BusFreq.bus.v[busfreq_uid], site_bus) + + np.testing.assert_allclose(rep.Psite_raw.v[0], + -rep.Pline_bus2.v[0], + rtol=0.0, atol=1e-8) + np.testing.assert_allclose(rep.Qsite_raw.v[0], + -rep.Qline_bus2.v[0], + rtol=0.0, atol=1e-8) + self.assertGreater(rep.Psite_raw.v[0], 0.0) + + np.testing.assert_allclose(rep.Psite_y.v[0], rep.Psite_raw.v[0], + rtol=0.0, atol=1e-8) + np.testing.assert_allclose(rep.Qsite_y.v[0], rep.Qsite_raw.v[0], + rtol=0.0, atol=1e-8) + + self.assertEqual(rep.Rloss.v[0], 0.0) + self.assertEqual(rep.Xloss.v[0], 0.0) + + ss.TDS.run(no_summary=True) + self.assertTrue(ss.TDS.converged) + self.assertEqual(ss.exit_code, 0) + finally: + os.unlink(path) + + +if __name__ == '__main__': + unittest.main() From 748a9533d433654443450e31f41156910f5af9ac Mon Sep 17 00:00:00 2001 From: Zilin Zhuang Date: Wed, 3 Jun 2026 09:51:19 -0400 Subject: [PATCH 6/6] Update model files --- andes/models/renewable/regfmc1.py | 24 ++++++++++++------------ andes/models/renewable/repcgfmc1.py | 19 +++++++------------ 2 files changed, 19 insertions(+), 24 deletions(-) diff --git a/andes/models/renewable/regfmc1.py b/andes/models/renewable/regfmc1.py index 7e044e7bc..758583235 100644 --- a/andes/models/renewable/regfmc1.py +++ b/andes/models/renewable/regfmc1.py @@ -190,7 +190,7 @@ def __init__(self): info='Time constant for voltage filter in GFL', unit='s', ) - self.kqv = NumParam(default=2, # modified + self.kqv = NumParam(default=2, tex_name='k_{qv}', info='Voltage error gain in GFL', current=True, @@ -360,7 +360,7 @@ def __init__(self, system, config): ) - # Reactive power measurement path (per diagram: Iq_GFM filtered through 1/(Tif*s+1)) + # Reactive power measurement path self.Iq_VSMLag = Lag(u='Iq_VSM', T=self.TIf, K=1, info='Filter for I_q GFM', name='Iq_VSMLag') @@ -553,13 +553,13 @@ def __init__(self, system, config): K=1, info='GFL voltage filter', name='VinvGFLLag', - ) # same as VinvLag_y + ) # Voltage error for GFL self.Verr_GFL = Algeb(tex_name='V_{err,GFL}', info='Voltage error for GFL', v_str='Vref0 - v', - e_str='Vref0 - VinvGFLLag_y - Verr_GFL', # Vref0 or VrefLag_y? shouldbe Vref0! + e_str='Vref0 - VinvGFLLag_y - Verr_GFL', ) # Offset piecewise-linear deadband for GFL voltage error: @@ -661,14 +661,14 @@ def __init__(self, system, config): ) - # --- Current Calculation (PLACEHOLDER - simplified) --- - # GFM branch current magnitude (simplified) + # --- Current Calculation --- + # GFM branch current magnitude self.IVSM_mag = VarService(tex_name='I_{VSM,mag}', info='GFM branch current magnitude', v_str='sqrt(Id_VSM_lim**2 + Iq_VSM_lim**2)', ) - # GFM branch current angle (simplified) + # GFM branch current angle self.IVSM_ang = VarService(tex_name=r'\phi_{VSM}', info='GFM branch current angle', v_str='a - atan2(Iq_VSM_lim, Id_VSM_lim + 1e-8)', @@ -677,7 +677,7 @@ def __init__(self, system, config): # GFL branch current magnitude # ---------- dq -> xy(αβ) rotation for GFL current ---------- - self.deltaV = VarService(v_str='a', tex_name=r'\delta_V', info='dq to xy rotation angle') # relative angle? + self.deltaV = VarService(v_str='a', tex_name=r'\delta_V', info='dq to xy rotation angle') self.Ialpha_GFL = Algeb( name='Ialpha_GFL', v_str='Id0_GFL* cos(a) + ( kqv * (Vref0 - v) + Iq0_GFL )* sin(a) ', @@ -691,7 +691,7 @@ def __init__(self, system, config): ) self.phi_gfl = Algeb( name='phi_gfl', v_str='atan2(Id0_GFL * sin(a) - (kqv * (Vref0 - v) + Iq0_GFL) * cos(a), Id0_GFL * cos(a) + (kqv * (Vref0 - v) + Iq0_GFL) * sin(a))', - e_str='atan2(Ibeta_GFL, Ialpha_GFL) - phi_gfl', # might have problem! + e_str='atan2(Ibeta_GFL, Ialpha_GFL) - phi_gfl', tex_name=r'\phi_{GFL}', info='GFL current angle in xy' ) @@ -701,11 +701,11 @@ def __init__(self, system, config): e_str='sqrt(Ip_GFL_lim**2 + Iq_GFL_lim**2) - IGFL_mag', ) - # Total current magnitude (PLACEHOLDER - vector sum needed) + # Total current magnitude self.Itotal = Algeb(tex_name='I_{total}', info='Total current magnitude (PLACEHOLDER)', v_str='sqrt(Id0_GFL**2 + (kqv * (Vref0 - v) + Iq0_GFL)**2)', - e_str='sqrt((Id_VSM + Ipcmd_sat_val)**2 + (Iq_VSM + Iqcmd_sat_val)**2 ) - Itotal', # Simplified, should be vector sum + e_str='sqrt((Id_VSM + Ipcmd_sat_val)**2 + (Iq_VSM + Iqcmd_sat_val)**2 ) - Itotal', ) self.k_scale = VarService( @@ -868,7 +868,7 @@ def v_numeric(self, **kwargs): class REGFMC1(REGFMC1Data, REGFMC1Model): """ - Hybrid Grid-Forming Converter Model (REGFMC1). + Hybrid GFL/GFM Converter Model (REGFMC1). This model represents a parallel combination of: - Grid-forming (GFM) voltage source with series impedance and VSM control diff --git a/andes/models/renewable/repcgfmc1.py b/andes/models/renewable/repcgfmc1.py index 8acf8a087..cb481e316 100644 --- a/andes/models/renewable/repcgfmc1.py +++ b/andes/models/renewable/repcgfmc1.py @@ -236,7 +236,7 @@ def __init__(self): power=True, ) - self.Kip = NumParam(default=0.1, #0 + self.Kip = NumParam(default=0.1, tex_name='K_{ip}', info='Proportional gain for active power PI controller', ) @@ -323,13 +323,13 @@ def __init__(self): unit='p.u.', ) - self.Kp_vc = NumParam(default=2, # 40 + self.Kp_vc = NumParam(default=2, tex_name='K_{p,vc}', info='Voltage control proportional gain', power=True, ) - self.Ki_vc = NumParam(default=6, # 2 + self.Ki_vc = NumParam(default=6, tex_name='K_{i,vc}', info='Voltage control integral gain', power=True, @@ -598,9 +598,6 @@ def __init__(self, system, config): e_str=f'{Qji} - Qline_bus2', ) - # Line-side Pij/Pji and Qij/Qji are positive out of the bus into the - # branch. Site injection is power entering the PCC from the plant, so - # the selected PCC-side branch outflow is negated. Psite_raw = ('-(0.5 * (1 + MeasBusSign) * Pline_bus1 + ' '0.5 * (1 - MeasBusSign) * Pline_bus2)') Qsite_raw = ('-(0.5 * (1 + MeasBusSign) * Qline_bus1 + ' @@ -949,8 +946,6 @@ def __init__(self, system, config): # Active power reference with frequency droop # Ptarget is a parameter, so Pref = Ptarget # Power error calculation - - self.Perr = Algeb(tex_name='P_{err}', info='Site power error', v_str='Ptarget_1 - Psite_y', @@ -1006,7 +1001,7 @@ def __init__(self, system, config): self.Pcmd_GFL.e_str = f'{Pcmd_out} - (p0)' # --- GFL Reactive Power Path --- - # Voltage control path (using internal Vref_site Algeb) + # Voltage control path self.Verr_site = Algeb(tex_name='V_{err,site}', info='Site voltage error', v_str='Vref_site_0 - v', @@ -1043,7 +1038,7 @@ def __init__(self, system, config): info='Voltage control integrator with anti-windup', ) - # Voltage control PI output (proportional + integral) + # Voltage control PI output self.Qvc = Algeb(tex_name='Q_{vc}', info='Voltage control PI output', v_str='Qref_site_0', @@ -1115,7 +1110,7 @@ def __init__(self, system, config): info='Integrator state for Qerror', ) - # Voltage control PI output (proportional + integral) + # Voltage control PI output self.Qerr_pi = Algeb(tex_name='Q_{vc}', info='Qerror PI output', v_str='q0 - (Pref_site_0**2 + Qref_site_0**2) * Xloss - Qref_site_0', @@ -1238,7 +1233,7 @@ def g_numeric(self, **kwargs): class REPCGFMC1(REPCGFMC1Data, REPCGFMC1Model): """ - REPCGFMC1: Plant controller for REGFMC1 (hybrid grid-forming converter). + REPCGFMC1: Plant controller for REGFMC1 (hybrid GFL/GFM converter). This model provides reference signals to REGFMC1: - GFM Branch: Voltage reference (Vref_GFM) and frequency reference (fref_GFM)