System Architecture#
The System class is the central orchestrator in ANDES, managing models, routines, and DAE data structures. The full API reference is found at andes.system.System.
System Overview#
import andes
ss = andes.load('case.xlsx')
# ss is a System instance containing:
# - All loaded models (ss.Bus, ss.PQ, ss.GENROU, etc.)
# - Analysis routines (ss.PFlow, ss.TDS, ss.EIG)
# - DAE storage (ss.dae)
# - Configuration (ss.config)
Dynamic Imports#
System dynamically imports groups, models, and routines at creation. To add new models, groups or routines, edit the corresponding registration file following existing examples.
System
├── Groups (StaticLoad, Generator, Exciter, ...)
├── Models (Bus, PQ, GENROU, ESST1A, ...)
└── Routines (PFlow, TDS, EIG)
- andes.system.System.import_models(self)
Delegate to
andes.system.registry.RegistryLoader.
- andes.system.System.import_groups(self)
Delegate to
andes.system.registry.RegistryLoader.
- andes.system.System.import_routines(self)
Delegate to
andes.system.registry.RegistryLoader.
Code Generation#
Under the hood, all models whose equations are provided in strings need to be processed to generate executable functions for simulations. We call this process "code generation". Code generation utilizes SymPy and can take up to one minute.
Code generation is automatically triggered upon the first ANDES run or whenever model changes are detected. The generated code is 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 storage path is ~/.andes/.
Note
Code generation has been done if you have 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=2)
Delegate to
andes.system.codegen.CodegenManager.
- andes.system.System.undill(self, autogen_stale=True)
Delegate to
andes.system.codegen.CodegenManager.
DAE Storage#
The numerical DAE arrays are stored in System.dae:
ss.dae.x # State variables (differential)
ss.dae.y # Algebraic variables
ss.dae.f # Differential equations (dx/dt)
ss.dae.g # Algebraic equations (= 0)
ss.dae.t # Current time
Time Series#
After TDS, use get_timeseries() to extract results as a pandas DataFrame:
omega_df = ss.TDS.get_timeseries(ss.GENROU.omega) # State variable
voltage_df = ss.TDS.get_timeseries(ss.Bus.v) # Algebraic variable
The underlying arrays are also available for direct access:
ss.dae.ts.t # Time points
ss.dae.ts.x # State history (n_steps x n_states)
ss.dae.ts.y # Algebraic history (n_steps x n_algebs)
- 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.spmatrixsparse 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.
Device Connectivity#
Use find_connected() to find all devices referencing a given bus or device:
ss.find_connected('Bus', 1)
# OrderedDict([('Slack', [1]), ('Line', ['Line_1', 'Line_2']),
# ('GENROU', ['GENROU_1']), ('BusFreq', ['BusFreq_2'])])
This works for any model or group, not just buses:
ss.find_connected('SynGen', 'GENROU_1')
# OrderedDict([('TGOV1', ['TGOV1_1']), ('ESST3A', ['ESST3A_2'])])
- andes.system.System.find_connected(self, model_or_group, idx)
Find all devices connected to a given device.
Scans
IdxParamreferences across all models to find devices that point to the specified target device.- Parameters:
- model_or_groupstr
Model name (e.g.,
'Bus') or group name (e.g.,'ACNode').- idxstr, int, float
Device idx to query.
- Returns:
- OrderedDict
{model_name: [idx, ...]}for each model with at least one device referencing the target. Empty models are omitted.
Examples
Find all devices connected to Bus 1:
ss.find_connected('Bus', 1) # OrderedDict([('PQ', [1]), ('Line', [1, 4]), ('GENROU', [1])])
Decentralized Architecture#
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 back to each model.
Models own their data: Each model has local copies of variable values
System orchestrates: Collects/distributes values between models and DAE
Parallel-friendly: Model equations can be evaluated independently
Data Flow#
┌─────────┐ ┌─────────┐ ┌─────────┐
│ Model │ ──────► │ DAE │ ◄────── │ Model │
│ (PQ) │ │ arrays │ │ (Gen) │
└─────────┘ └─────────┘ └─────────┘
│ ▲ │
│ │ │
└──────────────────┼───────────────────┘
│
┌────┴────┐
│ System │
│ methods │
└─────────┘
The collection of values from models needs to follow protocols to avoid conflicts. Details are given in the Variables section.
- 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.
Jacobian Matrices#
System builds sparse Jacobian matrices incrementally:
ss.dae.fx # df/dx (differential w.r.t. states)
ss.dae.fy # df/dy (differential w.r.t. algebraic)
ss.dae.gx # dg/dx (algebraic w.r.t. states)
ss.dae.gy # dg/dy (algebraic w.r.t. algebraic)
Matrix Sparsity Patterns#
The largest overhead in building and solving nonlinear equations is the building of Jacobian matrices. This is especially relevant when using the implicit integration approach which algebraizes 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:
Approach 1: In-place addition
self.fx += spmatrix(v, i, j, (n, n), 'd')
Although simple, this involves creating and discarding temporary objects on the
right hand side and, worse, changing the sparse pattern of self.fx.
Approach 2: Collect and construct Store the rows, columns and values in array-like objects and construct the Jacobians at the end. Very efficient but does not allow accessing the sparse matrix while building.
ANDES approach: Pre-allocation
ANDES uses a pre-allocation approach to avoid changing sparse patterns by
filling values into a known sparse matrix pattern. 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 only modify existing
values, it does not change the pattern and thus avoids memory copying.
- 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.
Initialization#
- 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.
Equation Updates#
- 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_varof 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.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.l_update_eq(self, models: OrderedDict, init=False, niter=0)
Update equation-dependent limiter discrete components by calling
l_check_eqof models. Force set equations after evaluating equations.This function is must be called after differential equation updates.
- 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).
Model Calling Protocol#
System calls model methods in a defined order during each iteration:
model.init()- Initialize variablesmodel.l_update_var()- Update discrete flags (pre-equation)model.f_update()- Evaluate differential equationsmodel.g_update()- Evaluate algebraic equationsmodel.j_update()- Update Jacobian contributionsmodel.l_update_eq()- Update discrete flags (post-equation)
External Variable Protocol#
When models share variables (e.g., Bus voltage accessed by loads):
Flag |
Purpose |
|---|---|
|
Values at same address are summed |
|
This variable sets the final value |
|
Equation values are summed |
|
This equation sets the final value |
Example: PV generator sets bus voltage initial value:
# In PV model
self.v = ExtAlgeb(src='v', model='Bus',
indexer=self.bus,
v_str='v0',
v_setter=True) # Overwrite bus voltage
Configuration#
System, models and routines have a member attribute config for specific
configurations. System manages all configs, including saving to a config file
and loading back.
ss.config # System config
ss.PFlow.config # Power flow config
ss.TDS.config # TDS config
ss.GENROU.config # Model-specific config
- andes.system.System.save_config(self, file_path=None, overwrite=False)
Delegate to
andes.system.config_runtime.SystemConfigRuntime.
Warning
Configs from files are passed to model constructors during instantiation. If
you need to modify config for a run, it must be done before instantiating
System, or before running andes from command line. Directly modifying
Model.config may not take effect or have side effects in the current
implementation.
Device Lifecycle#
Understanding how devices are created and managed helps when extending cases programmatically or developing new models.
Data Loading Pipeline#
When loading from files (xlsx, JSON, PSS/E), ANDES follows this sequence:
File parsing: I/O readers (
andes/io/xlsx.py,json.py,psse.py) parse the input format into dictionariesDevice registration: Each row calls
System.add(model_name, param_dict):Validates the model exists
Gets a unique
idxfrom the device's groupCalls
Model.add()to store parameter valuesRegisters the device with its group
System setup:
System.setup()finalizes the structure (addresses, code generation)
# This is what happens internally during andes.load():
# (simplified from andes/io/xlsx.py)
for name, df in df_models.items():
for row in df.to_dict(orient='records'):
system.add(name, row) # Called once per device row
- andes.system.System.add(self, model_name, param_dict=None, **kwargs)
Add a device instance for an existing model.
This method calls the
addmethod of the model and registers the device idx to its group.Parameters can be passed as a dictionary, as keyword arguments, or both. When both are provided, keyword arguments are merged into the dictionary (kwargs take precedence on conflicts).
- Parameters:
- model_namestr
Name of the model (e.g.,
'Fault','Toggle','PQ').- param_dictdict, optional
Dictionary of parameter names to values.
- **kwargs
Parameter names and values as keyword arguments.
- Returns:
- idx
The assigned device index.
Examples
Keyword arguments are the preferred style:
ss.add('Fault', bus=5, tf=1.0, tc=1.1)
Models with a
modelparameter (e.g.,Alter,Toggle) can now use keyword arguments directly:ss.add('Alter', model='TGOV1', dev=1, src='paux0', t=1.0, method='=', amount=0.05) ss.add('Toggle', model='Line', dev='Line_5', t=1.0)
Programmatic Device Addition#
Devices can be added programmatically before setup():
ss = andes.load('case.xlsx', setup=False)
ss.add('Fault', bus=3, tf=1.0, tc=1.1)
ss.setup()
Key constraints:
Devices cannot be added after
setup()(raisesNotImplementedError)Referenced devices must exist (e.g.,
busmust reference a validBus.idx)See Parameters for mandatory vs. optional parameters and per-unit conventions
Timed Event Mechanism#
Disturbance devices (Fault, Toggle, Alter) use TimerParam to schedule callbacks during time-domain simulation. These models belong to the TimedEvent group.
How It Works#
TimerParamstores a time value and a callback functionDuring TDS, the solver checks if
dae.tmatches any timer valuesWhen triggered, the callback executes (e.g.,
Fault.apply_fault())The callback modifies system state (shunt admittance, device
uflag, parameter values)
# From andes/models/timer.py - Fault implementation
class Fault(ModelData, Model):
def __init__(self, system, config):
# ...
self.tf = TimerParam(info='Bus fault start time',
callback=self.apply_fault)
self.tc = TimerParam(info='Bus fault end time',
callback=self.clear_fault)
def apply_fault(self, is_time: np.ndarray):
"""Apply fault when t = tf."""
for i in range(self.n):
if is_time[i] and self.u.v[i]:
self.uf.v[i] = 1 # Enable fault equations
# Store pre-fault algebraic variables for restoration
return action
The fault equations inject a shunt admittance at the faulted bus:
self.a = ExtAlgeb(model='Bus', src='a', indexer=self.bus,
e_str='u * uf * (v ** 2 * gf)') # Active power
self.v = ExtAlgeb(model='Bus', src='v', indexer=self.bus,
e_str='-u * uf * (v ** 2 * bf)') # Reactive power
Available Timed Event Models#
Model |
Purpose |
Key Parameters |
|---|---|---|
|
Three-phase fault at a bus |
|
|
Switch device on/off |
|
|
Modify parameter value at runtime |
|
Fault: Applies a shunt impedance (
xf,rf) to ground at timetf, clears attcToggle: Negates the
u(connectivity) field of any device at timetAlter: Applies arithmetic operations (
+,-,*,/,=) to any parameter or service
For practical examples of adding disturbances, see Time-Domain Simulation.
See Also#
Hybrid Symbolic-Numeric Framework - Symbolic-numeric framework
Atomic Types - Value and equation providers
DAE Formulation - DAE mathematical details