Modeling Cookbook

This chapter contains advanced topics on modeling and simulation and how they are implemented in ANDES. It aims to provide an in-depth explanation of how the ANDES framework is set up for symbolic modeling and numerical simulation. It also provides an example for interested users to implement customized DAE models.

System

Overview

System is the top-level class for organizing power system models and orchestrating calculations.

class andes.system.System(case: Optional[str] = None, name: Optional[str] = None, config: Optional[Dict[KT, VT]] = None, config_path: Optional[str] = None, default_config: Optional[bool] = False, options: Optional[Dict[KT, VT]] = None, **kwargs)[source]

System contains models and routines for modeling and simulation.

System contains a several special OrderedDict member attributes for housekeeping. These attributes include models, groups, routines and calls for loaded models, groups, analysis routines, and generated numerical function calls, respectively.

Notes

System stores model and routine instances as attributes. Model and routine attribute names are the same as their class names. For example, Bus is stored at system.Bus, the power flow calculation routine is at system.PFlow, and the numerical DAE instance is at system.dae. See attributes for the list of attributes.

Attributes:
dae : andes.variables.dae.DAE

Numerical DAE storage

files : andes.variables.fileman.FileMan

File path storage

config : andes.core.Config

System config storage

models : OrderedDict

model name and instance pairs

groups : OrderedDict

group name and instance pairs

routines : OrderedDict

routine name and instance pairs

Note

andes.System is an alias of andes.system.System.

Dynamic Imports

System dynamically imports groups, models, and routines at creation. To add new models, groups or routines, edit the corresponding file by adding entries following examples.

andes.system.System.import_models(self)

Import and instantiate models as System member attributes.

Models defined in models/__init__.py will be instantiated sequentially as attributes with the same name as the class name. In addition, all models will be stored in dictionary System.models with model names as keys and the corresponding instances as values.

Examples

system.Bus stores the Bus object, and system.GENCLS stores the classical generator object,

system.models['Bus'] points the same instance as system.Bus.

andes.system.System.import_groups(self)

Import all groups classes defined in devices/group.py.

Groups will be stored as instances with the name as class names. All groups will be stored to dictionary System.groups.

andes.system.System.import_routines(self)

Import routines as defined in routines/__init__.py.

Routines will be stored as instances with the name as class names. All groups will be stored to dictionary System.groups.

Examples

System.PFlow is the power flow routine instance, and System.TDS and System.EIG are time-domain analysis and eigenvalue analysis routines, respectively.

Code Generation

Under the hood, all symbolically defined equations need to be generated into anonymous function calls for accelerating numerical simulations. This process is automatically invoked for the first time ANDES is run command line. It takes several seconds up to a minute to finish the generation.

Note

Code generation has been done if one has executed andes, andes selftest, or andes prepare.

Warning

When models are modified (such as adding new models or changing equation strings), code generation needs to be executed again for consistency. It can be more conveniently triggered from command line with andes prepare -i.

andes.system.System.prepare(self, quick=False, incremental=False, models=None, nomp=False, ncpu=2)

Generate numerical functions from symbolically defined models.

All procedures in this function must be independent of test case.

Parameters:
quick : bool, optional

True to skip pretty-print generation to reduce code generation time.

incremental : bool, optional

True to generate only for modified models, incrementally.

models : list, OrderedDict, None

List or OrderedList of models to prepare

nomp : bool

True to disable multiprocessing

Warning

Generated lambda functions will be serialized to file, but pretty prints (SymPy objects) can only exist in the System instance on which prepare is called.

Notes

Option incremental compares the md5 checksum of all var and service strings, and only regenerate for updated models.

Examples

If one needs to print out LaTeX-formatted equations in a Jupyter Notebook, one need to generate such equations with

import andes
sys = andes.prepare()

Alternatively, one can explicitly create a System and generate the code

import andes
sys = andes.System()
sys.prepare()

Since the process is slow, generated numerical functions (Python Callable) will be serialized into a file for future speed up. The package used for serializing/de-serializing numerical calls is dill. System has a function called dill for serializing using the dill package.

andes.system.System.dill(self)

Serialize generated numerical functions in System.calls with package dill.

The serialized file will be stored to ~/.andes/calls.pkl, where ~ is the home directory path.

Notes

This function sets dill.settings['recurse'] = True to serialize the function calls recursively.

andes.system.System.undill(self)

Deserialize the function calls from ~/.andes/calls.pkl with dill.

If no change is made to models, future calls to prepare() can be replaced with undill() for acceleration.

DAE Storage

System.dae is an instance of the numerical DAE class.

andes.variables.dae.DAE(system)[source]

Class for storing numerical values of the DAE system, including variables, equations and first order derivatives (Jacobian matrices).

Variable values and equation values are stored as numpy.ndarray, while Jacobians are stored as kvxopt.spmatrix. The defined arrays and descriptions are as follows:

DAE Array Description
x Array for state variable values
y Array for algebraic variable values
z Array for 0/1 limiter states (if enabled)
f Array for differential equation derivatives
Tf Left-hand side time constant array for f
g Array for algebraic equation mismatches

The defined scalar member attributes to store array sizes are

Scalar Description
m The number of algebraic variables/equations
n The number of algebraic variables/equations
o The number of limiter state flags

The derivatives of f and g with respect to x and y are stored in four kvxopt.spmatrix sparse matrices: fx, fy, gx, and gy, where the first letter is the equation name, and the second letter is the variable name.

Notes

DAE in ANDES is defined in the form of

\[\begin{split}T \dot{x} = f(x, y) \\ 0 = g(x, y)\end{split}\]

DAE does not keep track of the association of variable and address. Only a variable instance keeps track of its addresses.

Model and DAE Values

ANDES uses a decentralized architecture between models and DAE value arrays. In this architecture, variables are initialized and equations are evaluated inside each model. Then, System provides methods for collecting initial values and equation values into DAE, as well as copying solved values to each model.

The collection of values from models needs to follow protocols to avoid conflicts. Details are given in the subsection Variables.

andes.system.System.vars_to_dae(self, model)

Copy variables values from models to System.dae.

This function clears DAE.x and DAE.y and collects values from models.

andes.system.System.vars_to_models(self)

Copy variable values from System.dae to models.

andes.system.System._e_to_dae(self, eq_name: Union[str, Tuple] = ('f', 'g'))

Helper function for collecting equation values into System.dae.f and System.dae.g.

Parameters:
eq_name : 'x' or 'y' or tuple

Equation type name

Matrix Sparsity Patterns

The largest overhead in building and solving nonlinear equations is the building of Jacobian matrices. This is especially relevant when we use the implicit integration approach which algebraized the differential equations. Given the unique data structure of power system models, the sparse matrices for Jacobians are built incrementally, model after model.

There are two common approaches to incrementally build a sparse matrix. The first one is to use simple in-place add on sparse matrices, such as doing

self.fx += spmatrix(v, i, j, (n, n), 'd')

Although the implementation is simple, it involves creating and discarding temporary objects on the right hand side and, even worse, changing the sparse pattern of self.fx.

The second approach is to store the rows, columns and values in an array-like object and construct the Jacobians at the end. This approach is very efficient but has one caveat: it does not allow accessing the sparse matrix while building.

ANDES uses a pre-allocation approach to avoid the change of sparse patterns by filling values into a known the sparse matrix pattern matrix. System collects the indices of rows and columns for each Jacobian matrix. Before in-place additions, ANDES builds a temporary zero-filled spmatrix, to which the actual Jacobian values are written later. Since these in-place add operations are only modifying existing values, it does not change the pattern and thus avoids memory copying. In addition, updating sparse matrices can be done with the exact same code as the first approach.

Still, this approach creates and discards temporary objects. It is however feasible to write a C function which takes three array-likes and modify the sparse matrices in place. This is feature to be developed, and our prototype shows a promising acceleration up to 50%.

andes.system.System.store_sparse_pattern(self, models: collections.OrderedDict)

Collect and store the sparsity pattern of Jacobian matrices.

This is a runtime function specific to cases.

Notes

For gy matrix, always make sure the diagonal is reserved. It is a safeguard if the modeling user omitted the diagonal term in the equations.

Calling Model Methods

System is an orchestrator for calling shared methods of models. These API methods are defined for initialization, equation update, Jacobian update, and discrete flags update.

The following methods take an argument models, which should be an OrderedDict of models with names as keys and instances as values.

andes.system.System.init(self, models: collections.OrderedDict, routine: str)

Initialize the variables for each of the specified models.

For each model, the initialization procedure is:

  • Get values for all ExtService.
  • Call the model init() method, which initializes internal variables.
  • Copy variables to DAE and then back to the model.
andes.system.System.e_clear(self, models: collections.OrderedDict)

Clear equation arrays in DAE and model variables.

This step must be called before calling f_update or g_update to flush existing values.

andes.system.System.l_update_var(self, models: collections.OrderedDict, niter=None, err=None)

Update variable-based limiter discrete states by calling l_update_var of models.

This function is must be called before any equation evaluation.

andes.system.System.f_update(self, models: collections.OrderedDict)

Call the differential equation update method for models in sequence.

Notes

Updated equation values remain in models and have not been collected into DAE at the end of this step.

andes.system.System.l_update_eq(self, models: collections.OrderedDict)

Update equation-dependent limiter discrete components by calling l_check_eq of models. Force set equations after evaluating equations.

This function is must be called after differential equation updates.

andes.system.System.g_update(self, models: collections.OrderedDict)

Call the algebraic equation update method for models in sequence.

Notes

Like f_update, updated values have not collected into DAE at the end of the step.

andes.system.System.j_update(self, models: collections.OrderedDict, info=None)

Call the Jacobian update method for models in sequence.

The procedure is - Restore the sparsity pattern with andes.variables.dae.DAE.restore_sparse() - For each sparse matrix in (fx, fy, gx, gy), evaluate the Jacobian function calls and add values.

Notes

Updated Jacobians are immediately reflected in the DAE sparse matrices (fx, fy, gx, gy).

Configuration

System, models and routines have a member attribute config for model-specific or routine-specific configurations. System manages all configs, including saving to a config file and loading back.

andes.system.System.get_config(self)

Collect config data from models.

Returns:
dict

a dict containing the config from devices; class names are keys and configs in a dict are values.

andes.system.System.save_config(self, file_path=None, overwrite=False)

Save all system, model, and routine configurations to an rc-formatted file.

Parameters:
file_path : str, optional

path to the configuration file default to ~/andes/andes.rc.

overwrite : bool, optional

If file exists, True to overwrite without confirmation. Otherwise prompt for confirmation.

Warning

Saved config is loaded back and populated at system instance creation time. Configs from the config file takes precedence over default config values.

andes.system.System.load_config(conf_path=None)

Load config from an rc-formatted file.

Parameters:
conf_path : None or str

Path to the config file. If is None, the function body will not run.

Returns:
configparse.ConfigParser

Warning

It is important to note that configs from files is passed to model constructors during instantiation. If one needs to modify config for a run, it needs to be done before instantiating System, or before running andes from command line. Directly modifying Model.config may not take effect or have side effect as for the current implementation.

Group

A group is a collection of similar functional models with common variables and parameters. It is mandatory to enforce the common variables and parameters when develop new models. The common variables and parameters are typically the interface when connecting different group models.

For example, the Group RenGen has variables Pe and Qe, which are active power output and reactive power output. Such common variables can be retrieved by other models, such as one in the Group RenExciter for further calculation.

In such a way, the same variable interface is realized so that all model in the same group could carry out similar function.

Models

This section introduces the modeling of power system devices. The terminology "model" is used to describe the mathematical representation of a type of device, such as synchronous generators or turbine governors. The terminology "device" is used to describe a particular instance of a model, for example, a specific generator.

To define a model in ANDES, two classes, ModelData and Model need to be utilized. Class ModelData is used for defining parameters that will be provided from input files. It provides API for adding data from devices and managing the data. Class Model is used for defining other non-input parameters, service variables, and DAE variables. It provides API for converting symbolic equations, storing Jacobian patterns, and updating equations.

Model Data

class andes.core.model.ModelData(*args, three_params=True, **kwargs)[source]

Class for holding parameter data for a model.

This class is designed to hold the parameter data separately from model equations. Models should inherit this class to define the parameters from input files.

Inherit this class to create the specific class for holding input parameters for a new model. The recommended name for the derived class is the model name with Data. For example, data for GENCLS should be named GENCLSData.

Parameters should be defined in the __init__ function of the derived class.

Refer to andes.core.param for available parameter types.

Notes

Three default parameters are pre-defined in ModelData and will be inherited by all models. They are

In rare cases one does not want to define these three parameters, one can pass three_params=True to the constructor of ModelData.

Examples

If we want to build a class PQData (for static PQ load) with three parameters, Vn, p0 and q0, we can use the following

from andes.core.model import ModelData, Model
from andes.core.param import IdxParam, NumParam

class PQData(ModelData):
    super().__init__()
    self.Vn = NumParam(default=110,
                       info="AC voltage rating",
                       unit='kV', non_zero=True,
                       tex_name=r'V_n')
    self.p0 = NumParam(default=0,
                       info='active power load in system base',
                       tex_name=r'p_0', unit='p.u.')
    self.q0 = NumParam(default=0,
                       info='reactive power load in system base',
                       tex_name=r'q_0', unit='p.u.')

In this example, all the three parameters are defined as andes.core.param.NumParam. In the full PQData class, other types of parameters also exist. For example, to store the idx of owner, PQData uses

self.owner = IdxParam(model='Owner', info="owner idx")
Attributes:
cache

A cache instance for different views of the internal data.

flags : dict

Flags to control the routine and functions that get called. If the model is using user-defined numerical calls, set f_num, g_num and j_num properly.

Cache

ModelData uses a lightweight class andes.core.model.ModelCache for caching its data as a dictionary or a pandas DataFrame. Four attributes are defined in ModelData.cache:

  • dict: all data in a dictionary with the parameter names as keys and v values as arrays.
  • dict_in: the same as dict except that the values are from v_in, the original input.
  • df: all data in a pandas DataFrame.
  • df_in: the same as df except that the values are from v_in.

Other attributes can be added by registering with cache.add_callback.

andes.core.model.ModelCache.add_callback(self, name: str, callback)

Add a cache attribute and a callback function for updating the attribute.

Parameters:
name : str

name of the cached function return value

callback : callable

callback function for updating the cached attribute

Define Voltage Ratings

If a model is connected to an AC Bus or a DC Node, namely, if bus, bus1, node or node1 exists as parameter, it must provide the corresponding parameter, Vn, Vn1, Vdcn or Vdcn1, for rated voltages.

Controllers not connected to Bus or Node will have its rated voltages omitted and thus Vb = Vn = 1, unless one uses andes.core.param.ExtParam to retrieve the bus/node values.

As a rule of thumb, controllers not directly connected to the network shall use system-base per unit for voltage and current parameters. Controllers (such as a turbine governor) may inherit rated power from controlled models and thus power parameters will be converted consistently.

Define a DAE Model

class andes.core.model.Model(system=None, config=None)[source]

Base class for power system DAE models.

After subclassing ModelData, subclass Model` to complete a DAE model. Subclasses of Model defines DAE variables, services, and other types of parameters, in the constructor __init__.

Notes

To modify parameters or services use set(), which writes directly to the given attribute, or alter(), which converts parameters to system base like that for input data.

Examples

Take the static PQ as an example, the subclass of Model, PQ, should looks like

class PQ(PQData, Model):
    def __init__(self, system, config):
        PQData.__init__(self)
        Model.__init__(self, system, config)

Since PQ is calling the base class constructors, it is meant to be the final class and not further derived. It inherits from PQData and Model and must call constructors in the order of PQData and Model. If the derived class of Model needs to be further derived, it should only derive from Model and use a name ending with Base. See andes.models.synchronous.GENBASE.

Next, in PQ.__init__, set proper flags to indicate the routines in which the model will be used

self.flags.update({'pflow': True})

Currently, flags pflow and tds are supported. Both are False by default, meaning the model is neither used in power flow nor time-domain simulation. A very common pitfall is forgetting to set the flag.

Next, the group name can be provided. A group is a collection of models with common parameters and variables. Devices idx of all models in the same group must be unique. To provide a group name, use

self.group = 'StaticLoad'

The group name must be an existing class name in andes.models.group. The model will be added to the specified group and subject to the variable and parameter policy of the group. If not provided with a group class name, the model will be placed in the Undefined group.

Next, additional configuration flags can be added. Configuration flags for models are load-time variables specifying the behavior of a model. It can be exported to an andes.rc file and automatically loaded when creating the System. Configuration flags can be used in equation strings, as long as they are numerical values. To add config flags, use

self.config.add(OrderedDict((('pq2z', 1), )))

It is recommended to use OrderedDict instead of dict, although the syntax is verbose. Note that booleans should be provided as integers (1, or 0), since True or False is interpreted as a string when loaded from the rc file and will cause an error.

Next, it's time for variables and equations! The PQ class does not have internal variables itself. It uses its bus parameter to fetch the corresponding a and v variables of buses. Equation wise, it imposes an active power and a reactive power load equation.

To define external variables from Bus, use

self.a = ExtAlgeb(model='Bus', src='a',
                  indexer=self.bus, tex_name=r'\theta')
self.v = ExtAlgeb(model='Bus', src='v',
                  indexer=self.bus, tex_name=r'V')

Refer to the subsection Variables for more details.

The simplest PQ model will impose constant P and Q, coded as

self.a.e_str = "u * p"
self.v.e_str = "u * q"

where the e_str attribute is the equation string attribute. u is the connectivity status. Any parameter, config, service or variables can be used in equation strings.

Three additional scalars can be used in equations: - dae_t for the current simulation time can be used if the model has flag tds. - sys_f for system frequency (from system.config.freq). - sys_mva for system base mva (from system.config.mva).

The above example is overly simplified. Our PQ model wants a feature to switch itself to a constant impedance if the voltage is out of the range (vmin, vmax). To implement this, we need to introduce a discrete component called Limiter, which yields three arrays of binary flags, zi, zl, and zu indicating in range, below lower limit, and above upper limit, respectively.

First, create an attribute vcmp as a Limiter instance

self.vcmp = Limiter(u=self.v, lower=self.vmin, upper=self.vmax,
                     enable=self.config.pq2z)

where self.config.pq2z is a flag to turn this feature on or off. After this line, we can use vcmp_zi, vcmp_zl, and vcmp_zu in other equation strings.

self.a.e_str = "u * (p0 * vcmp_zi + " \
               "p0 * vcmp_zl * (v ** 2 / vmin ** 2) + " \
               "p0 * vcmp_zu * (v ** 2 / vmax ** 2))"

self.v.e_str = "u * (q0 * vcmp_zi + " \
               "q0 * vcmp_zl * (v ** 2 / vmin ** 2) + "\
               "q0 * vcmp_zu * (v ** 2 / vmax ** 2))"

Note that PQ.a.e_str can use the three variables from vcmp even before defining PQ.vcmp, as long as PQ.vcmp is defined, because vcmp_zi is just a string literal in e_str.

The two equations above implements a piecewise power injection equation. It selects the original power demand if within range, and uses the calculated power when out of range.

Finally, to let ANDES pick up the model, the model name needs to be added to models/__init__.py. Follow the examples in the OrderedDict, where the key is the file name, and the value is the class name.

Attributes:
num_params : OrderedDict

{name: instance} of numerical parameters, including internal and external ones

Dynamicity Under the Hood

The magic for automatic creation of variables are all hidden in andes.core.model.Model.__setattr__(), and the code is incredible simple. It sets the name, tex_name, and owner model of the attribute instance and, more importantly, does the book keeping. In particular, when the attribute is a andes.core.block.Block subclass, __setattr__ captures the exported instances, recursively, and prepends the block name to exported ones. All these convenience owe to the dynamic feature of Python.

During the code generation phase, the symbols are created by checking the book-keeping attributes, such as states, algebs, and attributes in Model.cache.

In the numerical evaluation phase, Model provides a method, andes.core.model.get_inputs(), to collect the variable value arrays in a dictionary, which can be effortlessly passed as arguments to numerical functions.

Commonly Used Attributes in Models

The following Model attributes are commonly used for debugging. If the attribute is an OrderedDict, the keys are attribute names in str, and corresponding values are the instances.

  • params and params_ext, two OrderedDict for internal (both numerical and non-numerical) and external parameters, respectively.
  • num_params for numerical parameters, both internal and external.
  • states and algebs, two OrderedDict for state variables and algebraic variables, respectively.
  • states_ext and algebs_ext, two OrderedDict for external states and algebraics.
  • discrete, an OrderedDict for discrete components.
  • blocks, an OrderedDict for blocks.
  • services, an OrderedDict for services with v_str.
  • services_ext, an OrderedDict for externally retrieved services.

Attributes in Model.cache

Attributes in Model.cache are additional book-keeping structures for variables, parameters and services. The following attributes are defined.

  • all_vars: all the variables.
  • all_vars_names, a list of all variable names.
  • all_params, all parameters.
  • all_params_names, a list of all parameter names.
  • algebs_and_ext, an OrderedDict of internal and external algebraic variables.
  • states_and_ext, an OrderedDict of internal and external differential variables.
  • services_and_ext, an OrderedDict of internal and external service variables.
  • vars_int, an OrderedDict of all internal variables, states and then algebs.
  • vars_ext, an OrderedDict of all external variables, states and then algebs.

Equation Generation

Model.syms, an instance of SymProcessor, handles the symbolic to numeric generation when called. The equation generation is a multi-step process with symbol preparation, equation generation, Jacobian generation, initializer generation, and pretty print generation.

class andes.core.model.SymProcessor(parent)[source]

A helper class for symbolic processing and code generation.

Parameters:
parent : Model

The Model instance to process

Attributes:
xy : sympy.Matrix

variables pretty print in the order of State, ExtState, Algeb, ExtAlgeb

f : sympy.Matrix

differential equations pretty print

g : sympy.Matrix

algebraic equations pretty print

df : sympy.SparseMatrix

df /d (xy) pretty print

dg : sympy.SparseMatrix

dg /d (xy) pretty print

inputs_dict : OrderedDict

All possible symbols in equations, including variables, parameters, discrete flags, and config flags. It has the same variables as what get_inputs() returns.

vars_dict : OrderedDict

variable-only symbols, which are useful when getting the Jacobian matrices.

generate_init()[source]

Generate initialization equations.

generate_jacobians(diag_eps=1e-08)[source]

Generate Jacobians and store to corresponding triplets.

The internal indices of equations and variables are stored, alongside the lambda functions.

For example, dg/dy is a sparse matrix whose elements are (row, col, val), where row and col are the internal indices, and val is the numerical lambda function. They will be stored to

row -> self.calls._igy col -> self.calls._jgy val -> self.calls._vgy
generate_symbols()[source]

Generate symbols for symbolic equation generations.

This function should run before other generate equations.

Attributes:
inputs_dict : OrderedDict

name-symbol pair of all parameters, variables and configs

vars_dict : OrderedDict

name-symbol pair of all variables, in the order of (states_and_ext + algebs_and_ext)

Next, function generate_equation converts each DAE equation set to one numerical function calls and store it in Model.calls. The attributes for differential equation set and algebraic equation set are f and g. Differently, service variables will be generated one by one and store in an OrderedDict in Model.calls.s.

Jacobian Storage

Abstract Jacobian Storage

Using the .jacobian method on sympy.Matrix, the symbolic Jacobians can be easily obtained. The complexity lies in the storage of the Jacobian elements. Observed that the Jacobian equation generation happens before any system is loaded, thus only the variable indices in the variable array is available. For each non-zero item in each Jacobian matrix, ANDES stores the equation index, variable index, and the Jacobian value (either a constant number or a callable function returning an array).

Note that, again, a non-zero entry in a Jacobian matrix can be either a constant or an expression. For efficiency, constant numbers and lambdified callables are stored separately. Constant numbers, therefore, can be loaded into the sparse matrix pattern when a particular system is given.

Warning

Data structure for the Jacobian storage has changed. Pending documentation update. Please check andes.core.common.JacTriplet class for more details.

The triplets, the equation (row) index, variable (column) index, and values (constant numbers or callable) are stored in Model attributes with the name of _{i, j, v}{Jacobian Name}{c or None}, where {i, j, v} is a single character for row, column or value, {Jacobian Name} is a two-character Jacobian name chosen from fx, fy, gx, and gy, and {c or None} is either character c or no character, indicating whether it corresponds to the constants or non-constants in the Jacobian.

For example, the triplets for the constants in Jacobian gy are stored in _igyc, _jgyc, and _vgyc.

In terms of the non-constant entries in Jacobians, the callable functions are stored in the corresponding _v{Jacobian Name} array. Note the differences between, for example, _vgy an _vgyc: _vgy is a list of callables, while _vgyc is a list of constant numbers.

Concrete Jacobian Storage

When a specific system is loaded and the addresses are assigned to variables, the abstract Jacobian triplets, more specifically, the rows and columns, are replaced with the array of addresses. The new addresses and values will be stored in Model attributes with the names {i, j, v}{Jacobian Name}{c or None}. Note that there is no underscore for the concrete Jacobian triplets.

For example, if model PV has a list of variables [p, q, a, v] . The equation associated with p is - u * p0, and the equation associated with q is u * (v0 - v). Therefore, the derivative of equation v0 - v over v is -u. Note that u is unknown at generation time, thus the value is NOT a constant and should to go vgy.

The values in _igy, _jgy and _vgy contains, respectively, 1, 3, and a lambda function which returns -u.

When a specific system is loaded, for example, a 5-bus system, the addresses for the q and v are [11, 13, 15, and [5, 7, 9]. PV.igy and PV.jgy will thus query the corresponding address list based on PV._igy and PV._jgy and store [11, 13, 15, and [5, 7, 9].

Initialization

Value providers such as services and DAE variables need to be initialized. Services are initialized before any DAE variable. Both Services and DAE Variables are initialized sequentially in the order of declaration.

Each Service, in addition to the standard v_str for symbolic initialization, provides a v_numeric hook for specifying a custom function for initialization. Custom initialization functions for DAE variables, are lumped in a single function in Model.v_numeric.

ANDES has an experimental Newton-Krylov method based iterative initialization. All DAE variables with v_iter will be initialized using the iterative approach

Additional Numerical Equations

Addition numerical equations are allowed to complete the "hybrid symbolic-numeric" framework. Numerical function calls are useful when the model DAE is non-standard or hard to be generalized. Since the symbolic-to-numeric generation is an additional layer on top of the numerical simulation, it is fundamentally the same as user-provided numerical function calls.

ANDES provides the following hook functions in each Model subclass for custom numerical functions:

  • v_numeric: custom initialization function
  • s_numeric: custom service value function
  • g_numeric: custom algebraic equations; update the e of the corresponding variable.
  • f_numeric: custom differential equations; update the e of the corresponding variable.
  • j_numeric: custom Jacobian equations; the function should append to _i, _j and _v structures.

For most models, numerical function calls are unnecessary and not recommended as it increases code complexity. However, when the data structure or the DAE are difficult to generalize in the symbolic framework, the numerical equations can be used.

For interested readers, see the COI symbolic implementation which calculated the center-of-inertia speed of generators. The COI could have been implemented numerically with for loops instead of NumReduce, NumRepeat and external variables.

Atom Types

ANDES contains three types of atom classes for building DAE models. These types are parameter, variable and service.

Value Provider

Before addressing specific atom classes, the terminology v-provider, and e-provider are discussed. A value provider class (or v-provider for short) references any class with a member attribute named v, which should be a list or a 1-dimensional array of values. For example, all parameter classes are v-providers, since a parameter class should provide values for that parameter.

Note

In fact, all types of atom classes are v-providers, meaning that an instance of an atom class must contain values.

The values in the v attribute of a particular instance are values that will substitute the instance for computation. If in a model, one has a parameter

self.v0 = NumParam()
self.b = NumParam()

# where self.v0.v = np.array([1., 1.05, 1.1]
#   and self.b.v  = np.array([10., 10., 10.]

Later, this parameter is used in an equation, such as

self.v = ExtAlgeb(model='Bus', src='v',
                  indexer=self.bus,
                  e_str='v0 **2 * b')

While computing v0 ** 2 * b, v0 and b will be substituted with the values in self.v0.v and self.b.v.

Sharing this interface v allows interoperability among parameters and variables and services. In the above example, if one defines v0 as a ConstService instance, such as

self.v0 = ConstService(v_str='1.0')

Calculations will still work without modification.

Equation Provider

Similarly, an equation provider class (or e-provider) references any class with a member attribute named e, which should be a 1-dimensional array of values. The values in the e array are the results from the equation and will be summed to the numerical DAE at the addresses specified by the attribute a.

Note

Currently, only variables are e-provider types.

If a model has an external variable that links to Bus.v (voltage), such as

self.v = ExtAlgeb(model='Bus', src='v',
                  indexer=self.bus,
                  e_str='v0 **2 * b')

The addresses of the corresponding voltage variables will be retrieved into self.v.a, and the equation evaluation results will be stored in self.v.e

Parameters

Background

Parameter is a type of building atom for DAE models. Most parameters are read directly from an input file and passed to equation, and other parameters can be calculated from existing parameters.

The base class for parameters in ANDES is BaseParam, which defines interfaces for adding values and checking the number of values. BaseParam has its values stored in a plain list, the member attribute v. Subclasses such as NumParam stores values using a NumPy ndarray.

An overview of supported parameters is given below.

Subclasses Description
DataParam An alias of BaseParam. Can be used for any non-numerical parameters.
NumParam The numerical parameter type. Used for all parameters in equations
IdxParam The parameter type for storing idx into other models
ExtParam Externally defined parameter
TimerParam Parameter for storing the action time of events

Data Parameters

class andes.core.param.BaseParam(default: Union[float, str, int, None] = None, name: Optional[str] = None, tex_name: Optional[str] = None, info: Optional[str] = None, unit: Optional[str] = None, mandatory: bool = False, export: bool = True, iconvert: Optional[Callable] = None, oconvert: Optional[Callable] = None)[source]

The base parameter class.

This class provides the basic data structure and interfaces for all types of parameters. Parameters are from input files and in general constant once initialized.

Subclasses should overload the n() method for the total count of elements in the value array.

Parameters:
default : str or float, optional

The default value of this parameter if None is provided

name : str, optional

Parameter name. If not provided, it will be automatically set to the attribute name defined in the owner model.

tex_name : str, optional

LaTeX-formatted parameter name. If not provided, tex_name will be assigned the same as name.

info : str, optional

Descriptive information of parameter

mandatory : bool

True if this parameter is mandatory

export : bool

True if the parameter will be exported when dumping data into files. True for most parameters. False for BackRef.

Other Parameters:
 
iconvert : Callable

Converter to be applied to input data when a device is being added.

oconvert : callable

Converter to be applied to internal data when outputting.

Warning

The most distinct feature of BaseParam, DataParam and IdxParam is that values are stored in a list without conversion to array. BaseParam, DataParam or IdxParam are not allowed in equations.

Attributes:
v : list

A list holding all the values. The BaseParam class does not convert the v attribute into NumPy arrays.

property : dict

A dict containing the truth values of the model properties.

class andes.core.param.DataParam(default: Union[float, str, int, None] = None, name: Optional[str] = None, tex_name: Optional[str] = None, info: Optional[str] = None, unit: Optional[str] = None, mandatory: bool = False, export: bool = True, iconvert: Optional[Callable] = None, oconvert: Optional[Callable] = None)[source]

An alias of the BaseParam class.

This class is used for string parameters or non-computational numerical parameters. This class does not provide a to_array method. All input values will be stored in v as a list.

See also

andes.core.param.BaseParam
Base parameter class
class andes.core.param.IdxParam(default: Union[float, str, int, None] = None, name: Optional[str] = None, tex_name: Optional[str] = None, info: Optional[str] = None, unit: Optional[str] = None, mandatory: bool = False, unique: bool = False, export: bool = True, model: Optional[str] = None, iconvert: Optional[Callable] = None, oconvert: Optional[Callable] = None)[source]

An alias of BaseParam with an additional storage of the owner model name

This class is intended for storing idx into other models. It can be used in the future for data consistency check.

Notes

This will be useful when, for example, one connects two TGs to one SynGen.

Examples

A PQ model connected to Bus model will have the following code

class PQModel(...):
    def __init__(...):
        ...
        self.bus = IdxParam(model='Bus')

Numeric Parameters

class andes.core.param.NumParam(default: Union[float, str, Callable, None] = None, name: Optional[str] = None, tex_name: Optional[str] = None, info: Optional[str] = None, unit: Optional[str] = None, vrange: Union[List[T], Tuple, None] = None, vtype: Optional[Type[CT_co]] = <class 'float'>, iconvert: Optional[Callable] = None, oconvert: Optional[Callable] = None, non_zero: bool = False, non_positive: bool = False, non_negative: bool = False, mandatory: bool = False, power: bool = False, ipower: bool = False, voltage: bool = False, current: bool = False, z: bool = False, y: bool = False, r: bool = False, g: bool = False, dc_voltage: bool = False, dc_current: bool = False, export: bool = True)[source]

A computational numerical parameter.

Parameters defined using this class will have their v field converted to a NumPy array after adding.

The original input values will be copied to vin, and the system-base per-unit conversion coefficients (through multiplication) will be stored in pu_coeff.

Parameters:
default : str or float, optional

The default value of this parameter if no value is provided

name : str, optional

Name of this parameter. If not provided, name will be set to the attribute name of the owner model.

tex_name : str, optional

LaTeX-formatted parameter name. If not provided, tex_name will be assigned the same as name.

info : str, optional

A description of this parameter

mandatory : bool

True if this parameter is mandatory

unit : str, optional

Unit of the parameter

vrange : list, tuple, optional

Typical value range

vtype : type, optional

Type of the v field. The default is float.

Other Parameters:
 
Sn : str

Name of the parameter for the device base power.

Vn : str

Name of the parameter for the device base voltage.

non_zero : bool

True if this parameter must be non-zero. non_zero can be combined with non_positive or non_negative.

non_positive : bool

True if this parameter must be non-positive.

non_negative : bool

True if this parameter must be non-negative.

mandatory : bool

True if this parameter must not be None.

power : bool

True if this parameter is a power per-unit quantity under the device base.

iconvert : callable

Callable to convert input data from excel or others to the internal v field.

oconvert : callable

Callable to convert input data from internal type to a serializable type.

ipower : bool

True if this parameter is an inverse-power per-unit quantity under the device base.

voltage : bool

True if the parameter is a voltage pu quantity under the device base.

current : bool

True if the parameter is a current pu quantity under the device base.

z : bool

True if the parameter is an AC impedance pu quantity under the device base.

y : bool

True if the parameter is an AC admittance pu quantity under the device base.

r : bool

True if the parameter is a DC resistance pu quantity under the device base.

g : bool

True if the parameter is a DC conductance pu quantity under the device base.

dc_current : bool

True if the parameter is a DC current pu quantity under device base.

dc_voltage : bool

True if the parameter is a DC voltage pu quantity under device base.

External Parameters

class andes.core.param.ExtParam(model: str, src: str, indexer=None, vtype=<class 'float'>, allow_none=False, default=0.0, **kwargs)[source]

A parameter whose values are retrieved from an external model or group.

Parameters:
model : str

Name of the model or group providing the original parameter

src : str

The source parameter name

indexer : BaseParam

A parameter defined in the model defining this ExtParam instance. indexer.v should contain indices into model.src.v. If is None, the source parameter values will be fully copied. If model is a group name, the indexer cannot be None.

Attributes:
parent_model : Model

The parent model providing the original parameter.

Timer Parameter

class andes.core.param.TimerParam(callback: Optional[Callable] = None, default: Union[float, str, Callable, None] = None, name: Optional[str] = None, tex_name: Optional[str] = None, info: Optional[str] = None, unit: Optional[str] = None, non_zero: bool = False, mandatory: bool = False, export: bool = True)[source]

A parameter whose values are event occurrence times during the simulation.

The constructor takes an additional Callable self.callback for the action of the event. TimerParam has a default value of -1, meaning deactivated.

Examples

A connectivity status toggler class Toggler takes a parameter t for the toggle time. Inside Toggler.__init__, one would have

self.t = TimerParam()

The Toggler class also needs to define a method for togging the connectivity status

def _u_switch(self, is_time: np.ndarray):
    action = False
    for i in range(self.n):
        if is_time[i] and (self.u.v[i] == 1):
            instance = self.system.__dict__[self.model.v[i]]
            # get the original status and flip the value
            u0 = instance.get(src='u', attr='v', idx=self.dev.v[i])
            instance.set(src='u',
                         attr='v',
                         idx=self.dev.v[i],
                         value=1-u0)
            action = True
    return action

Finally, in Toggler.__init__, assign the function as the callback for self.t

self.t.callback = self._u_switch

Variables

DAE Variables, or variables for short, are unknowns to be solved using numerical or analytical methods. A variable stores values, equation values, and addresses in the DAE array. The base class for variables is BaseVar. In this subsection, BaseVar is used to represent any subclass of VarBase list in the table below.

Class Description
State A state variable and associated diff. equation \(\textbf{T} \dot{x} = \textbf{f}\)
Algeb An algebraic variable and an associated algebraic equation \(0 = \textbf{g}\)
ExtState An external state variable and part of the differential equation (uncommon)
ExtAlgeb An external algebraic variable and part of the algebraic equation

BaseVar has two types: the differential variable type State and the algebraic variable type Algeb. State variables are described by differential equations, whereas algebraic variables are described by algebraic equations. State variables can only change continuously, while algebraic variables can be discontinuous.

Based on the model the variable is defined, variables can be internal or external. Most variables are internal and only appear in equations in the same model. Some models have "public" variables that can be accessed by other models. For example, a Bus defines v for the voltage magnitude. Each device attached to a particular bus needs to access the value and impose the reactive power injection. It can be done with ExtAlgeb or ExtState, which links with an existing variable from a model or a group.

Variable, Equation and Address

Subclasses of BaseVar are value providers and equation providers. Each BaseVar has member attributes v and e for variable values and equation values, respectively. The initial value of v is set by the initialization routine, and the initial value of e is set to zero. In the process of power flow calculation or time domain simulation, v is not directly modifiable by models but rather updated after solving non-linear equations. e is updated by the models and summed up before solving equations.

Each BaseVar also stores addresses of this variable, for all devices, in its member attribute a. The addresses are 0-based indices into the numerical DAE array, f or g, based on the variable type.

For example, Bus has self.a = Algeb() as the voltage phase angle variable. For a 5-bus system, Bus.a.a stores the addresses of the a variable for all the five Bus devices. Conventionally, Bus.a.a will be assigned np.array([0, 1, 2, 3, 4]).

Value and Equation Strings

The most important feature of the symbolic framework is allowing to define equations using strings. There are three types of strings for a variable, stored in the following member attributes, respectively:

  • v_str: equation string for explicit initialization in the form of v = v_str(x, y).
  • v_iter: equation string for implicit initialization in the form of v_iter(x, y) = 0
  • e_str: equation string for (full or part of) the differential or algebraic equation.

The difference between v_str and v_iter should be clearly noted. v_str evaluates directly into the initial value, while all v_iter equations are solved numerically using the Newton-Krylov iterative method.

Values Between DAE and Models

ANDES adopts a decentralized architecture which provides each model a copy of variable values before equation evaluation. This architecture allows to parallelize the equation evaluation (in theory, or in practice if one works round the Python GIL). However, this architecture requires a coherent protocol for updating the DAE arrays and the BaseVar arrays. More specifically, how the variable and equations values from model VarBase should be summed up or forcefully set at the DAE arrays needs to be defined.

The protocol is relevant when a model defines subclasses of BaseVar that are supposed to be "public". Other models share this variable with ExtAlgeb or ExtState.

By default, all v and e at the same address are summed up. This is the most common case, such as a Bus connected by multiple devices: power injections from devices should be summed up.

In addition, BaseVar provides two flags, v_setter and e_setter, for cases when one VarBase needs to overwrite the variable or equation values.

Flags for Value Overwriting

BaseVar have special flags for handling value initialization and equation values. This is only relevant for public or external variables. The v_setter is used to indicate whether a particular BaseVar instance sets the initial value. The e_setter flag indicates whether the equation associated with a BaseVar sets the equation value.

The v_setter flag is checked when collecting data from models to the numerical DAE array. If v_setter is False, variable values of the same address will be added. If one of the variable or external variable has v_setter is True, it will, at the end, set the values in the DAE array to its value. Only one BaseVar of the same address is allowed to have v_setter == True.

A v_setter Example

A Bus is allowed to default the initial voltage magnitude to 1 and the voltage phase angle to 0. If a PV device is connected to a Bus device, the PV should be allowed to override the voltage initial value with the voltage set point.

In Bus.__init__(), one has

self.v = Algeb(v_str='1')

In PV.__init__, one can use

self.v0 = Param()
self.bus = IdxParam(model='Bus')

self.v = ExtAlgeb(src='v',
                  model='Bus',
                  indexer=self.bus,
                  v_str='v0',
                  v_setter=True)

where an ExtAlgeb is defined to access Bus.v using indexer self.bus. The v_str line sets the initial value to v0. In the variable initialization phase for PV, PV.v.v is set to v0.

During the value collection into DAE.y by the System class, PV.v, as a final v_setter, will overwrite the voltage magnitude for Bus devices with the indices provided in PV.bus.

class andes.core.var.BaseVar(name: Optional[str] = None, tex_name: Optional[str] = None, info: Optional[str] = None, unit: Optional[str] = None, v_str: Union[str, float, None] = None, v_iter: Optional[str] = None, e_str: Optional[str] = None, discrete: Optional[andes.core.discrete.Discrete] = None, v_setter: Optional[bool] = False, e_setter: Optional[bool] = False, v_str_add: Optional[bool] = False, addressable: Optional[bool] = True, export: Optional[bool] = True, diag_eps: Optional[float] = 0.0, deps: Optional[List[T]] = None)[source]

Base variable class.

Derived classes State and Algeb should be used to build model variables.

Parameters:
name : str, optional

Variable name

info : str, optional

Descriptive information

unit : str, optional

Unit

tex_name : str

LaTeX-formatted variable name. If is None, use name instead.

discrete : Discrete

Discrete component on which thi variable depends on. ANDES will call check_var() of the discrete component before initializing this variable.

Attributes:
a : array-like

variable address

v : array-like

local-storage of the variable value

e : array-like

local-storage of the corresponding equation value

e_str : str

the string/symbolic representation of the equation

v_str : str

explicit initialization equation

v_str_add : bool

True if the value of v_str will be added to the variable. Useful when other models access this variable and set part of the initial value

v_iter : str

implicit iterative equation in the form of 0 = v_iter

class andes.core.var.ExtVar(model: str, src: str, indexer: Union[List[T], numpy.ndarray, andes.core.param.BaseParam, andes.core.service.BaseService, None] = None, allow_none: Optional[bool] = False, name: Optional[str] = None, tex_name: Optional[str] = None, ename: Optional[str] = None, tex_ename: Optional[str] = None, info: Optional[str] = None, unit: Optional[str] = None, v_str: Union[str, float, None] = None, v_iter: Optional[str] = None, e_str: Optional[str] = None, v_setter: Optional[bool] = False, e_setter: Optional[bool] = False, addressable: Optional[bool] = True, export: Optional[bool] = True, diag_eps: Optional[float] = 0.0)[source]

Externally defined algebraic variable

This class is used to retrieve the addresses of externally- defined variable. The e value of the ExtVar will be added to the corresponding address in the DAE equation.

Parameters:
model : str

Name of the source model

src : str

Source variable name

indexer : BaseParam, BaseService

A parameter of the hosting model, used as indices into the source model and variable. If is None, the source variable address will be fully copied.

allow_none : bool

True to allow None in indexer

Attributes:
parent_model : Model

The parent model providing the original parameter.

uid : array-like

An array containing the absolute indices into the parent_instance values.

e_code : str

Equation code string; copied from the parent instance.

v_code : str

Variable code string; copied from the parent instance.

class andes.core.var.State(name: Optional[str] = None, tex_name: Optional[str] = None, info: Optional[str] = None, unit: Optional[str] = None, v_str: Union[str, float, None] = None, v_iter: Optional[str] = None, e_str: Optional[str] = None, discrete: Optional[andes.core.discrete.Discrete] = None, t_const: Union[andes.core.param.BaseParam, andes.core.common.DummyValue, andes.core.service.BaseService, None] = None, check_init: Optional[bool] = True, v_setter: Optional[bool] = False, e_setter: Optional[bool] = False, addressable: Optional[bool] = True, export: Optional[bool] = True, diag_eps: Optional[float] = 0.0, deps: Optional[List[T]] = None)[source]

Differential variable class, an alias of the BaseVar.

Parameters:
t_const : BaseParam, DummyValue

Left-hand time constant for the differential equation. Time constants will not be evaluated as part of the differential equation. They will be collected to array dae.Tf to multiply to the right-hand side dae.f.

check_init : bool

True to check if the equation right-hand-side is zero initially. Disabling the checking can be used for integrators when the initial input may not be zero.

Attributes:
e_code : str

Equation code string, equals string literal f

v_code : str

Variable code string, equals string literal x

class andes.core.var.Algeb(name: Optional[str] = None, tex_name: Optional[str] = None, info: Optional[str] = None, unit: Optional[str] = None, v_str: Union[str, float, None] = None, v_iter: Optional[str] = None, e_str: Optional[str] = None, discrete: Optional[andes.core.discrete.Discrete] = None, v_setter: Optional[bool] = False, e_setter: Optional[bool] = False, v_str_add: Optional[bool] = False, addressable: Optional[bool] = True, export: Optional[bool] = True, diag_eps: Optional[float] = 0.0, deps: Optional[List[T]] = None)[source]

Algebraic variable class, an alias of the BaseVar.

Attributes:
e_code : str

Equation code string, equals string literal g

v_code : str

Variable code string, equals string literal y

class andes.core.var.ExtState(model: str, src: str, indexer: Union[List[T], numpy.ndarray, andes.core.param.BaseParam, andes.core.service.BaseService, None] = None, allow_none: Optional[bool] = False, name: Optional[str] = None, tex_name: Optional[str] = None, ename: Optional[str] = None, tex_ename: Optional[str] = None, info: Optional[str] = None, unit: Optional[str] = None, v_str: Union[str, float, None] = None, v_iter: Optional[str] = None, e_str: Optional[str] = None, v_setter: Optional[bool] = False, e_setter: Optional[bool] = False, addressable: Optional[bool] = True, export: Optional[bool] = True, diag_eps: Optional[float] = 0.0)[source]

External state variable type.

Warning

ExtState is not allowed to set t_const, as it will conflict with the source State variable. In fact, one should not set e_str for ExtState.

class andes.core.var.ExtAlgeb(model: str, src: str, indexer: Union[List[T], numpy.ndarray, andes.core.param.BaseParam, andes.core.service.BaseService, None] = None, allow_none: Optional[bool] = False, name: Optional[str] = None, tex_name: Optional[str] = None, ename: Optional[str] = None, tex_ename: Optional[str] = None, info: Optional[str] = None, unit: Optional[str] = None, v_str: Union[str, float, None] = None, v_iter: Optional[str] = None, e_str: Optional[str] = None, v_setter: Optional[bool] = False, e_setter: Optional[bool] = False, addressable: Optional[bool] = True, export: Optional[bool] = True, diag_eps: Optional[float] = 0.0)[source]

External algebraic variable type.

class andes.core.var.AliasState(var, **kwargs)[source]

Alias state variable.

Refer to the docs of AliasAlgeb.

class andes.core.var.AliasAlgeb(var, **kwargs)[source]

Alias algebraic variable. Essentially ExtAlgeb that links to a a model's own variable.

AliasAlgeb is useful when the final output of a model is from a block, but the model must provide the final output in a pre-defined name. Using AliasAlgeb, A model can avoid adding an additional variable with a dummy equations.

Like ExtVar, labels of AliasAlgeb will not be saved in the final output. When plotting from file, one need to look up the original variable name.

Services

Services are helper variables outside the DAE variable list. Services are most often used for storing intermediate constants but can be used for special operations to work around restrictions in the symbolic framework. Services are value providers, meaning each service has an attribute v for storing service values. The base class of services is BaseService, and the supported services are listed as follows.

Class Description
ConstService Internal service for constant values.
VarService Variable service updated at each iteration before equations.
ExtService External service for retrieving values from value providers.
PostInitService Constant service evaluated after TDS initialization
NumReduce The service type for reducing linear 2-D arrays into 1-D arrays
NumRepeat The service type for repeating a 1-D array to linear 2-D arrays
IdxRepeat The service type for repeating a 1-D list to linear 2-D list
EventFlag Service type for flagging changes in inputs as an event
VarHold Hold input value when a hold signal is active
ExtendedEvent Extend an event signal for a given period of time
DataSelect Select optional str data if provided or use the fallback
NumSelect Select optional numerical data if provided
DeviceFinder Finds or creates devices linked to the given devices
BackRef Collects idx-es for the backward references
RefFlatten Converts BackRef list of lists into a 1-D list
InitChecker Checks initial values against typical values
FlagValue Flags values that equals the given value
Replace Replace values that returns True for the given lambda func

Internal Constants

The most commonly used service is ConstService. It is used to store an array of constants, whose value is evaluated from a provided symbolic string. They are only evaluated once in the model initialization phase, ahead of variable initialization. ConstService comes handy when one wants to calculate intermediate constants from parameters.

For example, a turbine governor has a NumParam R for the droop. ConstService allows to calculate the inverse of the droop, the gain, and use it in equations. The snippet from a turbine governor's __init__() may look like

self.R = NumParam()
self.G = ConstService(v_str='u/R')

where u is the online status parameter. The model can thus use G in subsequent variable or equation strings.

class andes.core.service.ConstService(v_str: Optional[str] = None, v_numeric: Optional[Callable] = None, vtype: Optional[type] = None, name: Optional[str] = None, tex_name=None, info=None)[source]

A type of Service that stays constant once initialized.

ConstService are usually constants calculated from parameters. They are only evaluated once in the initialization phase before variables are initialized. Therefore, uninitialized variables must not be used in v_str`.

Parameters:
name : str

Name of the ConstService

v_str : str

An equation string to calculate the variable value.

v_numeric : Callable, optional

A callable which returns the value of the ConstService

Attributes:
v : array-like or a scalar

ConstService value

class andes.core.service.VarService(v_str: Optional[str] = None, v_numeric: Optional[Callable] = None, vtype: Optional[type] = None, name: Optional[str] = None, tex_name=None, info=None)[source]

Variable service that gets updated in each step/loop as variables change.

This class is useful when one has non-differentiable algebraic equations, which make use of abs(), re and im. Instead of creating Algeb, one can put the equation in VarService, which will be updated before solving algebraic equations.

Warning

VarService is not solved with other algebraic equations, meaning that there is one step "delay" between the algebraic variables and VarService. Use an algebraic variable whenever possible.

Examples

In ESST3A model, the voltage and current sensors (vd + jvq), (Id + jIq) estimate the sensed VE using equation

\[VE = | K_{PC}*(v_d + 1j v_q) + 1j (K_I + K_{PC}*X_L)*(I_d + 1j I_q)|\]

One can use VarService to implement this equation

self.VE = VarService(
    tex_name='V_E',
    info='VE',
    v_str='Abs(KPC*(vd + 1j*vq) + 1j*(KI + KPC*XL)*(Id + 1j*Iq))',
    )
class andes.core.service.PostInitService(v_str: Optional[str] = None, v_numeric: Optional[Callable] = None, vtype: Optional[type] = None, name: Optional[str] = None, tex_name=None, info=None)[source]

Constant service that gets stored once after init.

This service is useful when one need to store initialization values stored in variables.

Examples

In ESST3A model, the vf variable is initialized followed by other variables. One can store the initial vf into vf0 so that equation vf - vf0 = 0 will hold.

self.vref0 = PostInitService(info='Initial reference voltage input',
                             tex_name='V_{ref0}',
                             v_str='vref',
                             )

Since all ConstService are evaluated before equation evaluation, without using PostInitService, one will need to create lots of ConstService to store values in the initialization path towards vf0, in order to correctly initialize vf.

External Constants

Service constants whose value is retrieved from an external model or group. Using ExtService is similar to using external variables. The values of ExtService will be retrieved once during the initialization phase before ConstService evaluation.

For example, a synchronous generator needs to retrieve the p and q values from static generators for initialization. ExtService is used for this purpose. In the __init__() of a synchronous generator model, one can define the following to retrieve StaticGen.p as p0:

self.p0 = ExtService(src='p',
                     model='StaticGen',
                     indexer=self.gen,
                     tex_name='P_0')
class andes.core.service.ExtService(model: str, src: str, indexer: Union[andes.core.param.BaseParam, andes.core.service.BaseService], attr: str = 'v', allow_none: bool = False, default=0, name: str = None, tex_name: str = None, vtype=None, info: str = None)[source]

Service constants whose value is from an external model or group.

Parameters:
src : str

Variable or parameter name in the source model or group

model : str

A model name or a group name

indexer : IdxParam or BaseParam

An "Indexer" instance whose v field contains the idx of devices in the model or group.

Examples

A synchronous generator needs to retrieve the p and q values from static generators for initialization. ExtService is used for this purpose.

In a synchronous generator, one can define the following to retrieve StaticGen.p as p0:

class GENCLSModel(Model):
    def __init__(...):
        ...
        self.p0 = ExtService(src='p',
                             model='StaticGen',
                             indexer=self.gen,
                             tex_name='P_0')

Shape Manipulators

This section is for advanced model developer.

All generated equations operate on 1-dimensional arrays and can use algebraic calculations only. In some cases, one model would use BackRef to retrieve 2-dimensional indices and will use such indices to retrieve variable addresses. The retrieved addresses usually has a different length of the referencing model and cannot be used directly for calculation. Shape manipulator services can be used in such case.

NumReduce is a helper Service type which reduces a linearly stored 2-D ExtParam into 1-D Service. NumRepeat is a helper Service type which repeats a 1-D value into linearly stored 2-D value based on the shape from a BackRef.

class andes.core.service.BackRef(**kwargs)[source]

A special type of reference collector.

BackRef is used for collecting device indices of other models referencing the parent model of the BackRef. The v``field will be a list of lists, each containing the `idx of other models referencing each device of the parent model.

BackRef can be passed as indexer for params and vars, or shape for NumReduce and NumRepeat. See examples for illustration.

See also

andes.core.service.NumReduce
A more complete example using BackRef to build the COI model

Examples

A Bus device has an IdxParam of area, storing the idx of area to which the bus device belongs. In Bus.__init__(), one has

self.area = IdxParam(model='Area')

Suppose Bus has the following data

idx area Vn
1 1 110
2 2 220
3 1 345
4 1 500

The Area model wants to collect the indices of Bus devices which points to the corresponding Area device. In Area.__init__, one defines

self.Bus = BackRef()

where the member attribute name Bus needs to match exactly model name that Area wants to collect idx for. Similarly, one can define self.ACTopology = BackRef() to collect devices in the ACTopology group that references Area.

The collection of idx happens in andes.system.System._collect_ref_param(). It has to be noted that the specific Area entry must exist to collect model idx-dx referencing it. For example, if Area has the following data

idx
1

Then, only Bus 1, 3, and 4 will be collected into self.Bus.v, namely, self.Bus.v == [ [1, 3, 4] ].

If Area has data

idx
1
2

Then, self.Bus.v will end up with [ [1, 3, 4], [2] ].

class andes.core.service.NumReduce(u, ref: andes.core.service.BackRef, fun: Callable, name=None, tex_name=None, info=None, cache=True)[source]

A helper Service type which reduces a linearly stored 2-D ExtParam into 1-D Service.

NumReduce works with ExtParam whose v field is a list of lists. A reduce function which takes an array-like and returns a scalar need to be supplied. NumReduce calls the reduce function on each of the lists and return all the scalars in an array.

Parameters:
u : ExtParam

Input ExtParam whose v contains linearly stored 2-dimensional values

ref : BackRef

The BackRef whose 2-dimensional shapes are used for indexing

fun : Callable

The callable for converting a 1-D array-like to a scalar

Examples

Suppose one wants to calculate the mean value of the Vn in one Area. In the Area class, one defines

class AreaModel(...):
    def __init__(...):
        ...
        # backward reference from `Bus`
        self.Bus = BackRef()

        # collect the Vn in an 1-D array
        self.Vn = ExtParam(model='Bus',
            src='Vn',
            indexer=self.Bus)

        self.Vn_mean = NumReduce(u=self.Vn,
            fun=np.mean,
            ref=self.Bus)

Suppose we define two areas, 1 and 2, the Bus data looks like

idx area Vn
1 1 110
2 2 220
3 1 345
4 1 500

Then, self.Bus.v is a list of two lists [ [1, 3, 4], [2] ]. self.Vn.v will be retrieved and linearly stored as [110, 345, 500, 220]. Based on the shape from self.Bus, numpy.mean() will be called on [110, 345, 500] and [220] respectively. Thus, self.Vn_mean.v will become [318.33, 220].

class andes.core.service.NumRepeat(u, ref, **kwargs)[source]

A helper Service type which repeats a v-provider's value based on the shape from a BackRef

Examples

NumRepeat was originally designed for computing the inertia-weighted average rotor speed (center of inertia speed). COI speed is computed with

\[\omega_{COI} = \frac{ \sum{M_i * \omega_i} } {\sum{M_i}}\]

The numerator can be calculated with a mix of BackRef, ExtParam and ExtState. The denominator needs to be calculated with NumReduce and Service Repeat. That is, use NumReduce to calculate the sum, and use NumRepeat to repeat the summed value for each device.

In the COI class, one would have

class COIModel(...):
    def __init__(...):
        ...
        self.SynGen = BackRef()
        self.SynGenIdx = RefFlatten(ref=self.SynGen)
        self.M = ExtParam(model='SynGen',
                          src='M',
                          indexer=self.SynGenIdx)

        self.wgen = ExtState(model='SynGen',
                             src='omega',
                             indexer=self.SynGenIdx)

        self.Mt = NumReduce(u=self.M,
                                 fun=np.sum,
                                 ref=self.SynGen)

        self.Mtr = NumRepeat(u=self.Mt,
                               ref=self.SynGen)

        self.pidx = IdxRepeat(u=self.idx,ref=self.SynGen)

Finally, one would define the center of inertia speed as

self.wcoi = Algeb(v_str='1', e_str='-wcoi')

self.wcoi_sub = ExtAlgeb(model='COI',
                         src='wcoi',
                         e_str='M * wgen / Mtr',
                         v_str='M / Mtr',
                         indexer=self.pidx,
                         )

It is very worth noting that the implementation uses a trick to separate the average weighted sum into n sub-equations, each calculating the \((M_i * \omega_i) / (\sum{M_i})\). Since all the variables are preserved in the sub-equation, the derivatives can be calculated correctly.

class andes.core.service.IdxRepeat(u, ref, **kwargs)[source]

Helper class to repeat IdxParam.

This class has the same functionality as andes.core.service.NumRepeat but only operates on IdxParam, DataParam or NumParam.

class andes.core.service.RefFlatten(ref, **kwargs)[source]

A service type for flattening andes.core.service.BackRef into a 1-D list.

Examples

This class is used when one wants to pass BackRef values as indexer.

andes.models.coi.COI collects referencing andes.models.group.SynGen with

self.SynGen = BackRef(info='SynGen idx lists', export=False)

After collecting BackRefs, self.SynGen.v will become a two-level list of indices, where the first level correspond to each COI and the second level correspond to generators of the COI.

Convert self.SynGen into 1-d as self.SynGenIdx, which can be passed as indexer for retrieving other parameters and variables

self.SynGenIdx = RefFlatten(ref=self.SynGen)

self.M = ExtParam(model='SynGen', src='M',
                  indexer=self.SynGenIdx, export=False,
                  )

Value Manipulation

class andes.core.service.Replace(old_val, flt, new_val, name=None, tex_name=None, info=None, cache=True)[source]

Replace parameters with new values if the function returns True

class andes.core.service.FlagValue(u, value, flag=0, name=None, tex_name=None, info=None, cache=True)[source]

Class for flagging values that equal to the given value.

By default, values that equal to value will be flagged as 0. Non-matching values will be flagged as 1.

Parameters:
u

Input parameter

value

Value to flag. Can be None, string, or a number.

flag : 0 by default, only 0 or 1 is accepted.

The flag for the matched ones

Warning

FlagNotNone can only be applied to BaseParam with cache=True. Applying to Service will fail unless cache is False (at a performance cost).

Idx and References

class andes.core.service.DeviceFinder(u, link, idx_name, name=None, tex_name=None, info=None)[source]

Service for finding indices of optionally linked devices.

If not provided, DeviceFinder will add devices at the beginning of System.setup.

Examples

IEEEST stabilizer takes an optional busf (IdxParam) for specifying the connected BusFreq, which is needed for mode 6. To avoid reimplementing BusFreq within IEEEST, one can do

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

where self.busf is the optional input, self.buss is the bus indices that busf should measure, and idx_name is the name of a BusFreq parameter through which the measured bus indices are specified. For each None values in self.busf, a BusFreq is created to measure the corresponding bus in self.buss.

That is, BusFreq.[idx_name].v = [link]. DeviceFinder will find / create BusFreq devices so that the returned list of BusFreq indices are connected to self.buss, respectively.

class andes.core.service.BackRef(**kwargs)[source]

A special type of reference collector.

BackRef is used for collecting device indices of other models referencing the parent model of the BackRef. The v``field will be a list of lists, each containing the `idx of other models referencing each device of the parent model.

BackRef can be passed as indexer for params and vars, or shape for NumReduce and NumRepeat. See examples for illustration.

See also

andes.core.service.NumReduce
A more complete example using BackRef to build the COI model

Examples

A Bus device has an IdxParam of area, storing the idx of area to which the bus device belongs. In Bus.__init__(), one has

self.area = IdxParam(model='Area')

Suppose Bus has the following data

idx area Vn
1 1 110
2 2 220
3 1 345
4 1 500

The Area model wants to collect the indices of Bus devices which points to the corresponding Area device. In Area.__init__, one defines

self.Bus = BackRef()

where the member attribute name Bus needs to match exactly model name that Area wants to collect idx for. Similarly, one can define self.ACTopology = BackRef() to collect devices in the ACTopology group that references Area.

The collection of idx happens in andes.system.System._collect_ref_param(). It has to be noted that the specific Area entry must exist to collect model idx-dx referencing it. For example, if Area has the following data

idx
1

Then, only Bus 1, 3, and 4 will be collected into self.Bus.v, namely, self.Bus.v == [ [1, 3, 4] ].

If Area has data

idx
1
2

Then, self.Bus.v will end up with [ [1, 3, 4], [2] ].

class andes.core.service.RefFlatten(ref, **kwargs)[source]

A service type for flattening andes.core.service.BackRef into a 1-D list.

Examples

This class is used when one wants to pass BackRef values as indexer.

andes.models.coi.COI collects referencing andes.models.group.SynGen with

self.SynGen = BackRef(info='SynGen idx lists', export=False)

After collecting BackRefs, self.SynGen.v will become a two-level list of indices, where the first level correspond to each COI and the second level correspond to generators of the COI.

Convert self.SynGen into 1-d as self.SynGenIdx, which can be passed as indexer for retrieving other parameters and variables

self.SynGenIdx = RefFlatten(ref=self.SynGen)

self.M = ExtParam(model='SynGen', src='M',
                  indexer=self.SynGenIdx, export=False,
                  )

Events

class andes.core.service.EventFlag(u, vtype: Optional[type] = None, name: Optional[str] = None, tex_name=None, info=None)[source]

Service to flag events when the input value changes. The typical input is a v-provider with binary values.

Implemented by providing self.check(**kwargs) as v_numeric. EventFlag.v stores the values of the input variable in the most recent iteration/step.

After the evaluation of self.check(), self.v will be updated.

class andes.core.service.ExtendedEvent(u, t_ext: Union[int, float, andes.core.param.BaseParam, andes.core.service.BaseService] = 0.0, trig: str = 'rise', enable=True, v_disabled=0, extend_only=False, vtype: Optional[type] = None, name: Optional[str] = None, tex_name=None, info=None)[source]

Service for indicating an event for an extended, predefined period of time following the event disappearance.

The triggering of an event, whether the rise or fall edge, is specified through trig. For example, if trig = rise, the change of the input from 0 to 1 will be considered as an input, whereas the subsequent change back to 0 will be considered as the event end.

ExtendedEvent.v stores the flags whether the extended time has completed. Outputs will become 1 once the event starts and return to 0 when the extended time ends.

Parameters:
u : v-provider

Triggering signal where the values are 0 or 1.

trig : str in ("rise", "fall")

Triggering edge for the beginning of an event. rise by default.

enable : bool or v-provider

If disabled, the output will be v_disabled

extend_only : bool

Only output during the extended period, not the event period.

Warning

The performance of this class needs to be optimized.

Data Select

class andes.core.service.DataSelect(optional, fallback, name: Optional[str] = None, tex_name: Optional[str] = None, info: Optional[str] = None)[source]

Class for selecting values for optional DataParam or NumParam.

This service is a v-provider that uses optional DataParam if available with a fallback.

DataParam will be tested for None, and NumParam will be tested with np.isnan().

Notes

An use case of DataSelect is remote bus. One can do

self.buss = DataSelect(option=self.busr, fallback=self.bus)

Then, pass self.buss instead of self.bus as indexer to retrieve voltages.

Another use case is to allow an optional turbine rating. One can do

self.Tn = NumParam(default=None)
self.Sg = ExtParam(...)
self.Sn = DataSelect(Tn, Sg)
class andes.core.service.NumSelect(optional, fallback, name: Optional[str] = None, tex_name: Optional[str] = None, info: Optional[str] = None)[source]

Class for selecting values for optional NumParam.

NumSelect works with internal and external parameters.

Notes

One use case is to allow an optional turbine rating. One can do

self.Tn = NumParam(default=None)
self.Sg = ExtParam(...)
self.Sn = DataSelect(Tn, Sg)

Miscellaneous

class andes.core.service.InitChecker(u, lower=None, upper=None, equal=None, not_equal=None, enable=True, error_out=False, **kwargs)[source]

Class for checking init values against known typical values.

Instances will be stored in Model.services_post and Model.services_icheck, which will be checked in Model.post_init_check() after initialization.

Parameters:
u

v-provider to be checked

lower : float, BaseParam, BaseVar, BaseService

lower bound

upper : float, BaseParam, BaseVar, BaseService

upper bound

equal : float, BaseParam, BaseVar, BaseService

values that the value from v_str should equal

not_equal : float, BaseParam, BaseVar, BaseService

values that should not equal

enable : bool

True to enable checking

Examples

Let's say generator excitation voltages are known to be in the range of 1.6 - 3.0 per unit. One can add the following instance to GENBase

self._vfc = InitChecker(u=self.vf,
                        info='vf range',
                        lower=1.8,
                        upper=3.0,
                        )

lower and upper can also take v-providers instead of float values.

One can also pass float values from Config to make it adjustable as in our implementation of GENBase._vfc.

Discrete

Background

The discrete component library contains a special type of block for modeling the discontinuity in power system devices. Such continuities can be device-level physical constraints or algorithmic limits imposed on controllers.

The base class for discrete components is andes.core.discrete.Discrete.

class andes.core.discrete.Discrete(name=None, tex_name=None, info=None, no_warn=False, min_iter=2, err_tol=0.01)[source]

Base discrete class.

Discrete classes export flag arrays (usually boolean) .

The uniqueness of discrete components is the way it works. Discrete components take inputs, criteria, and exports a set of flags with the component-defined meanings. These exported flags can be used in algebraic or differential equations to build piece-wise equations.

For example, Limiter takes a v-provider as input, two v-providers as the upper and the lower bound. It exports three flags: zi (within bound), zl (below lower bound), and zu (above upper bound). See the code example in models/pv.py for an example voltage-based PQ-to-Z conversion.

It is important to note when the flags are updated. Discrete subclasses can use three methods to check and update the value and equations. Among these methods, check_var is called before equation evaluation, but check_eq and set_eq are called after equation update. In the current implementation, check_var updates flags for variable-based discrete components (such as Limiter). check_eq updates flags for equation-involved discrete components (such as AntiWindup). set_var` is currently only used by AntiWindup to store the pegged states.

ANDES includes the following types of discrete components.

Limiters

class andes.core.discrete.Limiter(u, lower, upper, enable=True, name=None, tex_name=None, info=None, min_iter: int = 2, err_tol: float = 0.01, no_lower=False, no_upper=False, sign_lower=1, sign_upper=1, equal=True, no_warn=False, zu=0.0, zl=0.0, zi=1.0)[source]

Base limiter class.

This class compares values and sets limit values. Exported flags are zi, zl and zu.

Parameters:
u : BaseVar

Input Variable instance

lower : BaseParam

Parameter instance for the lower limit

upper : BaseParam

Parameter instance for the upper limit

no_lower : bool

True to only use the upper limit

no_upper : bool

True to only use the lower limit

sign_lower: 1 or -1

Sign to be multiplied to the lower limit

sign_upper: bool

Sign to be multiplied to the upper limit

equal : bool

True to include equal signs in comparison (>= or <=).

no_warn : bool

Disable initial limit warnings

zu : 0 or 1

Default value for zu if not enabled

zl : 0 or 1

Default value for zl if not enabled

zi : 0 or 1

Default value for zi if not enabled

Notes

If not enabled, the default flags are zu = zl = 0, zi = 1.

Attributes:
zl : array-like

Flags of elements violating the lower limit; A array of zeros and/or ones.

zi : array-like

Flags for within the limits

zu : array-like

Flags for violating the upper limit

class andes.core.discrete.SortedLimiter(u, lower, upper, n_select: int = 5, name=None, tex_name=None, enable=True, abs_violation=True, min_iter: int = 2, err_tol: float = 0.01, zu=0.0, zl=0.0, zi=1.0, ql=0.0, qu=0.0)[source]

A limiter that sorts inputs based on the absolute or relative amount of limit violations.

Parameters:
n_select : int

the number of violations to be flagged, for each of over-limit and under-limit cases. If n_select == 1, at most one over-limit and one under-limit inputs will be flagged. If n_select is zero, heuristics will be used.

abs_violation : bool

True to use the absolute violation. False if the relative violation abs(violation/limit) is used for sorting. Since most variables are in per unit, absolute violation is recommended.

class andes.core.discrete.HardLimiter(u, lower, upper, enable=True, name=None, tex_name=None, info=None, min_iter: int = 2, err_tol: float = 0.01, no_lower=False, no_upper=False, sign_lower=1, sign_upper=1, equal=True, no_warn=False, zu=0.0, zl=0.0, zi=1.0)[source]

Hard limiter for algebraic or differential variable. This class is an alias of Limiter.

class andes.core.discrete.AntiWindup(u, lower, upper, enable=True, no_warn=False, no_lower=False, no_upper=False, sign_lower=1, sign_upper=1, name=None, tex_name=None, info=None, state=None)[source]

Anti-windup limiter.

Anti-windup limiter prevents the wind-up effect of a differential variable. The derivative of the differential variable is reset if it continues to increase in the same direction after exceeding the limits. During the derivative return, the limiter will be inactive

if x > xmax and x dot > 0: x = xmax and x dot = 0
if x < xmin and x dot < 0: x = xmin and x dot = 0

This class takes one more optional parameter for specifying the equation.

Parameters:
state : State, ExtState

A State (or ExtState) whose equation value will be checked and, when condition satisfies, will be reset by the anti-windup-limiter.

Comparers

class andes.core.discrete.LessThan(u, bound, equal=False, enable=True, name=None, tex_name=None, info=None, cache=False, z0=0, z1=1)[source]

Less than (<) comparison function.

Exports two flags: z1 and z0. For elements satisfying the less-than condition, the corresponding z1 = 1. z0 is the element-wise negation of z1.

Notes

The default z0 and z1, if not enabled, can be set through the constructor.

class andes.core.discrete.Selector(*args, fun, tex_name=None, info=None)[source]

Selection between two variables using the provided reduce function.

The reduce function should take the given number of arguments. An example function is np.maximum.reduce which can be used to select the maximum.

Names are in s0, s1.

Warning

A potential bug when more than two inputs are provided, and values in different inputs are equal. Only two inputs are allowed.

Notes

A common pitfall is the 0-based indexing in the Selector flags. Note that exported flags start from 0. Namely, s0 corresponds to the first variable provided for the Selector constructor.

Examples

Example 1: select the largest value between v0 and v1 and put it into vmax.

After the definitions of v0 and v1, define the algebraic variable vmax for the largest value, and a selector vs

self.vmax = Algeb(v_str='maximum(v0, v1)',
                  tex_name='v_{max}',
                  e_str='vs_s0 * v0 + vs_s1 * v1 - vmax')

self.vs = Selector(self.v0, self.v1, fun=np.maximum.reduce)

The initial value of vmax is calculated by maximum(v0, v1), which is the element-wise maximum in SymPy and will be generated into np.maximum(v0, v1). The equation of vmax is to select the values based on vs_s0 and vs_s1.

class andes.core.discrete.Switcher(u, options: Union[list, Tuple], info: str = None, name: str = None, tex_name: str = None, cache=True)[source]

Switcher based on an input parameter.

The switch class takes one v-provider, compares the input with each value in the option list, and exports one flag array for each option. The flags are 0-indexed.

Exported flags are named with _s0, _s1, ..., with a total number of len(options). See the examples section.

Notes

Switches needs to be distinguished from Selector.

Switcher is for generating flags indicating option selection based on an input parameter. Selector is for generating flags at run time based on variable values and a selection function.

Examples

The IEEEST model takes an input for selecting the signal. Options are 1 through 6. One can construct

self.IC = NumParam(info='input code 1-6')  # input code
self.SW = Switcher(u=self.IC, options=[0, 1, 2, 3, 4, 5, 6])

If the IC values from the data file ends up being

self.IC.v = np.array([1, 2, 2, 4, 6])

Then, the exported flag arrays will be

{'IC_s0': np.array([0, 0, 0, 0, 0]),
 'IC_s1': np.array([1, 0, 0, 0, 0]),
 'IC_s2': np.array([0, 1, 1, 0, 0]),
 'IC_s3': np.array([0, 0, 0, 0, 0]),
 'IC_s4': np.array([0, 0, 0, 1, 0]),
 'IC_s5': np.array([0, 0, 0, 0, 0]),
 'IC_s6': np.array([0, 0, 0, 0, 1])
}

where IC_s0 is used for padding so that following flags align with the options.

Deadband

class andes.core.discrete.DeadBand(u, center, lower, upper, enable=True, equal=False, zu=0.0, zl=0.0, zi=0.0, name=None, tex_name=None, info=None)[source]

The basic deadband type.

Parameters:
u : NumParam

The pre-deadband input variable

center : NumParam

Neutral value of the output

lower : NumParam

Lower bound

upper : NumParam

Upper bound

enable : bool

Enabled if True; Disabled and works as a pass-through if False.

Notes

Input changes within a deadband will incur no output changes. This component computes and exports three flags.

Three flags computed from the current input:
  • zl: True if the input is below the lower threshold
  • zi: True if the input is within the deadband
  • zu: True if is above the lower threshold

Initial condition:

All three flags are initialized to zero. All flags are updated during check_var when enabled. If the deadband component is not enabled, all of them will remain zero.

Examples

Exported deadband flags need to be used in the algebraic equation corresponding to the post-deadband variable. Assume the pre-deadband input variable is var_in and the post-deadband variable is var_out. First, define a deadband instance db in the model using

self.db = DeadBand(u=self.var_in, center=self.dbc,
                   lower=self.dbl, upper=self.dbu)

To implement a no-memory deadband whose output returns to center when the input is within the band, the equation for var can be written as

var_out.e_str = 'var_in * (1 - db_zi) + \
                 (dbc * db_zi) - var_out'

Blocks

Background

The block library contains commonly used blocks (such as transfer functions and nonlinear functions). Variables and equations are pre-defined for blocks to be used as "lego pieces" for scripting DAE models. The base class for blocks is andes.core.block.Block.

The supported blocks include Lag, LeadLag, Washout, LeadLagLimit, PIController. In addition, the base class for piece-wise nonlinear functions, PieceWise is provided. PieceWise is used for implementing the quadratic saturation function MagneticQuadSat and exponential saturation function MagneticExpSat.

All variables in a block must be defined as attributes in the constructor, just like variable definition in models. The difference is that the variables are "exported" from a block to the capturing model. All exported variables need to placed in a dictionary, self.vars at the end of the block constructor.

Blocks can be nested as advanced usage. See the following API documentation for more details.

class andes.core.block.Block(name: Optional[str] = None, tex_name: Optional[str] = None, info: Optional[str] = None, namespace: str = 'local')[source]

Base class for control blocks.

Blocks are meant to be instantiated as Model attributes to provide pre-defined equation sets. Subclasses must overload the __init__ method to take custom inputs. Subclasses of Block must overload the define method to provide initialization and equation strings. Exported variables, services and blocks must be constructed into a dictionary self.vars at the end of the constructor.

Blocks can be nested. A block can have blocks but itself as attributes and therefore reuse equations. When a block has sub-blocks, the outer block must be constructed with a``name``.

Nested block works in the following way: the parent block modifies the sub-block's name attribute by prepending the parent block's name at the construction phase. The parent block then exports the sub-block as a whole. When the parent Model class picks up the block, it will recursively import the variables in the block and the sub-blocks correctly. See the example section for details.

Parameters:
name : str, optional

Block name

tex_name : str, optional

Block LaTeX name

info : str, optional

Block description.

namespace : str, local or parent

Namespace of the exported elements. If 'local', the block name will be prepended by the parent. If 'parent', the original element name will be used when exporting.

Warning

It is a good practice to avoid more than one level of nesting, to avoid multi-underscore variable names.

Examples

Example for two-level nested blocks. Suppose we have the following hierarchy

SomeModel  instance M
   |
LeadLag A  exports (x, y)
   |
Lag B      exports (x, y)

SomeModel instance M contains an instance of LeadLag block named A, which contains an instance of a Lag block named B. Both A and B exports two variables x and y.

In the code of Model, the following code is used to instantiate LeadLag

class SomeModel:
    def __init__(...)
        ...
        self.A = LeadLag(name='A',
                         u=self.foo1,
                         T1=self.foo2,
                         T2=self.foo3)

To use Lag in the LeadLag code, the following lines are found in the constructor of LeadLag

class LeadLag:
    def __init__(name, ...)
        ...
        self.B = Lag(u=self.y, K=self.K, T=self.T)
        self.vars = {..., 'A': self.A}

The __setattr__ magic of LeadLag takes over the construction and assigns A_B to B.name, given A's name provided at run time. self.A is exported with the internal name A at the end.

Again, the LeadLag instance name (A in this example) MUST be provided in SomeModel's constructor for the name prepending to work correctly. If there is more than one level of nesting, other than the leaf-level block, all parent blocks' names must be provided at instantiation.

When A is picked up by SomeModel.__setattr__, B is captured from A's exports. Recursively, B's variables are exported, Recall that B.name is now A_B, following the naming rule (parent block's name + variable name), B's internal variables become A_B_x and A_B_y.

In this way, B's define() needs no modification since the naming rule is the same. For example, B's internal y is always {self.name}_y, although B has gotten a new name A_B.

Transfer Functions

The following transfer function blocks have been implemented. They can be imported to build new models.

Algebraic

class andes.core.block.Gain(u, K, name=None, tex_name=None, info=None)[source]

Gain block.

     ┌───┐
u -> │ K │ -> y
     └───┘

Exports an algebraic output y.

define()[source]

Implemented equation and the initial condition are

\[\begin{split}y = K u \\ y^{(0)} = K u^{(0)}\end{split}\]

First Order

class andes.core.block.Integrator(u, T, K, y0, check_init=True, name=None, tex_name=None, info=None)[source]

Integrator block.

     ┌──────┐
u -> │ K/sT │ -> y
     └──────┘

Exports a differential variable y.

The initial output needs to be specified through y0.

define()[source]

Implemented equation and the initial condition are

\[\begin{split}\dot{y} = K u \\ y^{(0)} = 0\end{split}\]
class andes.core.block.IntegratorAntiWindup(u, T, K, y0, lower, upper, name=None, tex_name=None, info=None, no_warn=False)[source]

Integrator block with anti-windup limiter.

           upper
          /¯¯¯¯¯
     ┌──────┐
u -> │ K/sT │ -> y
     └──────┘
   _____/
   lower

Exports a differential variable y and an AntiWindup lim. The initial output must be specified through y0.

define()[source]

Implemented equation and the initial condition are

\[\begin{split}\dot{y} = K u \\ y^{(0)} = 0\end{split}\]
class andes.core.block.Lag(u, T, K, D=1, name=None, tex_name=None, info=None)[source]

Lag (low pass filter) transfer function.

     ┌────────┐
     │    K   │
u -> │ ────── │ -> y
     │ D + sT │
     └────────┘

Exports one state variable y as the output.

Parameters:
K

Gain

T

Time constant

D

Constant

u

Input variable

define()[source]

Notes

Equations and initial values are

\[\begin{split}T \dot{y} &= (Ku - Dy) \\ y^{(0)} &= Ku / D\end{split}\]
class andes.core.block.LagAntiWindup(u, T, K, lower, upper, D=1, name=None, tex_name=None, info=None)[source]

Lag (low pass filter) transfer function block with an anti-windup limiter.

             upper
           /¯¯¯¯¯¯
     ┌────────┐
     │    K   │
u -> │ ────── │ -> y
     │ D + sT │
     └────────┘
   ______/
   lower

Exports one state variable y as the output and one AntiWindup instance lim.

Parameters:
K

Gain

T

Time constant

D

Constant

u

Input variable

define()[source]

Notes

Equations and initial values are

\[\begin{split}T \dot{y} &= (Ku - Dy) \\ y^{(0)} &= K u / D\end{split}\]
class andes.core.block.Washout(u, T, K, name=None, tex_name=None, info=None)[source]

Washout filter (high pass) block.

     ┌────────┐
     │   sK   │
u -> │ ────── │ -> y
     │ 1 + sT │
     └────────┘

Exports state x (symbol x') and output algebraic variable y.

define()[source]

Notes

Equations and initial values:

\[\begin{split}T \dot{x'} &= (u - x') \\ T y &= K (u - x') \\ x'^{(0)} &= u \\ y^{(0)} &= 0\end{split}\]
class andes.core.block.WashoutOrLag(u, T, K, name=None, zero_out=True, tex_name=None, info=None)[source]

Washout with the capability to convert to Lag when K = 0.

Can be enabled with zero_out. Need to provide name to construct.

Exports state x (symbol x'), output algebraic variable y, and a LessThan block LT.

Parameters:
zero_out : bool, optional

If True, sT will become 1, and the washout will become a low-pass filter. If False, functions as a regular Washout.

define()[source]

Notes

Equations and initial values:

\[\begin{split}T \dot{x'} &= (u - x') \\ T y = z_0 K (u - x') + z_1 T x \\ x'^{(0)} &= u \\ y^{(0)} &= 0\end{split}\]

where z_0 is a flag array for the greater-than-zero elements, and z_1 is that for the less-than or equal-to zero elements.

class andes.core.block.LeadLag(u, T1, T2, K=1, zero_out=True, name=None, tex_name=None, info=None)[source]

Lead-Lag transfer function block in series implementation

     ┌───────────┐
     │   1 + sT1 │
u -> │ K ─────── │ -> y
     │   1 + sT2 │
     └───────────┘

Exports two variables: internal state x and output algebraic variable y.

Parameters:
T1 : BaseParam

Time constant 1

T2 : BaseParam

Time constant 2

zero_out : bool

True to allow zeroing out lead-lag as a pass through (when T1=T2=0)

Notes

To allow zeroing out lead-lag as a pure gain, set zero_out to True.

define()[source]

Notes

Implemented equations and initial values

\[\begin{split}T_2 \dot{x'} &= (u - x') \\ T_2 y &= K T_1 (u - x') + K T_2 x' + E_2 \, , \text{where} \\ E_2 = & \left\{\begin{matrix} (y - K x') &\text{ if } T_1 = T_2 = 0 \& zero\_out=True \\ 0& \text{ otherwise } \end{matrix}\right. \\ x'^{(0)} & = u\\ y^{(0)} & = Ku\\\end{split}\]
class andes.core.block.LeadLagLimit(u, T1, T2, lower, upper, name=None, tex_name=None, info=None)[source]

Lead-Lag transfer function block with hard limiter (series implementation)

     ┌─────────┐          upper
     │ 1 + sT1 │         /¯¯¯¯¯
u -> │ ─────── │ -> ynl / -> y
     │ 1 + sT2 │  _____/
     └─────────┘  lower

Exports four variables: state x, output before hard limiter ynl, output y, and AntiWindup lim.

define()[source]

Notes

Implemented control block equations (without limiter) and initial values

\[\begin{split}T_2 \dot{x'} &= (u - x') \\ T_2 y &= T_1 (u - x') + T_2 x' \\ x'^{(0)} &= y^{(0)} = u\end{split}\]

Second Order

class andes.core.block.Lag2ndOrd(u, K, T1, T2, name=None, tex_name=None, info=None)[source]

Second order lag transfer function (low-pass filter)

     ┌──────────────────┐
     │         K        │
u -> │ ──────────────── │ -> y
     │ 1 + sT1 + s^2 T2 │
     └──────────────────┘

Exports one two state variables (x, y), where y is the output.

Parameters:
u

Input

K

Gain

T1

First order time constant

T2

Second order time constant

define()[source]

Notes

Implemented equations and initial values are

\[\begin{split}T_2 \dot{x} &= Ku - y - T_1 x \\ \dot{y} &= x \\ x^{(0)} &= 0 \\ y^{(0)} &= K u\end{split}\]
class andes.core.block.LeadLag2ndOrd(u, T1, T2, T3, T4, zero_out=False, name=None, tex_name=None, info=None)[source]

Second-order lead-lag transfer function block

     ┌──────────────────┐
     │ 1 + sT3 + s^2 T4 │
u -> │ ──────────────── │ -> y
     │ 1 + sT1 + s^2 T2 │
     └──────────────────┘

Exports two internal states (x1 and x2) and output algebraic variable y.

# TODO: instead of implementing zero_out using LessThan and an additional term, consider correcting all parameters to 1 if all are 0.

define()[source]

Notes

Implemented equations and initial values are

\[\begin{split}T_2 \dot{x}_1 &= u - x_2 - T_1 x_1 \\ \dot{x}_2 &= x_1 \\ T_2 y &= T_2 x_2 + T_2 T_3 x_1 + T_4 (u - x_2 - T_1 x_1) + E_2 \, , \text{ where} \\ E_2 = & \left\{\begin{matrix} (y - x_2) &\text{ if } T_1 = T_2 = T_3 = T_4 = 0 \& zero\_out=True \\ 0& \text{ otherwise } \end{matrix}\right. \\ x_1^{(0)} &= 0 \\ x_2^{(0)} &= y^{(0)} = u\end{split}\]

Saturation

class andes.models.exciter.ExcExpSat(E1, SE1, E2, SE2, name=None, tex_name=None, info=None)[source]

Exponential exciter saturation block to calculate A and B from E1, SE1, E2 and SE2. Input parameters will be corrected and the user will be warned. To disable saturation, set either E1 or E2 to 0.

Parameters:
E1 : BaseParam

First point of excitation field voltage

SE1: BaseParam

Coefficient corresponding to E1

E2 : BaseParam

Second point of excitation field voltage

SE2: BaseParam

Coefficient corresponding to E2

define()[source]

Notes

The implementation solves for coefficients A and B which satisfy

\[E_1 S_{E1} = A e^{E1\times B} E_2 S_{E2} = A e^{E2\times B}\]

The solutions are given by

\[E_{1} S_{E1} e^{ \frac{E_1 \log{ \left( \frac{E_2 S_{E2}} {E_1 S_{E1}} \right)} } {E_1 - E_2}} - \frac{\log{\left(\frac{E_2 S_{E2}}{E_1 S_{E1}} \right)}}{E_1 - E_2}\]

Others

Value Selector

class andes.core.block.HVGate(u1, u2, name=None, tex_name=None, info=None)[source]

High Value Gate. Outputs the maximum of two inputs.

      ┌─────────┐
u1 -> │ HV Gate │
      │         │ ->  y
u2 -> │  (MAX)  │
      └─────────┘
class andes.core.block.LVGate(u1, u2, name=None, tex_name=None, info=None)[source]

Low Value Gate. Outputs the minimum of the two inputs.

      ┌─────────┐
u1 -> │ LV Gate |
      │         | ->  y
u2 -> │  (MIN)  |
      └─────────┘

Naming Convention

We loosely follow a naming convention when using modeling blocks. An instance of a modeling block is named with a two-letter acronym, followed by a number or a meaningful but short variaiable name. The acronym and the name are spelled in one word without underscore, as the output of the block already contains _y.

For example, two washout filters can be names WO1 and WO2. In another case, a first-order lag function for voltage sensing can be called LGv, or even LG if there is only one Lag instance in the model.

Naming conventions are not strictly enforced. Expressiveness and concision are encouraged.

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.

self.group = 'PSS'
self.flags.update({'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 andes/models/__init__.py, in file_classes, find the key pss and add IEEEST to its value list. In file_classes, keys are the .py file names under the folder models, and values are class names to be imported from that file. If the file name does not exist as a key in file_classes, add it after all prerequisite models. For example, PSS should be added after exciters (and generators, of course).

Finally, locate andes/models/group.py, check if the class with PSS exist. It is the name of IEEEST's group name. 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.