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 dictionary System.models with model names as keys and the corresponding instances as values.

Examples

system.Bus stores the Bus object, and system.GENCLS stores the classical generator object,

system.models['Bus'] points the same instance as system.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, and System.TDS and System.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 with undill() 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 as kvxopt.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: 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.