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.

The following classes are related to models:

ModelData(*args[, three_params])

Class for holding parameter data for a model.

Model([system, config])

Base class for power system DAE models.

ModelCache()

Class for caching the return value of callback functions.

ModelCall()

Class for storing generated function calls, Jacobian calls, and arguments.

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
namestr

name of the cached function return value

callbackcallable

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 define DAE variables, services, and other types of parameters, in the constructor __init__.

Examples

Take the static PQ as an example, the subclass of Model, PQ, should look 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.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 in 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. They 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 variable 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 implement a piece-wise 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_paramsOrderedDict

{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.SymProcessor(parent)[source]

A helper class for symbolic processing and code generation.

Parameters
parentModel

The Model instance to process

Attributes
xysympy.Matrix

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

fsympy.Matrix

differential equations pretty print

gsympy.Matrix

algebraic equations pretty print

dfsympy.SparseMatrix

df /d (xy) pretty print

dgsympy.SparseMatrix

dg /d (xy) pretty print

inputs_dictOrderedDict

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

vars_dictOrderedDict

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

generate_equations()[source]

Generate equations.

The pretty-print equations in matrices can be accessed in self.f_matrix and self.g_matrix.

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_dictOrderedDict

name-symbol pair of all parameters, variables and configs

vars_dictOrderedDict

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.