Examples#

We show two examples to demonstrate modeling from equations and modeling from control block diagrams.

  • The TGOV1 example shows code snippet for equation-based modeling and, as well as code for block-based modeling.

  • The IEEEST example walks through the source code and explains the complete setup, including optional parameters, input selection, and manual per-unit conversion.

TGOV1#

The TGOV1 turbine governor model is shown as a practical example using the library.

../_images/tgov1.png

This model is composed of a lead-lag transfer function and a first-order lag transfer function with an anti-windup limiter, which are sufficiently complex for demonstration. The corresponding differential equations and algebraic equations are given below.

\[ \begin{align}\begin{aligned}\begin{split}\left[ \begin{matrix} \dot{x}_{LG} \\ \dot{x}_{LL} \end{matrix} \right] = \left[ \begin{matrix}z_{i,lim}^{LG} \left(P_{d} - x_{LG}\right) / {T_1} \\ \left(x_{LG} - x_{LL}\right) / T_3 \end{matrix} \right]\end{split}\\\begin{split}\left[ \begin{matrix} 0 \\ 0 \\ 0 \\ 0 \\ 0 \\ 0 \end{matrix} \right] = \left[ \begin{matrix} (1 - \omega) - \omega_{d} \\ R \times \tau_{m0} - P_{ref} \\ \left(P_{ref} + \omega_{d}\right)/R - P_{d}\\ D_{t} \omega_{d} + y_{LL} - P_{OUT}\\ \frac{T_2}{T_3} \left(x_{LG} - x_{LL}\right) + x_{LL} - y_{LL}\\ u \left(P_{OUT} - \tau_{m0}\right) \end{matrix} \right]\end{split}\end{aligned}\end{align} \]

where LG and LL denote the lag block and the lead-lag block, \(\dot{x}_{LG}\) and \(\dot{x}_{LL}\) are the internal states, \(y_{LL}\) is the lead-lag output, \(\omega\) the generator speed, \(\omega_d\) the generator under-speed, \(P_d\) the droop output, \(\tau_{m0}\) the steady-state torque input, and \(P_{OUT}\) the turbine output that will be summed at the generator.

The code to describe the above model using equations is given below. The complete code can be found in class TGOV1ModelAlt in andes/models/governor.py.

def __init__(self, system, config):
  # 1. Declare parameters from case file inputs.
  self.R = NumParam(info='Turbine governor droop',
                    non_zero=True, ipower=True)
  # Other parameters are omitted.

  # 2. Declare external variables from generators.
  self.omega = ExtState(src='omega',
                 model='SynGen',
                 indexer=self.syn,
                 info='Generator speed')
  self.tm = ExtAlgeb(src='tm',
              model='SynGen',
              indexer=self.syn,
              e_str='u*(pout-tm0)',
              info='Generator torque input')

  # 3. Declare initial values from generators.
  self.tm0 = ExtService(src='tm',
               model='SynGen',
               indexer=self.syn,
               info='Initial torque input')

  # 4. Declare variables and equations.
  self.pref = Algeb(info='Reference power input',
                v_str='tm0*R',
                e_str='tm0*R-pref')
  self.wd = Algeb(info='Generator under speed',
              e_str='(1-omega)-wd')
  self.pd = Algeb(info='Droop output',
              v_str='tm0',
              e_str='(wd+pref)/R-pd')
  self.LG_x = State(info='State in the lag TF',
                v_str='pd',
                e_str='LG_lim_zi*(pd-LG_x)/T1')
  self.LG_lim = AntiWindup(u=self.LG_x,
                  lower=self.VMIN,
                  upper=self.VMAX)
  self.LL_x = State(info='State in the lead-lag TF',
                v_str='LG_x',
                e_str='(LG_x-LL_x)/T3')
  self.LL_y = Algeb(info='Lead-lag Output',
                v_str='LG_x',
                e_str='T2/T3*(LG_x-LL_x)+LL_x-LL_y')
  self.pout = Algeb(info='Turbine output power',
                v_str='tm0',
                e_str='(LL_y+Dt*wd)-pout')

Another implementation of TGOV1 makes extensive use of the modeling blocks. The resulting code is more readable as follows.

def __init__(self, system, config):
    TGBase.__init__(self, system, config)

    self.gain = ConstService(v_str='u/R')

    self.pref = Algeb(info='Reference power input',
                      tex_name='P_{ref}',
                      v_str='tm0 * R',
                      e_str='tm0 * R - pref',
                      )

    self.wd = Algeb(info='Generator under speed',
                    unit='p.u.',
                    tex_name=r'\omega_{dev}',
                    v_str='0',
                    e_str='(wref - omega) - wd',
                    )
    self.pd = Algeb(info='Pref plus under speed times gain',
                    unit='p.u.',
                    tex_name="P_d",
                    v_str='u * tm0',
                    e_str='u*(wd + pref + paux) * gain - pd')

    self.LAG = LagAntiWindup(u=self.pd,
                             K=1,
                             T=self.T1,
                             lower=self.VMIN,
                             upper=self.VMAX,
                             )

    self.LL = LeadLag(u=self.LAG_y,
                      T1=self.T2,
                      T2=self.T3,
                      )

    self.pout.e_str = '(LL_y + Dt * wd) - pout'

The complete code can be found in class TGOV1Model in andes/models/governor.py.

IEEEST#

In this example, we will explain step-by-step how IEEEST is programmed. The block diagram of IEEEST is given as follows. We recommend you to open up the source code in andes/models/pss.py and then continue reading.

../_images/ieeest.png

First of all, modeling components are imported at the beginning.

Next, PSSBaseData is defined to hold parameters shared by all PSSs. PSSBaseData inherits from ModelData and calls the base constructor. There is only one field avr defined for the linked exciter idx.

Then, IEEESTData defines the input parameters for IEEEST. Use IdxParam for fields that store idx-es of devices that IEEEST devices link to. Use NumParam for numerical parameters.

PSSBase#

PSSBase is defined for the common (external) parameters, services and variables shared by all PSSs. The class and constructor signatures are

class PSSBase(Model):
    def __init__(self, system, config):
        super().__init__(system, config)

PSSBase inherits from Model and calls the base constructor. Note that the call to Model's constructor takes two positional arguments, system and config of types System and ModelConfig. Next, the group is specified, and the model flags are set.

Note

It is important to set the TDS flag to register the model. If not set, data for the model will be successfully loaded, but the variables in the model will not receive any address, and the model equations will be skipped.

There is a similar flag self.flags.pflow for models to participate in power flow calculations. Most dynamic models, however, are initialized after power flow.

self.group = 'PSS'
self.flags.tds = True

Next, Replace is used to replace input parameters that satisfy a lambda function with new values.

self.VCUr = Replace(self.VCU, lambda x: np.equal(x, 0.0), 999)
self.VCLr = Replace(self.VCL, lambda x: np.equal(x, 0.0), -999)

The value replacement happens when VCUr and VCLr is first accessed. Replace is executed in the model initialization phase (at the end of services update).

Next, the indices of connected generators, buses, and bus frequency measurements are retrieved. Synchronous generator idx is retrieved with

self.syn = ExtParam(model='Exciter', src='syn', indexer=self.avr, export=False,
                    info='Retrieved generator idx', vtype=str)

Using the retrieved self.syn, it retrieves the buses to which the generators are connected.

self.bus = ExtParam(model='SynGen', src='bus', indexer=self.syn, export=False,
                    info='Retrieved bus idx', vtype=str, default=None,
                    )

PSS models support an optional remote bus specified through parameter busr. When busr is None, the generator-connected bus should be used. The following code uses DataSelect to select busr if available but falls back to bus otherwise.

self.buss = DataSelect(self.busr, self.bus, info='selected bus (bus or busr)')

Each PSS links to a bus frequency measurement device. If the input data does not specify one or the specified one does not exist, DeviceFinder can find the correct measurement device for the bus where frequency measurements should be taken.

self.busfreq = DeviceFinder(self.busf, link=self.buss, idx_name='bus')

where busf is the optional frequency measurement device idx, buss is the bus idx for which measurement device needs to be found or created.

Next, external parameters, variables and services are retrieved. Note that the PSS output vsout is pre-allocated but the equation string is left to specific models.

IEEESTModel#

IEEESTModel inherits from PSSBase and adds specific model components. After calling PSSBase's constructor, IEEESTModel adds config entries to allow specifying the model for frequency measurement, because there may be multiple frequency measurement models in the future.

self.config.add(OrderedDict([('freq_model', 'BusFreq')]))
self.config.add_extra('_help', {'freq_model': 'default freq. measurement model'})
self.config.add_extra('_alt', {'freq_model': ('BusFreq',)})

We set the chosen measurement model to busf so that DeviceFinder knows which model to use if it needs to create new devices.

self.busf.model = self.config.freq_model

Next, because bus voltage is an algebraic variable, we use Derivative to calculate the finite difference to approximate its derivative.

self.dv = Derivative(self.v, tex_name='dV/dt', info='Finite difference of bus voltage')

Then, we retrieve the coefficient to convert power from machine base to system base using ConstService, given by Sb / Sn. This is needed for input mode 3, electric power in machine base.

self.SnSb = ExtService(model='SynGen', src='M', indexer=self.syn, attr='pu_coeff',
                       info='Machine base to sys base factor for power',
                       tex_name='(Sb/Sn)')

Note that the ExtService access the pu_coeff field of the M variables of synchronous generators. Since M is a machine-base power quantity, M.pu_coeff stores the multiplication coefficient to convert each of them from machine bases to the system base, which is Sb / Sn.

The input mode is parsed into boolean flags using Switcher:

self.SW = Switcher(u=self.MODE,
                   options=[0, 1, 2, 3, 4, 5, 6],
                   )

where the input u is the MODE parameter, and options is a list of accepted values. Switcher boolean arrays s0, s1, ..., sN, where N = len(options) - 1. We added 0 to options for padding so that SW_s1 corresponds to MODE 1. It improves the readability of the code as we will see next.

The input signal sig is an algebraic variable given by

self.sig = Algeb(tex_name='S_{ig}',
                 info='Input signal',
                 )

self.sig.v_str = 'SW_s1*(omega-1) + SW_s2*0 + SW_s3*(tm0/SnSb) + ' \
                 'SW_s4*(tm-tm0) + SW_s5*v + SW_s6*0'

self.sig.e_str = 'SW_s1*(omega-1) + SW_s2*(f-1) + SW_s3*(te/SnSb) + ' \
                 'SW_s4*(tm-tm0) + SW_s5*v + SW_s6*dv_v - sig'

The v_str and e_str are separated from the constructor to improve readability. They construct piece-wise functions to select the correct initial values and equations based on mode. For any variables in v_str, they must be defined before sig so that they will be initialized ahead of sig. Clearly, omega, tm, and v are defined in PSSBase and thus come before sig.

The following comes the most effective part: modeling using transfer function blocks. We utilized several blocks to describe the model from the diagram. Note that the output of a block is always the block name followed by _y. For example, the input of F2 is the output of F1, given by F1_y.

self.F1 = Lag2ndOrd(u=self.sig, K=1, T1=self.A1, T2=self.A2)

self.F2 = LeadLag2ndOrd(u=self.F1_y, T1=self.A3, T2=self.A4,
                        T3=self.A5, T4=self.A6, zero_out=True)

self.LL1 = LeadLag(u=self.F2_y, T1=self.T1, T2=self.T2, zero_out=True)

self.LL2 = LeadLag(u=self.LL1_y, T1=self.T3, T2=self.T4, zero_out=True)

self.Vks = Gain(u=self.LL2_y, K=self.KS)

self.WO = WashoutOrLag(u=self.Vks_y, T=self.T6, K=self.T5, name='WO',
                       zero_out=True)  # WO_y == Vss

self.VLIM = Limiter(u=self.WO_y, lower=self.LSMIN, upper=self.LSMAX,
                    info='Vss limiter')

self.Vss = Algeb(tex_name='V_{ss}', info='Voltage output before output limiter',
                 e_str='VLIM_zi * WO_y + VLIM_zu * LSMAX + VLIM_zl * LSMIN - Vss')

self.OLIM = Limiter(u=self.v, lower=self.VCLr, upper=self.VCUr,
                    info='output limiter')

self.vsout.e_str = 'OLIM_zi * Vss - vsout'

In the end, the output equation is assigned to vsout.e_str. It completes the equations of the IEEEST model.

Finalize#

Assemble IEEESTData and IEEESTModel into IEEEST:

class IEEEST(IEEESTData, IEEESTModel):
    def __init__(self, system, config):
        IEEESTData.__init__(self)
        IEEESTModel.__init__(self, system, config)

Locate and edit the file andes/models/__init__.py. The variable file_classes is a list of tuples that stores the source file and class names in strings. In each tuple, the first element is the .py file name in the models folder, and the second element is a list of class names to be imported from that file.

Find the line with pss, then add IEEEST to the corresponding list of model names. The line will look like

file_classes = list([
    ...
    ('pss', ['IEEEST', 'ST2CUT']),
    ...
])

Note in the above that the string 'IEEEST' is used. The line above is valid as long as from andes.models.pss import IEEEST is valid. If the source file name does not exist in any line of file_classes, one may add it after all prerequisite models. For example, the pss line should be added after exciters (and generators, of course).

Finally, locate andes/models/group.py, check if the class PSS exist. It needs to match the group name of IEEEST. If not, create one by inheriting from GroupBase:

class PSS(GroupBase):
    """Power system stabilizer group."""

    def __init__(self):
        super().__init__()
        self.common_vars.extend(('vsout',))

where we added vsout to the common_vars list. All models in the PSS group must have a variable named vsout, which is defined in PSSBase.

This completes the IEEEST model. When developing new models, use andes prepare to generate numerical code and start debugging.