def __init__(self, system, config): TestFrame.__init__(self, system, config) self.Text = NumParam(default=1.0, info='Extended event time', unit='s') self.uin = Algeb( v_str=0, e_str='sin(dae_t) - uin', tex_name='u_{in}', ) self.zf = Algeb( v_str=0, e_str= 'Piecewise((0, dae_t <= 2), (1, dae_t <=6), (0, dae_t<=12), (1, dae_t<=15), ' '(0, True)) - zf', tex_name='z_f', ) self.PI = PIController(u=self.uin, kp=1, ki=0.1) self.PIF = PIFreeze(u=self.uin, kp=0.5, ki=0.5, x0=0, freeze=self.zf) self.PIAW = PITrackAW( u=self.uin, kp=0.5, ki=0.5, ks=2, lower=-0.5, upper=0.5, x0=0.0, ) self.PIAWF = PITrackAWFreeze(u=self.uin, kp=0.5, ki=0.5, ks=2, x0=0, freeze=self.zf, lower=-0.5, upper=0.5) self.ExtEvent = ExtendedEvent(u=self.zf, t_ext=self.Text, trig='rise', extend_only=True) self.ze = Algeb(v_str='ExtEvent', e_str='ExtEvent - ze')
def __init__(self, system, config): Model.__init__(self, system, config) self.flags.tds = True self.group = 'RenExciter' self.config.add(OrderedDict((('kqs', 2), ('kvs', 2), ('tpfilt', 0.02), ))) self.config.add_extra('_help', kqs='Q PI controller tracking gain', kvs='Voltage PI controller tracking gain', tpfilt='Time const. for Pref filter', ) self.config.add_extra('_tex', kqs='K_{qs}', kvs='K_{vs}', tpfilt='T_{pfilt}', ) # --- Sanitize inputs --- self.Imaxr = Replace(self.Imax, flt=lambda x: np.less_equal(x, 0), new_val=1e8, tex_name='I_{maxr}') # --- Flag switchers --- self.SWPF = Switcher(u=self.PFFLAG, options=(0, 1), tex_name='SW_{PF}', cache=True) self.SWV = Switcher(u=self.VFLAG, options=(0, 1), tex_name='SW_{V}', cache=True) self.SWQ = Switcher(u=self.QFLAG, options=(0, 1), tex_name='SW_{V}', cache=True) self.SWP = Switcher(u=self.PFLAG, options=(0, 1), tex_name='SW_{P}', cache=True) self.SWPQ = Switcher(u=self.PQFLAG, options=(0, 1), tex_name='SW_{PQ}', cache=True) # --- External parameters --- self.bus = ExtParam(model='RenGen', src='bus', indexer=self.reg, export=False, info='Retrieved bus idx', vtype=str, default=None, ) self.buss = DataSelect(self.busr, self.bus, info='selected bus (bus or busr)') self.gen = ExtParam(model='RenGen', src='gen', indexer=self.reg, export=False, info='Retrieved StaticGen idx', vtype=str, default=None, ) self.Sn = ExtParam(model='RenGen', src='Sn', indexer=self.reg, tex_name='S_n', export=False, ) # --- External variables --- self.a = ExtAlgeb(model='Bus', src='a', indexer=self.bus, tex_name=r'\theta', info='Bus voltage angle', ) self.v = ExtAlgeb(model='Bus', src='v', indexer=self.bus, tex_name=r'V', info='Bus voltage magnitude', ) # check whether to use `bus` or `buss` self.Pe = ExtAlgeb(model='RenGen', src='Pe', indexer=self.reg, export=False, info='Retrieved Pe of RenGen') self.Qe = ExtAlgeb(model='RenGen', src='Qe', indexer=self.reg, export=False, info='Retrieved Qe of RenGen') self.Ipcmd = ExtAlgeb(model='RenGen', src='Ipcmd', indexer=self.reg, export=False, info='Retrieved Ipcmd of RenGen', e_str='-Ipcmd0 + IpHL_y', ) self.Iqcmd = ExtAlgeb(model='RenGen', src='Iqcmd', indexer=self.reg, export=False, info='Retrieved Iqcmd of RenGen', e_str='-Iqcmd0 - IqHL_y', ) self.p0 = ExtService(model='RenGen', src='p0', indexer=self.reg, tex_name='P_0', ) self.q0 = ExtService(model='RenGen', src='q0', indexer=self.reg, tex_name='Q_0', ) # Initial current commands self.Ipcmd0 = ConstService('p0 / v', info='initial Ipcmd') self.Iqcmd0 = ConstService('-q0 / v', info='initial Iqcmd') # --- Initial power factor angle --- # NOTE: if `p0` = 0, `pfaref0` = pi/2, `tan(pfaref0)` = inf self.pfaref0 = ConstService(v_str='atan2(q0, p0)', tex_name=r'\Phi_{ref0}', info='Initial power factor angle', ) # flag devices with `p0`=0, which causes `tan(PF) = +inf` self.zp0 = ConstService(v_str='Eq(p0, 0)', vtype=float, tex_name='z_{p0}', ) # --- Discrete components --- self.Vcmp = Limiter(u=self.v, lower=self.Vdip, upper=self.Vup, tex_name='V_{cmp}', info='Voltage dip comparator', equal=False, ) self.Volt_dip = VarService(v_str='1 - Vcmp_zi', info='Voltage dip flag; 1-dip, 0-normal', tex_name='z_{Vdip}', ) # --- Equations begin --- self.s0 = Lag(u=self.v, T=self.Trv, K=1, info='Voltage filter', ) self.VLower = Limiter(u=self.v, lower=0.01, upper=999, no_upper=True, info='Limiter for lower voltage cap', ) self.vp = Algeb(tex_name='V_p', info='Sensed lower-capped voltage', v_str='v * VLower_zi + 0.01 * VLower_zl', e_str='v * VLower_zi + 0.01 * VLower_zl - vp', ) self.pfaref = Algeb(tex_name=r'\Phi_{ref}', info='power factor angle ref', unit='rad', v_str='pfaref0', e_str='pfaref0 - pfaref', ) self.S1 = Lag(u='Pe', T=self.Tp, K=1, tex_name='S_1', info='Pe filter', ) # ignore `Qcpf` if `pfaref` is pi/2 by multiplying (1-zp0) self.Qcpf = Algeb(tex_name='Q_{cpf}', info='Q calculated from P and power factor', v_str='q0', e_str='(1-zp0) * (S1_y * tan(pfaref) - Qcpf)', diag_eps=True, unit='p.u.', ) self.Qref = Algeb(tex_name='Q_{ref}', info='external Q ref', v_str='q0', e_str='q0 - Qref', unit='p.u.', ) self.PFsel = Algeb(v_str='SWPF_s0*Qref + SWPF_s1*Qcpf', e_str='SWPF_s0*Qref + SWPF_s1*Qcpf - PFsel', info='Output of PFFLAG selector', ) self.PFlim = Limiter(u=self.PFsel, lower=self.QMin, upper=self.QMax) self.Qerr = Algeb(tex_name='Q_{err}', info='Reactive power error', v_str='(PFsel*PFlim_zi + QMin*PFlim_zl + QMax*PFlim_zu) - Qe', e_str='(PFsel*PFlim_zi + QMin*PFlim_zl + QMax*PFlim_zu) - Qe - Qerr', ) self.PIQ = PITrackAWFreeze(u=self.Qerr, kp=self.Kqp, ki=self.Kqi, ks=self.config.kqs, lower=self.VMIN, upper=self.VMAX, freeze=self.Volt_dip, ) # If `VFLAG=0`, set the input as `Vref1` (see the NREL report) self.Vsel = GainLimiter(u='SWV_s0 * Vref1 + SWV_s1 * PIQ_y', K=1, R=1, lower=self.VMIN, upper=self.VMAX, info='Selection output of VFLAG', ) # --- Placeholders for `Iqmin` and `Iqmax` --- self.s4 = LagFreeze(u='PFsel / vp', T=self.Tiq, K=1, freeze=self.Volt_dip, tex_name='s_4', info='Filter for calculated voltage with freeze', ) # --- Upper portion - Iqinj calculation --- self.Verr = Algeb(info='Voltage error (Vref0)', v_str='Vref0 - s0_y', e_str='Vref0 - s0_y - Verr', tex_name='V_{err}', ) self.dbV = DeadBand1(u=self.Verr, lower=self.dbd1, upper=self.dbd2, center=0.0, enable='DB_{V}', info='Deadband for voltage error (ref0)' ) self.pThld = ConstService(v_str='Indicator(Thld > 0)', tex_name='p_{Thld}') self.nThld = ConstService(v_str='Indicator(Thld < 0)', tex_name='n_{Thld}') self.Thld_abs = ConstService(v_str='abs(Thld)', tex_name='|Thld|') self.fThld = ExtendedEvent(self.Volt_dip, t_ext=self.Thld_abs, ) # Gain after dbB Iqv = "(dbV_y * Kqv)" Iqinj = f'{Iqv} * Volt_dip + ' \ f'(1 - Volt_dip) * fThld * ({Iqv} * nThld + Iqfrz * pThld)' # state transition, output of Iqinj self.Iqinj = Algeb(v_str=Iqinj, e_str=Iqinj + ' - Iqinj', tex_name='I_{qinj}', info='Additional Iq signal during under- or over-voltage', ) # --- Lower portion - active power --- self.wg = Algeb(tex_name=r'\omega_g', info='Drive train generator speed', v_str='1.0', e_str='1.0 - wg', ) self.Pref = Algeb(tex_name='P_{ref}', info='external P ref', v_str='p0 / wg', e_str='p0 / wg - Pref', unit='p.u.', ) self.pfilt = LagRate(u=self.Pref, T=self.config.tpfilt, K=1, rate_lower=self.dPmin, rate_upper=self.dPmax, info='Active power filter with rate limits', tex_name='P_{filt}', ) self.Psel = Algeb(tex_name='P_{sel}', info='Output selection of PFLAG', v_str='SWP_s1*wg*pfilt_y + SWP_s0*pfilt_y', e_str='SWP_s1*wg*pfilt_y + SWP_s0*pfilt_y - Psel', ) # `s5_y` is `Pord` self.s5 = LagAWFreeze(u=self.Psel, T=self.Tpord, K=1, lower=self.PMIN, upper=self.PMAX, freeze=self.Volt_dip, tex_name='s5', ) self.Pord = AliasState(self.s5_y) # --- Current limit logic --- self.kVq12 = ConstService(v_str='(Iq2 - Iq1) / (Vq2 - Vq1)', tex_name='k_{Vq12}', ) self.kVq23 = ConstService(v_str='(Iq3 - Iq2) / (Vq3 - Vq2)', tex_name='k_{Vq23}', ) self.kVq34 = ConstService(v_str='(Iq4 - Iq3) / (Vq4 - Vq3)', tex_name='k_{Vq34}', ) self.zVDL1 = ConstService(v_str='(Vq1 <= Vq2) & (Vq2 <= Vq3) & (Vq3 <= Vq4) & ' '(Iq1 <= Iq2) & (Iq2 <= Iq3) & (Iq3 <= Iq4)', tex_name='z_{VDL1}', info='True if VDL1 is in service', ) self.VDL1 = Piecewise(u=self.s0_y, points=('Vq1', 'Vq2', 'Vq3', 'Vq4'), funs=('Iq1', f'({self.s0_y.name} - Vq1) * kVq12 + Iq1', f'({self.s0_y.name} - Vq2) * kVq23 + Iq2', f'({self.s0_y.name} - Vq3) * kVq34 + Iq3', 'Iq4'), tex_name='V_{DL1}', info='Piecewise linear characteristics of Vq-Iq', ) self.kVp12 = ConstService(v_str='(Ip2 - Ip1) / (Vp2 - Vp1)', tex_name='k_{Vp12}', ) self.kVp23 = ConstService(v_str='(Ip3 - Ip2) / (Vp3 - Vp2)', tex_name='k_{Vp23}', ) self.kVp34 = ConstService(v_str='(Ip4 - Ip3) / (Vp4 - Vp3)', tex_name='k_{Vp34}', ) self.zVDL2 = ConstService(v_str='(Vp1 <= Vp2) & (Vp2 <= Vp3) & (Vp3 <= Vp4) & ' '(Ip1 <= Ip2) & (Ip2 <= Ip3) & (Ip3 <= Ip4)', tex_name='z_{VDL2}', info='True if VDL2 is in service', ) self.VDL2 = Piecewise(u=self.s0_y, points=('Vp1', 'Vp2', 'Vp3', 'Vp4'), funs=('Ip1', f'({self.s0_y.name} - Vp1) * kVp12 + Ip1', f'({self.s0_y.name} - Vp2) * kVp23 + Ip2', f'({self.s0_y.name} - Vp3) * kVp34 + Ip3', 'Ip4'), tex_name='V_{DL2}', info='Piecewise linear characteristics of Vp-Ip', ) self.fThld2 = ExtendedEvent(self.Volt_dip, t_ext=self.Thld2, extend_only=True, ) self.VDL1c = VarService(v_str='Lt(VDL1_y, Imaxr)') self.VDL2c = VarService(v_str='Lt(VDL2_y, Imaxr)') # `Iqmax` not considering mode or `Thld2` Iqmax1 = '(zVDL1*(VDL1c*VDL1_y + (1-VDL1c)*Imaxr) + 1e8*(1-zVDL1))' # `Ipmax` not considering mode or `Thld2` Ipmax1 = '(zVDL2*(VDL2c*VDL2_y + (1-VDL2c)*Imaxr) + 1e8*(1-zVDL2))' Ipmax2sq0 = '(Imax**2 - Iqcmd0**2)' Ipmax2sq = '(Imax**2 - IqHL_y**2)' # `Ipmax20`-squared (non-negative) self.Ipmax2sq0 = ConstService(v_str=f'Piecewise((0, Le({Ipmax2sq0}, 0.0)), ({Ipmax2sq0}, True), \ evaluate=False)', tex_name='I_{pmax20,nn}^2', ) self.Ipmax2sq = VarService(v_str=f'Piecewise((0, Le({Ipmax2sq}, 0.0)), ({Ipmax2sq}, True), \ evaluate=False)', tex_name='I_{pmax2}^2', ) Ipmax = f'((1-fThld2) * (SWPQ_s0*sqrt(Ipmax2sq) + SWPQ_s1*{Ipmax1}))' Ipmax0 = f'((1-fThld2) * (SWPQ_s0*sqrt(Ipmax2sq0) + SWPQ_s1*{Ipmax1}))' self.Ipmax = Algeb(v_str=f'{Ipmax0}', e_str=f'{Ipmax} + (fThld2 * Ipmaxh) - Ipmax', tex_name='I_{pmax}', diag_eps=True, info='Upper limit on Ipcmd', ) self.Ipmaxh = VarHold(self.Ipmax, hold=self.fThld2) Iqmax2sq = '(Imax**2 - IpHL_y**2)' Iqmax2sq0 = '(Imax**2 - Ipcmd0**2)' # initialization equation by using `Ipcmd0` self.Iqmax2sq0 = ConstService(v_str=f'Piecewise((0, Le({Iqmax2sq0}, 0.0)), ({Iqmax2sq0}, True), \ evaluate=False)', tex_name='I_{qmax,nn}^2', ) self.Iqmax2sq = VarService(v_str=f'Piecewise((0, Le({Iqmax2sq}, 0.0)), ({Iqmax2sq}, True), \ evaluate=False)', tex_name='I_{qmax2}^2') self.Iqmax = Algeb(v_str=f'(SWPQ_s0*{Iqmax1} + SWPQ_s1*sqrt(Iqmax2sq0))', e_str=f'(SWPQ_s0*{Iqmax1} + SWPQ_s1*sqrt(Iqmax2sq)) - Iqmax', tex_name='I_{qmax}', info='Upper limit on Iqcmd', ) self.Iqmin = ApplyFunc(self.Iqmax, lambda x: -x, cache=False, tex_name='I_{qmin}', info='Lower limit on Iqcmd', ) self.Ipmin = ConstService(v_str='0.0', tex_name='I_{pmin}', info='Lower limit on Ipcmd', ) self.PIV = PITrackAWFreeze(u='Vsel_y - s0_y * SWV_s0', x0='-SWQ_s1 * Iqcmd0', kp=self.Kvp, ki=self.Kvi, ks=self.config.kvs, lower=self.Iqmin, upper=self.Iqmax, freeze=self.Volt_dip, ) self.Qsel = Algeb(info='Selection output of QFLAG', v_str='SWQ_s1 * PIV_y + SWQ_s0 * s4_y', e_str='SWQ_s1 * PIV_y + SWQ_s0 * s4_y - Qsel', tex_name='Q_{sel}', ) # `IpHL_y` is `Ipcmd` self.IpHL = GainLimiter(u='s5_y / vp', K=1, R=1, lower=self.Ipmin, upper=self.Ipmax, ) # `IqHL_y` is `Iqcmd` self.IqHL = GainLimiter(u='Qsel + Iqinj', K=1, R=1, lower=self.Iqmin, upper=self.Iqmax)
def __init__(self, system, config): Model.__init__(self, system, config) self.flags.tds = True self.group = 'DGProtection' self.bus = ExtParam(model='DG', src='bus', indexer=self.dev, export=False) self.fn = ExtParam(model='DG', src='fn', indexer=self.dev, export=False) # -- Frequency protection # Convert frequency deviation range to p.u. self.f = ExtAlgeb(export=False, info='DG frequency read value', unit='p.u.', model='FreqMeasurement', src='f', indexer=self.busfreq, ) self.fHz = Algeb(v_str='fn * f', e_str='fn * f - fHz', info='frequency in Hz', tex_name=r'f_{Hz}', ) # -- Lock DG frequency signal and output power self.ltu = ConstService(v_str='0.8') self.ltl = ConstService(v_str='0.2') # `Ldsum_zu` is `ue` dsum = 'fen * (IAWfl1_lim_zu * Lfl1_zi + IAWfl2_lim_zu * Lfl2_zi + ' \ 'IAWfu1_lim_zu * Lfu1_zi + IAWfu2_lim_zu * Lfu2_zi) + ' \ 'Ven * (IAWVl1_lim_zu * LVl1_zi + IAWVl2_lim_zu * LVl2_zi + ' \ 'IAWVl3_lim_zu * LVl3_zi + ' \ 'IAWVu1_lim_zu * LVu1_zi + IAWVu2_lim_zu * LVu2_zi) - ' \ 'dsum' self.dsum = Algeb(v_str='0', e_str=dsum, info='lock signal summation', tex_name=r'd_{tot}', ) self.Ldsum = Limiter(u=self.dsum, lower=self.ltl, upper=self.ltu, info='lock signal comparer, zu is to act', equal=False, no_warn=True, ) self.ue = Algeb(v_str='0', e_str='Ldsum_zu - ue', info='lock flag', tex_name=r'ue', ) self.zero = ConstService('0') self.res = ExtendedEvent(self.ue, t_ext=self.Tres, trig="rise", extend_only=True) # lock DG frequency signal # fflag option 1: leave source signal online in protection self.fin = ExtAlgeb(model='DG', src='f', indexer=self.dev, info='original f from DG', ) self.fHzl = ExtAlgeb(model='DG', src='fHz', indexer=self.dev, export=False, e_str='- ue * (fn * f)', info='Frequency measure lock', ename='fHzl', tex_ename='f_{Hzl}', ) # TODO: add fflag option 2: block the source signal in protection # lock output power of DG self.Pext = ExtAlgeb(model='DG', src='Pext', indexer=self.dev, info='original Pext from DG', ) self.Pref = ExtAlgeb(model='DG', src='Pref', indexer=self.dev, info='original Pref from DG', ) self.Pdrp = ExtAlgeb(model='DG', src='DB_y', indexer=self.dev, info='original Pdrp from DG', ) self.Psum = ExtAlgeb(model='DG', src='Psum', indexer=self.dev, export=False, e_str='- ue * (Pext + Pref + Pdrp)', info='Active power lock', ename='Pneg', tex_ename='P_{neg}', ) self.Qdrp = ExtAlgeb(model='DG', src='Qdrp', indexer=self.dev, info='original Qdrp from DG', ) self.Qref = ExtAlgeb(model='DG', src='Qref', indexer=self.dev, info='original Qref from DG', ) self.Qsum = ExtAlgeb(model='DG', src='Qsum', indexer=self.dev, export=False, e_str='- ue * (Qdrp + Qref)', info='Reactive power lock', ename='Qneg', tex_ename='Q_{neg}', )