Scripting#

This section is a tutorial for using ANDES in an interactive/scripting environment. All scripting shells are supported, including Python shell, IPython, Jupyter Notebook, and Jupyter Lab. The examples below use Jupyter Notebook.

Jupyter Notebook#

Jupyter Notebook is a convenient tool to run Python code and present results. Jupyter Notebook can be installed with

conda install jupyter notebook

After the installation, change the directory to the folder where you wish to store notebooks, then start the notebook with

jupyter notebook

A browser window should open automatically with the notebook browser loaded. To create a new notebook, use the "New" button near the upper-right corner.

Note

In the following, code blocks starting with >>> are Python code and should be executed inside Python, IPython, or Jupyter Notebook. Python code should not be entered into Anaconda Prompt or Linux shell.

Import#

Like other Python libraries, ANDES needs to be imported into an interactive scripting Python environment.

>>> import andes
>>> andes.config_logger()

Verbosity#

If you are debugging ANDES, you can enable debug messages with

>>> andes.config_logger(stream_level=10)

or simply

>>> andes.config_logger(10)

The stream_level uses the same verbosity levels as for the command line. If not explicitly enabled, the default level 20 (INFO) will apply.

To set a new logging level for the current session, call config_logger with the desired new levels.

Making a System#

Before running studies, an andes.system.System object needs to be created to hold the system data. The System object can be created by passing the path to the case file to the entry-point function.

There are multiple ways to create the System object. Among them, andes.main.run is the most convenient way. For example, to run the file kundur_full.xlsx in the same directory as the notebook, use

>>> ss = andes.run('kundur_full.xlsx')

This function will parse the input file, run the power flow, and return the system as an object. Outputs will look like

Parsing input file </Users/user/notebooks/kundur/kundur_full.xlsx>
Input file kundur_full.xlsx parsed in 0.4172 second.
-> Power flow calculation with Newton Raphson method:
0: |F(x)| = 14.9283
1: |F(x)| = 3.60859
2: |F(x)| = 0.170093
3: |F(x)| = 0.00203827
4: |F(x)| = 3.76414e-07
Converged in 5 iterations in 0.0222 second.
Report saved to </Users/user/notebooks/kundur_full_out.txt> in 0.0015 second.
-> Single process finished in 0.4677 second.

In this example, ss is an instance of andes.System. It contains member attributes for models, routines, and numerical DAE.

The naming convention for the System attributes is as follows

  • Model attributes share the same name as class names. For example, ss.Bus is the Bus instance, and ss.GENROU is the GENROU instance.

  • Routine attributes share the same name as class names. For example, ss.PFlow and ss.TDS are the routine instances.

  • The numerical DAE instance is in lower case ss.dae.

To work with PSS/E inputs, refer to Examples - "Working with Data".

Note

andes.main.run can accept multiple input files for multiprocessing. They can be passed as a list of strings to the first positional argument.

Passing options#

andes.run() can accept options that are available to the command-line andes run. Options need to be passed as keyword arguments to andes.run() in addition to the positional argument for the test case. For example, setting no_output to True will disable all file outputs. When scripting, one can do

>>> ss = andes.run('kundur_full.xlsx', no_output=True)

which is equivalent to the following shell command:

andes run kundur_full.xlsx --no-output

Please note that the dash between no and output needs to be replaced with an underscore for scripting. This is the convention in Python's argument parser.

Another example is to specify a folder for output files. By default, outputs will be saved to the folder where Python is run (or where the notebook is run). In case you need to organize outputs, a path prefix can be passed to andes.run() through output_path:

>>> ss = andes.run('kundur_full.xlsx', output_path='outputs/')

which will put outputs into folder outputs relative to the current path. You can also supply an absolute path to output_path.

The next example is to specify the simulation time for a time-domain simulation. There are multiple ways to implement it (see Examples), and one way is to pass the end time (in sec) through argument tf and set the routine to tds:

>>> ss = andes.run('kundur_full.xlsx', routine='tds', tf=5)

which will set the simulation time to 5 seconds.

Note

While andes run accepts single-letter alias for the option, such as andes run -n for andes run --no-output, andes.run() can only work with the full option name (with hyphen replaced by underscore)

Load Only#

In many workflows, one will simulate many scenarios with largely identical system data. A base case can be loaded and modified to create scenarios in memory. See Example "Working with Data" for details

Inspecting Parameter#

DataFrame#

Parameters for the loaded system can be readily inspected in Jupyter Notebook using Pandas.

Parameters for a model instance can be retrieved in a DataFrame using the as_df() method on the model instance. For example, to view the parameters of Bus, use

>>> ss.Bus.as_df()

A table will be printed with the columns being parameters and the rows being Bus devices/instances. For a system that has been setup, parameters have been converted to per unit values in the system base specified by ss.config.mva. The per-unit values in the system base will be used in computation as all computation in ANDES uses system-base per-unit data.

To view the original input values, use the as_df(vin=True) method. For example, to view the system-base per unit value of GENROU, use

>>> ss.GENROU.as_df(vin=True)

Parameter in the table is the same as that in the input file without any conversion. Some input data, by convention, are given as per unit in the device base; see Per Unit System for details.

Note that andes.core.modeldata.ModelData.as_df() returns a view. Modifying the returned dataframe will not affect the original data used for simulation. To modify the data, see Example "Working with Data".

Running Studies#

Three routines are currently supported: PFlow, TDS and EIG. Each routine provides a run() method to execute. The System instance contains member attributes having the same names. For example, to run the time-domain simulation for ss, use

>>> ss.TDS.run()

To change configuration for routines, one can set the attribute before calling run. For example, to change the end time to 5 sec, one can do

>>> ss.TDS.config.tf = 5
>>> ss.TDS.run()

Note that not all config changes are respected. Some config values are used while creating the routine instance. For config changes that does not necessarily have to be done on-the-fly, it is recommended to edit the config file.

Checking Exit Code#

andes.System contains field exit_code for checking if error occurred in run time. A normal completion without error should always have exit_code == 0. One should read output messages carefully and check the exit code, which is particularly useful for batch simulations.

Error may occur in any phase - data parsing, power flow, or simulation. To diagnose, split the simulation steps and check the outputs from each one.

Plotting TDS Results#

TDS comes with a plotting utility for scripting usage. After running the simulation, a plotter attributed will be created for TDS. To use the plotter, provide the attribute instance of the variable to plot. For example, to plot all the generator speed, use

>>> ss.TDS.plotter.plot(ss.GENROU.omega)

Note

If you see the error

AttributeError: 'NoneType' object has no attribute 'plot'

You will need to manually load plotter with

>>> ss.TDS.load_plotter()

Optional indices is accepted to choose the specific elements to plot. It can be passed as a tuple through the a argument

>>> ss.TDS.plotter.plot(ss.GENROU.omega, a=(0, ))

In the above example, the speed of the "zero-th" generator will be plotted.

Scaling#

A lambda function can be passed to argument ycalc to scale the values. This is useful to convert a per-unit variable to nominal. For example, to plot generator speed in Hertz, use

>>> ss.TDS.plotter.plot(ss.GENROU.omega, a=(0, ),
                        ycalc=lambda x: 60*x,
                        )

Formatting#

A few formatting arguments are supported:

  • grid = True to turn on grid display

  • greyscale = True to switch to greyscale

  • ylabel takes a string for the y-axis label

Extracting Data#

One can extract data from ANDES for custom plotting. Variable names can be extracted from the following fields of ss.dae:

Un-formatted names (non-LaTeX):

  • x_name: state variable names

  • y_name: algebraic variable names

  • xy_name: state variable names followed by algebraic ones

LaTeX-formatted names:

  • x_tex_name: state variable names

  • y_tex_name: algebraic variable names

  • xy_tex_name: state variable names followed by algebraic ones

These lists only contain the variable names used in the current analysis routine. If you only ran power flow, ss.dae.y_name will only contain the power flow algebraic variables, and ss.dae.x_name will likely be empty. After initializing time-domain simulation, these lists will be extended to include all variables used by TDS.

In case you want to extract the discontinuous flags from TDS, you can set store_z to 1 in the config file under section [TDS]. When enabled, discontinuous flag names will be populated at

  • ss.dae.z_name: discontinuous flag names

  • ss.dae.z_tex_name: LaTeX-formatted discontinuous flag names

If not enabled, both lists will be empty.

Power flow solutions#

The full power flow solutions are stored at ss.dae.xy after running power flow (and before initializing dynamic models). You can extract values from ss.dae.xy, which corresponds to the names in ss.dae.xy_name or ss.dae.xy_tex_name.

If you want to extract variables from a particular model, for example, bus voltages, you can directly access the v field of that variable

>>> import numpy as np
>>> voltages = np.array(ss.Bus.v.v)

which stores a copy of the bus voltage values. Note that the first v is the voltage variable of Bus, and the second v stands for value. It is important to make a copy by using np.array() to avoid accidental changes to the solutions.

If you want to extract bus voltage phase angles, do

>>> angle = np.array(ss.Bus.a.v)

where a is the field name for voltage angle.

To find out names of variables in a model, use command andes doc or refer to Model reference.

Time-domain data#

Time-domain simulation data will be ready when simulation completes. It is stored in ss.dae.ts, which has the following fields:

  • txyz: a two-dimensional array. The first column is time stamps, and the following are variables. Each row contains all variables for that time step.

  • t: all time stamps.

  • x: all state variables (one column per variable).

  • y: all algebraic variables (one column per variable).

  • z: all discontinuous flags (if enabled, one column per flag).

If you want the output in pandas DataFrame, call

ss.dae.ts.unpack(df=True)

Dataframes are stored in the following fields of ss.dae.ts:

  • df: dataframe for states and algebraic variables

  • df_z: dataframe for discontinuous flags (if enabled)

For both dataframes, time is the index column, and each column correspond to one variable.

Note

Looking to extract data for a single variable? See Examples - "Working with Data".

Pretty Print of Equations#

Each ANDES models offers pretty print of \(\LaTeX\)-formatted equations in the jupyter notebook environment.

To use this feature, symbolic equations need to be generated in the current session using

import andes ss = andes.System() ss.prepare()

Or, more concisely, one can do

import andes ss = andes.prepare()

This process may take a few minutes to complete. To save time, you can selectively generate it only for interested models. For example, to generate for the classical generator model GENCLS, do

import andes ss = andes.System() ss.GENROU.prepare()

Once done, equations can be viewed by accessing ss.<ModelName>.syms.<PrintName>, where <ModelName> is the model name, and <PrintName> is the equation or Jacobian name.

Note

Pretty print only works for the particular System instance whose prepare() method is called. In the above example, pretty print only works for ss after calling prepare().

Supported equation names include the following:

  • xy: variables in the order of State, ExtState, Algeb and ExtAlgeb

  • f: the right-hand side of differential equations \(\mathbf{M} \dot{\mathbf{x}} = \mathbf{f}\)

  • g: implicit algebraic equations \(0 = \mathbf{g}\)

  • df: derivatives of f over all variables xy

  • dg: derivatives of g over all variables xy

  • s: the value equations for ConstService

For example, to print the algebraic equations of model GENCLS, one can use ss.GENCLS.syms.g.

Finding Help#

docstring#

To find out how a Python class, method, or function should be used, use the built-in help() function. This will print out the docstring of the class/method/function. For example, to check how the get method of GENROU should be called, do

help(ss.GENROU.get)

In Jupyter notebook, this can be simplified into ?ss.GENROU.get or ss.GENROU.get?.

Please report issues if you find missing docstring.

Model docs#

Model docs can be shown by printing the return of doc(). For example, to check the docs of GENCLS, do

print(ss.GENCLS.doc())

It is the same as calling andes doc GENCLS from the command line. Likewise, a pretty-print version is available online in Model reference.