You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
1425 lines
57 KiB
1425 lines
57 KiB
.. _modeling:
|
|
|
|
*****************
|
|
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.
|
|
|
|
.. autoclass:: andes.system.System
|
|
:noindex:
|
|
|
|
.. 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.
|
|
|
|
.. autofunction:: andes.system.System.import_models
|
|
:noindex:
|
|
|
|
.. autofunction:: andes.system.System.import_groups
|
|
:noindex:
|
|
|
|
.. autofunction:: andes.system.System.import_routines
|
|
:noindex:
|
|
|
|
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``.
|
|
|
|
.. autofunction:: andes.system.System.prepare
|
|
:noindex:
|
|
|
|
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.
|
|
|
|
.. autofunction:: andes.system.System.dill
|
|
:noindex:
|
|
|
|
.. autofunction:: andes.system.System.undill
|
|
:noindex:
|
|
|
|
DAE Storage
|
|
-----------
|
|
|
|
``System.dae`` is an instance of the numerical DAE class.
|
|
|
|
.. autofunction:: andes.variables.dae.DAE
|
|
:noindex:
|
|
|
|
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.
|
|
|
|
.. autofunction:: andes.system.System.vars_to_dae
|
|
:noindex:
|
|
|
|
.. autofunction:: andes.system.System.vars_to_models
|
|
:noindex:
|
|
|
|
.. autofunction:: andes.system.System._e_to_dae
|
|
:noindex:
|
|
|
|
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%.
|
|
|
|
.. autofunction:: andes.system.System.store_sparse_pattern
|
|
:noindex:
|
|
|
|
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.
|
|
|
|
.. autofunction:: andes.system.System.init
|
|
:noindex:
|
|
|
|
.. autofunction:: andes.system.System.e_clear
|
|
:noindex:
|
|
|
|
.. autofunction:: andes.system.System.l_update_var
|
|
:noindex:
|
|
|
|
.. autofunction:: andes.system.System.f_update
|
|
:noindex:
|
|
|
|
.. autofunction:: andes.system.System.l_update_eq
|
|
:noindex:
|
|
|
|
.. autofunction:: andes.system.System.g_update
|
|
:noindex:
|
|
|
|
.. autofunction:: andes.system.System.j_update
|
|
:noindex:
|
|
|
|
|
|
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.
|
|
|
|
.. autofunction:: andes.system.System.get_config
|
|
:noindex:
|
|
|
|
.. autofunction:: andes.system.System.save_config
|
|
:noindex:
|
|
|
|
.. autofunction:: andes.system.System.load_config
|
|
:noindex:
|
|
|
|
.. warning::
|
|
|
|
It is important to note that configs from files is passed to *model constructors* during instantiation.
|
|
If one needs to modify config for a run, it needs to be done before instantiating ``System``, or before running
|
|
``andes`` from command line.
|
|
Directly modifying ``Model.config`` may not take effect or have side effect as for the current implementation.
|
|
|
|
Group
|
|
======
|
|
A group is a collection of similar functional models with common variables and parameters.
|
|
It is mandatory to enforce the common variables and parameters when develop new models.
|
|
The common variables and parameters are typically the interface when connecting different group models.
|
|
|
|
For example, the Group `RenGen` has variables `Pe` and `Qe`, which are active power output and reactive power output.
|
|
Such common variables can be retrieved by other models, such as one in the
|
|
Group `RenExciter` for further calculation.
|
|
|
|
In such a way, the same variable interface is realized so that all model in the same group could carry out similar
|
|
function.
|
|
|
|
Models
|
|
======
|
|
This section introduces the modeling of power system devices. The terminology "model" is used to describe the
|
|
mathematical representation of a *type* of device, such as synchronous generators or turbine governors. The
|
|
terminology "device" is used to describe a particular instance of a model, for example, a specific generator.
|
|
|
|
To define a model in ANDES, two classes, ``ModelData`` and ``Model`` need to be utilized. Class ``ModelData`` is
|
|
used for defining parameters that will be provided from input files. It provides API for adding data from
|
|
devices and managing the data.
|
|
Class ``Model`` is used for defining other non-input parameters, service
|
|
variables, and DAE variables. It provides API for converting symbolic equations, storing Jacobian patterns, and
|
|
updating equations.
|
|
|
|
Model Data
|
|
----------
|
|
.. autoclass:: andes.core.model.ModelData
|
|
:noindex:
|
|
|
|
Cache
|
|
`````
|
|
`ModelData` uses a lightweight class :py: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`.
|
|
|
|
.. autofunction:: andes.core.model.ModelCache.add_callback
|
|
:noindex:
|
|
|
|
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 :py:class:`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
|
|
--------------------
|
|
.. autoclass:: andes.core.model.Model
|
|
:noindex:
|
|
|
|
Dynamicity Under the Hood
|
|
-------------------------
|
|
The magic for automatic creation of variables are all hidden in :py:func:`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 :py:class:`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, :py:func:`andes.core.model.get_inputs`, to
|
|
collect the variable value arrays in a dictionary, which can be effortlessly passed as arguments to numerical
|
|
functions.
|
|
|
|
Commonly Used Attributes in Models
|
|
``````````````````````````````````
|
|
The following ``Model`` attributes are commonly used for debugging.
|
|
If the attribute is an `OrderedDict`, the keys are attribute names in str, and corresponding values are the
|
|
instances.
|
|
|
|
- ``params`` and ``params_ext``, two `OrderedDict` for internal (both numerical and non-numerical) and external
|
|
parameters, respectively.
|
|
- ``num_params`` for numerical parameters, both internal and external.
|
|
- ``states`` and ``algebs``, two ``OrderedDict`` for state variables and algebraic variables, respectively.
|
|
- ``states_ext`` and ``algebs_ext``, two ``OrderedDict`` for external states and algebraics.
|
|
- ``discrete``, an `OrderedDict` for discrete components.
|
|
- ``blocks``, an `OrderedDict` for blocks.
|
|
- ``services``, an `OrderedDict` for services with ``v_str``.
|
|
- ``services_ext``, an `OrderedDict` for externally retrieved services.
|
|
|
|
Attributes in `Model.cache`
|
|
```````````````````````````
|
|
Attributes in `Model.cache` are additional book-keeping structures for variables, parameters and services.
|
|
The following attributes are defined.
|
|
|
|
- ``all_vars``: all the variables.
|
|
- ``all_vars_names``, a list of all variable names.
|
|
- ``all_params``, all parameters.
|
|
- ``all_params_names``, a list of all parameter names.
|
|
- ``algebs_and_ext``, an `OrderedDict` of internal and external algebraic variables.
|
|
- ``states_and_ext``, an `OrderedDict` of internal and external differential variables.
|
|
- ``services_and_ext``, an `OrderedDict` of internal and external service variables.
|
|
- ``vars_int``, an `OrderedDict` of all internal variables, states and then algebs.
|
|
- ``vars_ext``, an `OrderedDict` of all external variables, states and then algebs.
|
|
|
|
Equation Generation
|
|
-------------------
|
|
``Model.syms``, an instance of ``SymProcessor``, handles the symbolic to numeric generation when called. The
|
|
equation generation is a multi-step process with symbol preparation, equation generation, Jacobian generation,
|
|
initializer generation, and pretty print generation.
|
|
|
|
.. autoclass:: andes.core.model.SymProcessor
|
|
:members: generate_symbols, generate_equations, generate_jacobians, generate_init
|
|
:noindex:
|
|
|
|
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
|
|
:py:mod:`andes.core.common.JacTriplet` class for more details.
|
|
|
|
The triplets, the equation (row) index, variable (column) index, and values (constant numbers or callable) are
|
|
stored in ``Model`` attributes with the name of ``_{i, j, v}{Jacobian Name}{c or None}``, where
|
|
``{i, j, v}`` is a single character for row, column or value, ``{Jacobian Name}`` is a two-character Jacobian
|
|
name chosen from ``fx, fy, gx, and gy``, and ``{c or None}`` is either character ``c`` or no character,
|
|
indicating whether it corresponds to the constants or non-constants in the Jacobian.
|
|
|
|
For example, the triplets for the
|
|
constants in Jacobian ``gy`` are stored in ``_igyc``, ``_jgyc``, and ``_vgyc``.
|
|
|
|
In terms of the non-constant entries in Jacobians, the callable functions are stored in the corresponding
|
|
``_v{Jacobian Name}`` array. Note the differences between, for example, ``_vgy`` an ``_vgyc``: ``_vgy`` is a
|
|
list of callables, while ``_vgyc`` is a list of constant numbers.
|
|
|
|
Concrete Jacobian Storage
|
|
`````````````````````````
|
|
When a specific system is loaded and the addresses are assigned to variables, the abstract Jacobian triplets,
|
|
more specifically, the rows and columns, are replaced with the array of addresses. The new addresses and values
|
|
will be stored in ``Model`` attributes with the names ``{i, j, v}{Jacobian Name}{c or None}``. Note that there
|
|
is no underscore for the concrete Jacobian triplets.
|
|
|
|
For example, if model ``PV`` has a list of variables ``[p, q, a, v]`` .
|
|
The equation associated with ``p`` is ``- u * p0``, and the equation associated with ``q`` is ``u * (v0 - v)``.
|
|
Therefore, the derivative of equation ``v0 - v`` over ``v`` is ``-u``. Note that ``u`` is unknown at generation
|
|
time, thus the value is NOT a constant and should to go ``vgy``.
|
|
|
|
The values in ``_igy``, ``_jgy`` and ``_vgy`` contains, respectively, ``1``, ``3``, and a lambda function which
|
|
returns ``-u``.
|
|
|
|
When a specific system is loaded, for example, a 5-bus system, the addresses for the ``q`` and ``v`` are ``[11,
|
|
13, 15``, and ``[5, 7, 9]``.
|
|
``PV.igy`` and ``PV.jgy`` will thus query the corresponding address list based on ``PV._igy`` and ``PV._jgy``
|
|
and store ``[11, 13, 15``, and ``[5, 7, 9]``.
|
|
|
|
Initialization
|
|
--------------
|
|
Value providers such as services and DAE variables need to be initialized. Services are initialized before
|
|
any DAE variable. Both Services and DAE Variables are initialized *sequentially* in the order of declaration.
|
|
|
|
Each Service, in addition to the standard ``v_str`` for symbolic initialization, provides a ``v_numeric`` hook
|
|
for specifying a custom function for initialization. Custom initialization functions for DAE variables, are
|
|
lumped in a single function in ``Model.v_numeric``.
|
|
|
|
ANDES has an *experimental* Newton-Krylov method based iterative initialization. All DAE variables with ``v_iter``
|
|
will be initialized using the iterative approach
|
|
|
|
Additional Numerical Equations
|
|
------------------------------
|
|
Addition numerical equations are allowed to complete the "hybrid symbolic-numeric" framework. Numerical function
|
|
calls are useful when the model DAE is non-standard or hard to be generalized. Since the
|
|
symbolic-to-numeric generation is an additional layer on top of the numerical simulation, it is fundamentally
|
|
the same as user-provided numerical function calls.
|
|
|
|
ANDES provides the following hook functions in each ``Model`` subclass for custom numerical functions:
|
|
|
|
- ``v_numeric``: custom initialization function
|
|
- ``s_numeric``: custom service value function
|
|
- ``g_numeric``: custom algebraic equations; update the ``e`` of the corresponding variable.
|
|
- ``f_numeric``: custom differential equations; update the ``e`` of the corresponding variable.
|
|
- ``j_numeric``: custom Jacobian equations; the function should append to ``_i``, ``_j`` and ``_v`` structures.
|
|
|
|
For most models, numerical function calls are unnecessary and not recommended as it increases code complexity.
|
|
However, when the data structure or the DAE are difficult to generalize in the symbolic framework, the numerical
|
|
equations can be used.
|
|
|
|
For interested readers, see the ``COI`` symbolic implementation which calculated the
|
|
center-of-inertia speed of generators. The ``COI`` could have been implemented numerically with for loops
|
|
instead of ``NumReduce``, ``NumRepeat`` and external variables.
|
|
|
|
..
|
|
Atoms
|
|
ANDES defines several types of atoms for building DAE models, including parameters, DAE variables,
|
|
and service variables. Atoms can be used to build models and libraries, combined with discrete
|
|
components and blocks.
|
|
|
|
|
|
Atom Types
|
|
============
|
|
ANDES contains three types of atom classes for building DAE models.
|
|
These types are parameter, variable and service.
|
|
|
|
Value Provider
|
|
--------------
|
|
|
|
Before addressing specific atom classes, the terminology `v-provider`, and `e-provider` are discussed.
|
|
A value provider class (or `v-provider` for short) references any class with a member attribute named ``v``,
|
|
which should be a list or a 1-dimensional array of values.
|
|
For example, all parameter classes are v-providers, since a parameter class should provide
|
|
values for that parameter.
|
|
|
|
.. note::
|
|
In fact, all types of atom classes are v-providers, meaning that an instance of an atom class must contain values.
|
|
|
|
The values in the `v` attribute of a particular instance are values that will substitute the instance for computation.
|
|
If in a model, one has a parameter ::
|
|
|
|
self.v0 = NumParam()
|
|
self.b = NumParam()
|
|
|
|
# where self.v0.v = np.array([1., 1.05, 1.1]
|
|
# and self.b.v = np.array([10., 10., 10.]
|
|
|
|
Later, this parameter is used in an equation, such as ::
|
|
|
|
self.v = ExtAlgeb(model='Bus', src='v',
|
|
indexer=self.bus,
|
|
e_str='v0 **2 * b')
|
|
|
|
While computing `v0 ** 2 * b`, `v0` and `b` will be substituted with the values in `self.v0.v` and `self.b.v`.
|
|
|
|
Sharing this interface `v` allows interoperability among parameters and variables and services.
|
|
In the above example, if one defines `v0` as a `ConstService` instance, such as ::
|
|
|
|
self.v0 = ConstService(v_str='1.0')
|
|
|
|
Calculations will still work without modification.
|
|
|
|
Equation Provider
|
|
-----------------
|
|
Similarly, an equation provider class (or `e-provider`) references any class with a member attribute named ``e``,
|
|
which should be a 1-dimensional array of values.
|
|
The values in the `e` array are the results from the equation and will be summed to the numerical DAE at the addresses
|
|
specified by the attribute `a`.
|
|
|
|
.. note::
|
|
Currently, only variables are `e-provider` types.
|
|
|
|
If a model has an external variable that links to Bus.v (voltage), such as ::
|
|
|
|
self.v = ExtAlgeb(model='Bus', src='v',
|
|
indexer=self.bus,
|
|
e_str='v0 **2 * b')
|
|
|
|
The addresses of the corresponding voltage variables will be retrieved into `self.v.a`,
|
|
and the equation evaluation results will be stored in `self.v.e`
|
|
|
|
Parameters
|
|
==========
|
|
|
|
Background
|
|
-----------
|
|
|
|
Parameter is a type of building atom for DAE models.
|
|
Most parameters are read directly from an input file and passed to equation,
|
|
and other parameters can be calculated from existing parameters.
|
|
|
|
The base class for parameters in ANDES is `BaseParam`, which defines interfaces for adding values and
|
|
checking the number of values. `BaseParam` has its values stored in a plain list, the member attribute `v`.
|
|
Subclasses such as `NumParam` stores values using a NumPy ndarray.
|
|
|
|
An overview of supported parameters is given below.
|
|
|
|
+---------------+----------------------------------------------------------------------------+
|
|
| Subclasses | Description |
|
|
+===============+============================================================================+
|
|
| DataParam | An alias of `BaseParam`. Can be used for any non-numerical parameters. |
|
|
+---------------+----------------------------------------------------------------------------+
|
|
| NumParam | The numerical parameter type. Used for all parameters in equations |
|
|
+---------------+----------------------------------------------------------------------------+
|
|
| IdxParam | The parameter type for storing `idx` into other models |
|
|
+---------------+----------------------------------------------------------------------------+
|
|
| ExtParam | Externally defined parameter |
|
|
+---------------+----------------------------------------------------------------------------+
|
|
| TimerParam | Parameter for storing the action time of events |
|
|
+---------------+----------------------------------------------------------------------------+
|
|
|
|
Data Parameters
|
|
---------------
|
|
.. autoclass:: andes.core.param.BaseParam
|
|
:noindex:
|
|
|
|
.. autoclass:: andes.core.param.DataParam
|
|
:noindex:
|
|
|
|
.. autoclass:: andes.core.param.IdxParam
|
|
:noindex:
|
|
|
|
Numeric Parameters
|
|
------------------
|
|
.. autoclass:: andes.core.param.NumParam
|
|
:noindex:
|
|
|
|
External Parameters
|
|
-------------------
|
|
.. autoclass:: andes.core.param.ExtParam
|
|
:noindex:
|
|
|
|
Timer Parameter
|
|
---------------
|
|
.. autoclass:: andes.core.param.TimerParam
|
|
:noindex:
|
|
|
|
|
|
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 :math:`\textbf{T} \dot{x} = \textbf{f}`|
|
|
+-----------+---------------------------------------------------------------------------------------+
|
|
| Algeb | An algebraic variable and an associated algebraic equation :math:`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`.
|
|
|
|
.. autoclass:: andes.core.var.BaseVar
|
|
:noindex:
|
|
|
|
.. autoclass:: andes.core.var.ExtVar
|
|
:noindex:
|
|
|
|
.. autoclass:: andes.core.var.State
|
|
:noindex:
|
|
|
|
.. autoclass:: andes.core.var.Algeb
|
|
:noindex:
|
|
|
|
.. autoclass:: andes.core.var.ExtState
|
|
:noindex:
|
|
|
|
.. autoclass:: andes.core.var.ExtAlgeb
|
|
:noindex:
|
|
|
|
.. autoclass:: andes.core.var.AliasState
|
|
:noindex:
|
|
|
|
.. autoclass:: andes.core.var.AliasAlgeb
|
|
:noindex:
|
|
|
|
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.
|
|
|
|
.. autoclass:: andes.core.service.ConstService
|
|
:noindex:
|
|
|
|
.. autoclass:: andes.core.service.VarService
|
|
:noindex:
|
|
|
|
.. autoclass:: andes.core.service.PostInitService
|
|
:noindex:
|
|
|
|
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')
|
|
|
|
.. autoclass:: andes.core.service.ExtService
|
|
:noindex:
|
|
|
|
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`.
|
|
|
|
.. autoclass:: andes.core.service.BackRef
|
|
:noindex:
|
|
|
|
.. autoclass:: andes.core.service.NumReduce
|
|
:noindex:
|
|
|
|
.. autoclass:: andes.core.service.NumRepeat
|
|
:noindex:
|
|
|
|
.. autoclass:: andes.core.service.IdxRepeat
|
|
:noindex:
|
|
|
|
.. autoclass:: andes.core.service.RefFlatten
|
|
:noindex:
|
|
|
|
|
|
Value Manipulation
|
|
------------------
|
|
.. autoclass:: andes.core.service.Replace
|
|
:noindex:
|
|
|
|
.. autoclass:: andes.core.service.FlagValue
|
|
:noindex:
|
|
|
|
|
|
Idx and References
|
|
-------------------------------------------
|
|
.. autoclass:: andes.core.service.DeviceFinder
|
|
:noindex:
|
|
|
|
.. autoclass:: andes.core.service.BackRef
|
|
:noindex:
|
|
|
|
.. autoclass:: andes.core.service.RefFlatten
|
|
:noindex:
|
|
|
|
|
|
Events
|
|
----------
|
|
.. autoclass:: andes.core.service.EventFlag
|
|
:noindex:
|
|
|
|
.. autoclass:: andes.core.service.ExtendedEvent
|
|
:noindex:
|
|
|
|
|
|
Data Select
|
|
-----------
|
|
.. autoclass:: andes.core.service.DataSelect
|
|
:noindex:
|
|
|
|
.. autoclass:: andes.core.service.NumSelect
|
|
:noindex:
|
|
|
|
|
|
Miscellaneous
|
|
-------------
|
|
.. autoclass:: andes.core.service.InitChecker
|
|
:noindex:
|
|
|
|
|
|
|
|
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 :py:mod:`andes.core.discrete.Discrete`.
|
|
|
|
.. autoclass:: andes.core.discrete.Discrete
|
|
:noindex:
|
|
|
|
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
|
|
--------
|
|
.. autoclass:: andes.core.discrete.Limiter
|
|
:noindex:
|
|
|
|
.. autoclass:: andes.core.discrete.SortedLimiter
|
|
:noindex:
|
|
|
|
.. autoclass:: andes.core.discrete.HardLimiter
|
|
:noindex:
|
|
|
|
.. autoclass:: andes.core.discrete.AntiWindup
|
|
:noindex:
|
|
|
|
Comparers
|
|
---------
|
|
.. autoclass:: andes.core.discrete.LessThan
|
|
:noindex:
|
|
|
|
.. autoclass:: andes.core.discrete.Selector
|
|
:noindex:
|
|
|
|
.. autoclass:: andes.core.discrete.Switcher
|
|
:noindex:
|
|
|
|
Deadband
|
|
--------
|
|
.. autoclass:: andes.core.discrete.DeadBand
|
|
:noindex:
|
|
|
|
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 :py:mod:`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.
|
|
|
|
.. autoclass:: andes.core.block.Block
|
|
:noindex:
|
|
|
|
Transfer Functions
|
|
------------------
|
|
|
|
The following transfer function blocks have been implemented.
|
|
They can be imported to build new models.
|
|
|
|
Algebraic
|
|
`````````
|
|
.. autoclass:: andes.core.block.Gain
|
|
:members: define
|
|
:noindex:
|
|
|
|
First Order
|
|
```````````
|
|
.. autoclass:: andes.core.block.Integrator
|
|
:members: define
|
|
:noindex:
|
|
|
|
.. autoclass:: andes.core.block.IntegratorAntiWindup
|
|
:members: define
|
|
:noindex:
|
|
|
|
.. autoclass:: andes.core.block.Lag
|
|
:members: define
|
|
:noindex:
|
|
|
|
.. autoclass:: andes.core.block.LagAntiWindup
|
|
:members: define
|
|
:noindex:
|
|
|
|
.. autoclass:: andes.core.block.Washout
|
|
:members: define
|
|
:noindex:
|
|
|
|
.. autoclass:: andes.core.block.WashoutOrLag
|
|
:members: define
|
|
:noindex:
|
|
|
|
.. autoclass:: andes.core.block.LeadLag
|
|
:members: define
|
|
:noindex:
|
|
|
|
.. autoclass:: andes.core.block.LeadLagLimit
|
|
:members: define
|
|
:noindex:
|
|
|
|
Second Order
|
|
````````````
|
|
.. autoclass:: andes.core.block.Lag2ndOrd
|
|
:members: define
|
|
:noindex:
|
|
|
|
.. autoclass:: andes.core.block.LeadLag2ndOrd
|
|
:members: define
|
|
:noindex:
|
|
|
|
Saturation
|
|
----------
|
|
.. autoclass:: andes.models.exciter.ExcExpSat
|
|
:members: define
|
|
:noindex:
|
|
|
|
|
|
Others
|
|
------
|
|
|
|
Value Selector
|
|
``````````````
|
|
.. autoclass:: andes.core.block.HVGate
|
|
:noindex:
|
|
|
|
.. autoclass:: andes.core.block.LVGate
|
|
:noindex:
|
|
|
|
Naming Convention
|
|
-----------------
|
|
|
|
We loosely follow a naming convention when using modeling blocks.
|
|
An instance of a modeling block is named with a two-letter
|
|
acronym, followed by a number or a meaningful but short variaiable name.
|
|
The acronym and the name are spelled in one word without underscore, as
|
|
the output of the block already contains ``_y``.
|
|
|
|
For example, two washout filters can be names ``WO1`` and ``WO2``.
|
|
In another case, a first-order lag function for voltage sensing
|
|
can be called ``LGv``, or even ``LG`` if there is only one Lag
|
|
instance in the model.
|
|
|
|
Naming conventions are not strictly enforced. Expressiveness
|
|
and concision are encouraged.
|
|
|
|
Examples
|
|
========
|
|
We show two examples to demonstrate modeling from equations and modeling from
|
|
control block diagrams.
|
|
|
|
- The TGOV1 example shows code snippet for equation-based modeling
|
|
and, as well as code for block-based modeling.
|
|
- The IEEEST example walks through the source code and explains the complete
|
|
setup, including optional parameters, input selection, and manual per-unit
|
|
conversion.
|
|
|
|
TGOV1
|
|
-----
|
|
The TGOV1_ turbine governor model is shown as a practical example using the library.
|
|
|
|
.. image:: images/example-tgov1/tgov1.png
|
|
:align: center
|
|
|
|
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.
|
|
|
|
.. math::
|
|
|
|
\left[
|
|
\begin{matrix}
|
|
\dot{x}_{LG} \\
|
|
\dot{x}_{LL}
|
|
\end{matrix}
|
|
\right]
|
|
=
|
|
\left[
|
|
\begin{matrix}z_{i,lim}^{LG} \left(P_{d} - x_{LG}\right) / {T_1}
|
|
\\
|
|
\left(x_{LG} - x_{LL}\right) / T_3
|
|
\end{matrix}
|
|
\right]
|
|
|
|
\left[
|
|
\begin{matrix}
|
|
0 \\
|
|
0 \\
|
|
0 \\
|
|
0 \\
|
|
0 \\
|
|
0
|
|
\end{matrix}
|
|
\right]
|
|
=
|
|
\left[
|
|
\begin{matrix}
|
|
(1 - \omega) - \omega_{d} \\
|
|
R \times \tau_{m0} - P_{ref} \\
|
|
\left(P_{ref} + \omega_{d}\right)/R - P_{d}\\
|
|
D_{t} \omega_{d} + y_{LL} - P_{OUT}\\
|
|
\frac{T_2}{T_3} \left(x_{LG} - x_{LL}\right) + x_{LL} - y_{LL}\\
|
|
u \left(P_{OUT} - \tau_{m0}\right)
|
|
\end{matrix}
|
|
\right]
|
|
|
|
where *LG* and *LL* denote the lag block and the lead-lag block, :math:`\dot{x}_{LG}` and :math:`\dot{x}_{LL}`
|
|
are the internal states, :math:`y_{LL}` is the lead-lag output, :math:`\omega` the generator speed,
|
|
:math:`\omega_d` the generator under-speed, :math:`P_d` the droop output, :math:`\tau_{m0}` the steady-state
|
|
torque input, and :math:`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``.
|
|
|
|
.. code:: python
|
|
|
|
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.
|
|
|
|
.. code:: python
|
|
|
|
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.
|
|
|
|
.. image:: images/diagrams/ieeest.png
|
|
:align: center
|
|
|
|
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
|
|
|
|
.. code:: python
|
|
|
|
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.
|
|
|
|
.. code:: python
|
|
|
|
self.group = 'PSS'
|
|
self.flags.update({'tds': True})
|
|
|
|
Next, ``Replace`` is used to replace input parameters that satisfy a lambda function
|
|
with new values.
|
|
|
|
.. code:: python
|
|
|
|
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
|
|
|
|
.. code:: python
|
|
|
|
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.
|
|
|
|
.. code:: python
|
|
|
|
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.
|
|
|
|
.. code:: python
|
|
|
|
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.
|
|
|
|
.. code:: python
|
|
|
|
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.
|
|
|
|
.. code:: python
|
|
|
|
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.
|
|
|
|
.. code:: python
|
|
|
|
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.
|
|
|
|
.. code:: python
|
|
|
|
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.
|
|
|
|
.. code:: python
|
|
|
|
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``:
|
|
|
|
.. code:: python
|
|
|
|
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
|
|
|
|
.. code:: python
|
|
|
|
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``.
|
|
|
|
.. code:: python
|
|
|
|
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``:
|
|
|
|
.. code:: python
|
|
|
|
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``:
|
|
|
|
.. code:: python
|
|
|
|
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.
|