Models#
This section introduces the modeling of power system devices. The terminology "model" is used to describe the mathematical representation of a type of device, such as synchronous generators or turbine governors. The terminology "device" is used to describe a particular instance of a model, for example, a specific generator.
To define a model in ANDES, two classes, ModelData
and Model
need to be
utilized. Class ModelData
is used for defining parameters that will be
provided from input files. It provides API for adding data from devices and
managing the data. Class Model
is used for defining other non-input
parameters, service variables, and DAE variables. It provides API for converting
symbolic equations, storing Jacobian patterns, and updating equations.
The following classes are related to models:
|
Class for holding parameter data for a model. |
|
Base class for power system DAE models. |
Class for caching the return value of callback functions. |
|
Class for storing generated function calls, Jacobian calls, and arguments. |
Cache#
ModelData uses a lightweight class andes.core.model.ModelCache
for
caching its data as a dictionary or a pandas DataFrame. Four attributes are
defined in ModelData.cache:
dict: all data in a dictionary with the parameter names as keys and v values as arrays.
dict_in: the same as dict except that the values are from v_in, the original input.
df: all data in a pandas DataFrame.
df_in: the same as df except that the values are from v_in.
Other attributes can be added by registering with cache.add_callback.
- andes.core.model.ModelCache.add_callback(self, name: str, callback)
Add a cache attribute and a callback function for updating the attribute.
- Parameters:
- namestr
name of the cached function return value
- callbackcallable
callback function for updating the cached attribute
Define Voltage Ratings#
If a model is connected to an AC Bus or a DC Node, namely, if bus
, bus1
,
node
or node1
exists as parameter, it must provide the corresponding
parameter, Vn
, Vn1
, Vdcn
or Vdcn1
, for rated voltages.
Controllers not connected to Bus or Node will have its rated voltages omitted
and thus Vb = Vn = 1
, unless one uses andes.core.param.ExtParam
to retrieve the bus/node values.
As a rule of thumb, controllers not directly connected to the network shall use system-base per unit for voltage and current parameters. Controllers (such as a turbine governor) may inherit rated power from controlled models and thus power parameters will be converted consistently.
Define a DAE Model#
- class andes.core.model.Model(system=None, config=None)[source]
Base class for power system DAE models.
After subclassing ModelData, subclass Model` to complete a DAE model. Subclasses of Model define DAE variables, services, and other types of parameters, in the constructor
__init__
.- Attributes:
- num_paramsOrderedDict
{name: instance} of numerical parameters, including internal and external ones
Examples
Take the static PQ as an example, the subclass of Model, PQ, should look like
class PQ(PQData, Model): def __init__(self, system, config): PQData.__init__(self) Model.__init__(self, system, config)
Since PQ is calling the base class constructors, it is meant to be the final class and not further derived. It inherits from PQData and Model and must call constructors in the order of PQData and Model. If the derived class of Model needs to be further derived, it should only derive from Model and use a name ending with Base. See
andes.models.synchronous.genbase.GENBase
.Next, in PQ.__init__, set proper flags to indicate the routines in which the model will be used
self.flags.update({'pflow': True})
Currently, flags pflow and tds are supported. Both are False by default, meaning the model is neither used in power flow nor in time-domain simulation. A very common pitfall is forgetting to set the flag.
Next, the group name can be provided. A group is a collection of models with common parameters and variables. Devices' idx of all models in the same group must be unique. To provide a group name, use
self.group = 'StaticLoad'
The group name must be an existing class name in
andes.models.group
. The model will be added to the specified group and subject to the variable and parameter policy of the group. If not provided with a group class name, the model will be placed in the Undefined group.Next, additional configuration flags can be added. Configuration flags for models are load-time variables, specifying the behavior of a model. They can be exported to an andes.rc file and automatically loaded when creating the System. Configuration flags can be used in equation strings, as long as they are numerical values. To add config flags, use
self.config.add(OrderedDict((('pq2z', 1), )))
It is recommended to use OrderedDict instead of dict, although the syntax is verbose. Note that booleans should be provided as integers (1 or 0), since True or False is interpreted as a string when loaded from the rc file and will cause an error.
Next, it's time for variables and equations! The PQ class does not have internal variables itself. It uses its bus parameter to fetch the corresponding a and v variables of buses. Equation wise, it imposes an active power and a reactive power load equation.
To define external variables from Bus, use
self.a = ExtAlgeb(model='Bus', src='a', indexer=self.bus, tex_name=r'\theta') self.v = ExtAlgeb(model='Bus', src='v', indexer=self.bus, tex_name=r'V')
Refer to the subsection Variables for more details.
The simplest PQ model will impose constant P and Q, coded as
self.a.e_str = "u * p" self.v.e_str = "u * q"
where the e_str attribute is the equation string attribute. u is the connectivity status. Any parameter, config, service or variable can be used in equation strings.
Three additional scalars can be used in equations: -
dae_t
for the current simulation time (can be used if the model has flag tds). -sys_f
for system frequency (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 implement a piece-wise power injection equation. It selects the original power demand if within range, and uses the calculated power when out of range.
Finally, to let ANDES pick up the model, the model name needs to be added to models/__init__.py. Follow the examples in the OrderedDict, where the key is the file name, and the value is the class name.
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.SymProcessor(parent)[source]
A helper class for symbolic processing and code generation.
- Parameters:
- parentModel
The Model instance to process
- Attributes:
- xysympy.Matrix
variables pretty print in the order of State, ExtState, Algeb, ExtAlgeb
- fsympy.Matrix
differential equations pretty print
- gsympy.Matrix
algebraic equations pretty print
- dfsympy.SparseMatrix
df /d (xy) pretty print
- dgsympy.SparseMatrix
dg /d (xy) pretty print
- inputs_dictOrderedDict
All possible symbols in equations, including variables, parameters, discrete flags, and config flags. It has the same variables as what
get_inputs()
returns.- vars_dictOrderedDict
variable-only symbols, which are useful when getting the Jacobian matrices.
- generate_equations()[source]
Generate equations.
The pretty-print equations in matrices can be accessed in
self.f_matrix
andself.g_matrix
.
- generate_init()[source]
Generate initialization equations.
- generate_jacobians(diag_eps=1e-08)[source]
Generate Jacobians and store to corresponding triplets.
The internal indices of equations and variables are stored, alongside the lambda functions.
For example, dg/dy is a sparse matrix whose elements are
(row, col, val)
, 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_dictOrderedDict
name-symbol pair of all parameters, variables and configs
- vars_dictOrderedDict
name-symbol pair of all variables, in the order of (states_and_ext + algebs_and_ext)
Next, function generate_equation
converts each DAE equation set to one
numerical function calls and store it in Model.calls
. The attributes for
differential equation set and algebraic equation set are f
and g
.
Differently, service variables will be generated one by one and store in an
OrderedDict
in Model.calls.s
.
Jacobian Storage#
Abstract Jacobian Storage#
Using the .jacobian
method on sympy.Matrix
, the symbolic Jacobians can
be easily obtained. The complexity lies in the storage of the Jacobian elements.
Observed that the Jacobian equation generation happens before any system is
loaded, thus only the variable indices in the variable array is available. For
each non-zero item in each Jacobian matrix, ANDES stores the equation index,
variable index, and the Jacobian value (either a constant number or a callable
function returning an array).
Note that, again, a non-zero entry in a Jacobian matrix can be either a constant or an expression. For efficiency, constant numbers and lambdified callables are stored separately. Constant numbers, therefore, can be loaded into the sparse matrix pattern when a particular system is given.
Warning
Data structure for the Jacobian storage has changed. Pending documentation
update. Please check andes.core.common.JacTriplet
class for more
details.
The triplets, the equation (row) index, variable (column) index, and values
(constant numbers or callable) are stored in Model
attributes with the name
of _{i, j, v}{Jacobian Name}{c or None}
, where {i, j, v}
is a single
character for row, column or value, {Jacobian Name}
is a two-character
Jacobian name chosen from fx, fy, gx, and gy
, and {c or None}
is either
character c
or no character, indicating whether it corresponds to the
constants or non-constants in the Jacobian.
For example, the triplets for the constants in Jacobian gy
are stored in
_igyc
, _jgyc
, and _vgyc
.
In terms of the non-constant entries in Jacobians, the callable functions are
stored in the corresponding _v{Jacobian Name}
array. Note the differences
between, for example, _vgy
an _vgyc
: _vgy
is a list of callables,
while _vgyc
is a list of constant numbers.
Concrete Jacobian Storage#
When a specific system is loaded and the addresses are assigned to variables,
the abstract Jacobian triplets, more specifically, the rows and columns, are
replaced with the array of addresses. The new addresses and values will be
stored in Model
attributes with the names {i, j, v}{Jacobian Name}{c or
None}
. Note that there is no underscore for the concrete Jacobian triplets.
For example, if model PV
has a list of variables [p, q, a, v]
. The
equation associated with p
is - u * p0
, and the equation associated with
q
is u * (v0 - v)
. Therefore, the derivative of equation v0 - v
over
v
is -u
. Note that u
is unknown at generation time, thus the value
is NOT a constant and should to go vgy
.
The values in _igy
, _jgy
and _vgy
contains, respectively, 1
,
3
, and a lambda function which returns -u
.
When a specific system is loaded, for example, a 5-bus system, the addresses for
the q
and v
are [11, 13, 15
, and [5, 7, 9]
. PV.igy
and
PV.jgy
will thus query the corresponding address list based on PV._igy
and PV._jgy
and store [11, 13, 15
, and [5, 7, 9]
.
Initialization#
Value providers such as services and DAE variables need to be initialized. Services are initialized before any DAE variable. Both Services and DAE Variables are initialized sequentially in the order of declaration.
Each Service, in addition to the standard v_str
for symbolic initialization,
provides a v_numeric
hook for specifying a custom function for
initialization. Custom initialization functions for DAE variables, are lumped in
a single function in Model.v_numeric
.
ANDES has an experimental Newton-Krylov method based iterative initialization.
All DAE variables with v_iter
will be initialized using the iterative
approach
Additional Numerical Equations#
Addition numerical equations are allowed to complete the "hybrid symbolic-numeric" framework. Numerical function calls are useful when the model DAE is non-standard or hard to be generalized. Since the symbolic-to-numeric generation is an additional layer on top of the numerical simulation, it is fundamentally the same as user-provided numerical function calls.
ANDES provides the following hook functions in each Model
subclass for
custom numerical functions:
v_numeric
: custom initialization 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.