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 atsystem.PFlow
, and the numerical DAE instance is atsystem.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 dictionarySystem.models
with model names as keys and the corresponding instances as values.Examples
system.Bus
stores the Bus object, andsystem.GENCLS
stores the classical generator object,system.models['Bus']
points the same instance assystem.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, andSystem.TDS
andSystem.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) 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.
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 packagedill
.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
withdill
.If no change is made to models, future calls to
prepare()
can be replaced withundill()
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 askvxopt.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.
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 areidx
, unique device idx of typeandes.core.param.DataParam
u
, connection status of typeandes.core.param.NumParam
name
, (device name of typeandes.core.param.DataParam
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 followingfrom 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 usesself.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, oralter()
, 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 (fromsystem.config.freq
). -sys_mva
for system base mva (fromsystem.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
andparams_ext
, two OrderedDict for internal (both numerical and non-numerical) and external parameters, respectively.num_params
for numerical parameters, both internal and external.states
andalgebs
, twoOrderedDict
for state variables and algebraic variables, respectively.states_ext
andalgebs_ext
, twoOrderedDict
for external states and algebraics.discrete
, an OrderedDict for discrete components.blocks
, an OrderedDict for blocks.services
, an OrderedDict for services withv_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 document
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.
- non_vars_dict : OrderedDict
symbols in
input_syms
but not invar_syms
.
-
generate_init
()[source] Generate lambda functions for initial values.
-
generate_jacobians
()[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)
, whererow
andcol
are the internal indices, andval
is the numerical lambda function. They will be stored torow -> 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)
- non_vars_dict : OrderedDict
name-symbol pair of all non-variables, namely, (inputs_dict - vars_dict)
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 functions_numeric
: custom service value functiong_numeric
: custom algebraic equations; update thee
of the corresponding variable.f_numeric
: custom differential equations; update thee
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.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
.
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 thev
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 isfloat
.
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 haveself.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.tself.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, addressable: Optional[bool] = True, export: Optional[bool] = True, diag_eps: Optional[float] = 0.0)[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
Associated discrete component. Will call check_var on the discrete component.
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
-
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, 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)[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, addressable: Optional[bool] = True, export: Optional[bool] = True, diag_eps: Optional[float] = 0.0)[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, 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 sett_const
, as it will conflict with the sourceState
variable. In fact, one should not sete_str
forExtState
.
-
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, 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. UsingAliasAlgeb
, A model can avoid adding an additional variable with a dummy equations.Like
ExtVar
, labels ofAliasAlgeb
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 theidx
of devices in the model or group.
Examples
A synchronous generator needs to retrieve the
p
andq
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
asp0
: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 hasself.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 definesself.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 dataidx 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 theArea
class, one definesclass 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 referencingandes.models.group.SynGen
withself.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 hasself.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 definesself.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 dataidx 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 referencingandes.models.group.SynGen
withself.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.
EventFlag.v stores the values of the input variable from the previous iteration/step.
-
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 to flag events that extends for period of time after event disappears.
EventFlag.v stores the flags whether the extended time has completed. Outputs will become 1 once then event starts until the extended time ends.
Parameters: - trig : str, rise, fall
Triggering edge for the inception 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 ofself.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_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.
See also
numpy.ufunc.reduce
- NumPy reduce function
andes.core.block.HVGate
andes.core.block.LVGate
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 intonp.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
andy
.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 assignsA_B
to B.name, given A's name provided at run time. self.A is exported with the internal nameA
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 becomeA_B_x
andA_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 nameA_B
.
Transfer Functions¶
The following transfer function blocks have been implemented. They can be imported to build new models.
Algebraic¶
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)[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, name=None, tex_name=None, info=None)[source] Lag (low pass filter) transfer function.
┌────────┐ │ K │ u -> │ ────── │ -> y │ 1 + sT │ └────────┘
Exports one state variable y as the output.
Parameters: - K
Gain
- T
Time constant
- u
Input variable
-
define
()[source] Notes
Equations and initial values are
\[\begin{split}T \dot{y} &= (Ku - y) \\ y^{(0)} &= K u\end{split}\]
-
class
andes.core.block.
LagAntiWindup
(u, T, K, lower, upper, name=None, tex_name=None, info=None)[source] Lag (low pass filter) transfer function block with an anti-windup limiter.
upper /¯¯¯¯¯¯ ┌────────┐ │ K │ u -> │ ────── │ -> y │ 1 + sT │ └────────┘ ______/ lower
Exports one state variable y as the output and one AntiWindup instance lim.
Parameters: - K
Gain
- T
Time constant
- u
Input variable
-
define
()[source] Notes
Equations and initial values are
\[\begin{split}T \dot{y} &= (Ku - y) \\ y^{(0)} &= K u\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, andz_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) | └─────────┘
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.

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.
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.

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.