System#
Overview#
System is the top-level class for organizing power system models and
orchestrating calculations. The full API reference of System is found at
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
models/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 routines will be stored to dictionary
System.routines
.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 models whose equations are provided in strings need be processed to generate executable functions for simulations. We call this process "code generation". Code generation utilizes SymPy, a symbolic toolbox, and can take up to one minute.
Code generation is automatically triggered upon the first ANDES run or whenever model changes are detected. Code generation only needs to run once unless the generated code is removed or model edits are detected. The generated code is then stored and reused for speed up.
The generated Python code is called pycode
. It is a Python package (folder)
with each module (a .py file) storing the executable Python code and metadata
for numerical simulation. The default path to store pycode
is
HOME_DIR/.andes
, where HOME_DIR
is one's home directory.
Note
Code generation has been done if one has executed andes
,
andes selftest
, or andes prepare
.
Warning
For developers: when models are modified (such as adding new models or
changing equation strings), code generation needs to be executed again
for consistency. ANDES can automatically detect changes, and it can be
manually triggered from command line using andes prepare -i
.
- andes.system.System.prepare(self, quick=False, incremental=False, models=None, nomp=False, ncpu=1)
Generate numerical functions from symbolically defined models.
All procedures in this function must be independent of test case.
- Parameters:
- quickbool, optional
True to skip pretty-print generation to reduce code generation time.
- incrementalbool, optional
True to generate only for modified models, incrementally.
- modelslist, OrderedDict, None
List or OrderedList of models to prepare
- nompbool
True to disable multiprocessing
Warning
Generated lambda functions will be serialized to file, but pretty prints (SymPy objects) can only exist in the System instance on which prepare is called.
Notes
Option
incremental
compares the md5 checksum of all var and service strings, and only regenerate for updated models.Examples
If one needs to print out LaTeX-formatted equations in a Jupyter Notebook, one need to generate such equations with
import andes sys = andes.prepare()
Alternatively, one can explicitly create a System and generate the code
import andes sys = andes.System() sys.prepare()
- andes.system.System.undill(self, autogen_stale=True)
Reload generated function functions, from either the
$HOME/.andes/pycode
folder.If no change is made to models, future calls to
prepare()
can be replaced withundill()
for acceleration.- Parameters:
- autogen_stale: bool
True to automatically call code generation if stale code is detected. Regardless of this option, codegen is trigger if importing existing code fails.
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 state 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: 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: 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: 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: 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: OrderedDict, niter=0, 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: 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: OrderedDict, init=False, niter=0)
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: 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: 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.save_config(self, file_path=None, overwrite=False)
Save all system, model, and routine configurations to an rc-formatted file.
- Parameters:
- file_pathstr, optional
path to the configuration file default to ~/andes/andes.rc.
- overwritebool, 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.
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.