PSCAD automation with Python: getting started

PSCAD is scripted through its Automation Library, a Python package called mhi.pscad that talks to PSCAD over its automation server. mhi.pscad.launch() starts a PSCAD, mhi.pscad.connect() attaches to one already open, and from there a case is opened, built, run and read. This page is the whole path from nothing to a first result, and the calls to learn on the way.

What PSCAD automation is

The Automation Library is not part of the PSCAD install. It is a Python package, published on PyPI as mhi-pscad, that connects to the automation server inside PSCAD and drives it: load a workspace, open a project, read and write a component's parameters, build, run, read the result files. Any Python 3.7 or newer can do it, and PSCAD itself needs a licence certificate before a run will start.

What matters most is what PSCAD is. PSCAD is an electromagnetic transient simulator, so there is no load flow to call, no short-circuit calculation and no frequency-domain harmonic scan. There is one calculation: build the case into Fortran, run it in the time domain, and read the waveforms it recorded. Everything a study asks for comes out of those waveforms.

That also changes what a script does to the model. There is no event list to fill: a fault, a trip or a set-point change is a component already drawn in the case, and a study changes when it happens by writing that component's parameters. A harmonic spectrum, an RMS voltage and a damping ratio are all read out of the recorded waveform afterwards.

So every PSCAD automation, whatever the study, is the same seven steps: connect, change, build, run, read, check, record.

The method

  1. Install the Automation Library into the Python you will run the script with: py -3.12 -m pip install mhi-pscad. The generated scripts check this before they connect, and end with the exact pip line when something is missing, rather than failing halfway through a study.
  2. Connect. px_connect() reads the script's own settings: RUN_MODE "auto" uses the PSCAD that is already open, starts one when there is none, and uses the host PSCAD when the file is run from inside it.
  3. Open the case. PROJECT is a .pscx path or the name of a case already in the workspace; WORKSPACE loads a .pswx first and LIBRARIES any .pslx the model's components live in.
  4. Find what you want to change by definition name, by the component's Name, or both.
  5. Read and write parameters, in the unit PSCAD holds them in.
  6. Build, because a PSCAD case is Fortran that EMTDC compiles before it can run.
  7. Run, then read a channel and take a number off it.

Connecting

These are the three ways in. A generated script does not call them directly: it calls px_connect(), which picks one from RUN_MODE, installs the compatibility shim an older PSCAD needs, times the attempt out, and turns a failure into a message that names the installed versions it found.

import mhi.pscad

pscad = mhi.pscad.launch(minimize=True, splash=False, timeout=120)  # start one
pscad = mhi.pscad.connect()                                         # or use the one that is open
pscad = mhi.pscad.application()                                     # or from inside PSCAD

print(pscad.version, mhi.pscad.VERSION)   # the PSCAD, and the Automation Library
print(pscad.licensed())                   # False means a run will not start

Opening the case and finding a component

app = px_connect()                      # opens PROJECT from SETTINGS

source = px_find(app, "master:source_1")           # exactly one, or an error listing what is there
resistor = px_find(app, "master:resistor", "Load") # a definition and a Name
channels = px_find(app, "master:pgb", match="all") # every output channel in the case

A definition name is written lower-case namespace first, as PSCAD writes it: master:source_1, master:resistor, master:tfaultn. A definition of the case itself is just its name. Either argument may hold * and ?. The component reference lists every Master Library definition with its parameters.

Reading and writing a parameter

PSCAD hands a parameter back as a plain number, as text, or as a value that carries its unit, whose text reads 2.5 [ohm]. px_get gives you the number and converts the unit when you ask for one; px_set writes in the unit the parameter already carries, so a resistance PSCAD shows in ohm stays in ohm.

vm = px_get(source, "Vm", unit="kV")      # a number, in kV
rs = px_get(source, "Rs", unit="ohm")

px_set(resistor, "R", 12.5, unit="ohm")   # recorded, so it can be put back
px_set(source, "Vm", 66.0, unit="kV")

Building and running

build = px_build(app, clean=False)
if not build.ok:
    for line in build.errors:
        log(line, "warn")
    raise SystemExit("the case does not build")

run = px_run(app, duration=0.2, step=50, plot_step=250)   # seconds, us, us
print(run.status, run.t_end, run.names())

step is the EMTDC time step and plot_step how often a channel is written, both in microseconds, as PSCAD shows them. 50 us and 250 us are the usual pair. px_run builds the case first when it needs it, so a script that only ever runs never calls px_build itself.

Reading a channel, then recording it

voltage = run.series("Mid Point Voltage")   # a Series: t in s, y in the channel's unit
worst = peak(voltage, kind="abs")           # .value and .time

table("First run").add({
    "Peak voltage": worst.value,
    "At (s)": worst.time,
    "Channel unit": voltage.unit,
})
finish(app)   # puts back what was changed, saves the table, writes the summary

The calls you use most

CallWhat it does
px_connect()Starts PSCAD or attaches to the one that is open, opens PROJECT and gives back the application. Reads the SETTINGS at the top of the script; calling it again gives the same application.
px_find(app, defn, name, match)Finds a component by its definition ("master:resistor"), by its Name, or by both. match "one" wants exactly one, "first" takes the first, "all" gives a list. No match is an error that lists the closest definitions and names in the case.
px_get(obj, param, unit=None)Reads one parameter: a number where it is one, else its text. unit converts a unit-carrying value, so a resistance PSCAD holds in ohm can be read in kohm.
px_set(obj, param, value, unit=None)Writes one parameter, in the unit the parameter already carries, and records the old value so it can be put back.
px_build(app, clean=False)Compiles the case with its Fortran compiler. Gives back a result with .ok, .errors, .warnings, .messages and .seconds.
px_run(app, duration, step, plot_step)Builds the case if it needs it, runs the EMT simulation and reads the channels back. duration is in seconds, step and plot_step in microseconds.
run.series("Vbus")One output channel of a run as a Series of time and value, in the channel unit the .inf index gives. A name the run did not record gives an empty Series and one warning, never an error.
set_fault(app, defn, name, at=, duration=)Writes the start time and the duration on a fault component in the model, and gives back (start, clear). PSCAD has no event list, so this is how a fault is timed.
sim_settings(app, duration=, step=, plot_step=)Writes the simulation settings of the case: duration in seconds, the time step and the plot step in microseconds, and the output format.
px_mark() and px_undo(mark)Opens a mark and puts back every parameter written since it. One pass of a loop sits between the two, so a sweep leaves the case as it found it.
table("FRT").add({...})Adds a row to a results table. The loop values are added for you, and the table is saved to the run folder as CSV or Excel when the run ends.
finish(app)The finally of the script: puts back every parameter, layer state and substitution the workflow changed, saves the tables, writes the summary.

These are the Builder's runtime helpers, the functions it copies into the file it writes. Each one is a thin layer over the Automation Library that adds the parts a study needs: the units, the error messages that name what the case actually holds, and the record of every change so the case is left as it was found.

Open this in the Builder. The Build the case and report its errors Quick Start loads in the PSCAD Workflow Builder, ready for your own case. Change any block and download the Python file.

Open in the Builder

The full Python script from the Builder

The Builder draws a workflow as blocks and writes one Python file from it. The simplest useful one is a build: compile the case and table what the compiler said. This is the file it writes, with the case set to vdiv.pscx, one of the tutorial examples that come with PSCAD:

build_the_case.py as the Builder writes it: the settings and the workflow⬇ Download the full file (.py, 1,498 lines)

#!/usr/bin/env python3
# Build the case
"""Made with the PSCAD Workflow Builder, https://tool.adjiebrotots.com/pscad-scripter/builder/

BEFORE YOU RUN
  1. Check the SETTINGS below (lines marked REQUIRED).
  2. Have PSCAD installed and licensed; the script opens one, or uses the one already open (see RUN_MODE).
  3. Run it with Python 3.7 or newer and mhi-pscad installed, e.g.  py -3.12 build_the_case.py
Results and run.log go to a new folder next to this file (or in OUTPUT_DIR).
"""
# ==== IMPORTS ====
import csv
import fnmatch
import importlib
import math
import os
import re
import sys
import time

# ==== SETTINGS (edit here) ====
# The name of this workflow: used for the run folder and in the log.
WORKFLOW_NAME = "Build the case"
# REQUIRED. The PSCAD case to work on: a .pscx path, or the name of a case already in the
# workspace.
PROJECT = "C:/Users/Public/Documents/PSCAD/5.0/Examples/tutorial/vdiv.pscx"
# OPTIONAL. A .pswx workspace to load before the case ("" = keep the one that is open).
WORKSPACE = ""
# OPTIONAL. Extra .pslx / .pscx libraries to load before the case.
LIBRARIES = []
# OPTIONAL. Where each run writes its folder of results and run.log ("" = the folder of this
# script).
OUTPUT_DIR = ""
# OPTIONAL. True: put every parameter, layer state and substitution the workflow changed back
# when it ends.
UNDO_CHANGES = True
# OPTIONAL. How the results tables are saved in the run folder when the run ends: "csv", "xlsx"
# (Excel, needs pandas and openpyxl) or "both".
TABLE_FORMAT = "csv"
# OPTIONAL. Nominal frequency in Hz for the frequency checks: 50, 60 or "auto" (read the case's
# own global substitution).
F_NOMINAL = "auto"
# OPTIONAL. "auto", "launch" (start a new PSCAD), "connect" (use the one that is open) or
# "inside" (run from inside PSCAD).
RUN_MODE = "auto"
# OPTIONAL. True: show the PSCAD window while running.
SHOW_PSCAD = False
# OPTIONAL. The Fortran compiler that builds the case, as PSCAD names it ("" = the one PSCAD has
# selected).
FORTRAN_VERSION = ""
# OPTIONAL. Which installed PSCAD to start ("" = the newest).
PSCAD_VERSION = ""
# OPTIONAL. The port PSCAD listens on for automation (0 = let PSCAD choose).
PSCAD_PORT = 0
# OPTIONAL. Seconds to wait for PSCAD to start.
CONNECT_TIMEOUT = 120

# True: connect, find every element named in this workflow, say what was found, then stop
# without running anything.
CHECK_ONLY = False


# ==== WORKFLOW ====
# Note: A PSCAD case is Fortran that EMTDC compiles before it can run, so a build has errors of its own: a floating input, a missing signal, a component that does not fit. This builds the case and writes w...
def main():
    preflight([])
    app = px_connect()
    try:
        if CHECK_ONLY:
            check_elements(app, [])
            return
        # -- Step 1: Start (Case: vdiv.pscx) --
        # -- Step 2: Build the case (clean, then build) --
        build = px_build(app, None, True, False)
        # Build the case: Did not build
        if not build.ok:
            # -- Step 3: Did not build (Into "Build", saved at the end automatically) --
            table("Build").add({
                "Built": build.ok,
                "Errors": build.errors,
            })
            return
        # -- Step 4: Add row to results table (Into "Build", saved at the end automatically) --
        table("Build").add({
            "Built": build.ok,
            "Seconds": build.seconds,
            "Warnings": build.warnings,
        })
    except StopWorkflow as stop:
        log(f"Stopped: {stop}")
    finally:
        finish(app)

The file goes on with 1,404 lines of helper functions copied from the Builder's runtime (connecting, building, running, reading the channels, the checks, tables, graphs, putting values back), only those this workflow needs. The download has them all.

How the script works

  • SETTINGS at the top are the only lines to edit. PROJECT is the case; RUN_MODE, PSCAD_VERSION and FORTRAN_VERSION say which PSCAD and which compiler to use; OUTPUT_DIR is where the run folder goes; TABLE_FORMAT is "csv", "xlsx" or "both".
  • preflight([]) runs before anything else: it imports every package the script needs and ends with the pip line for whatever is missing.
  • CHECK_ONLY = True connects, finds every component the workflow names, says what it found and stops without building or running. It is the safe first run on a case you have not scripted before.
  • px_build(app, None, True, False) is a clean build: the previous build is thrown away first, which is the honest thing after a component definition has changed.
  • The Built output of the build decides which table row is written, so a case that does not build still leaves a row, with the compiler errors in it, instead of a traceback.
  • finish(app) in the finally puts back everything the workflow changed, saves the results table into a new run folder and writes run.log beside it.

Pitfalls

  • An Automation Library from 2.3 on needs a shim to drive PSCAD 5.0.0. That PSCAD sends objects back in the mhi.pscad.common namespace, which the library moved to mhi.common. Without an alias the library's receive thread dies with ModuleNotFoundError on the first call that returns such an object (Project.scenarios() is one), and the whole connection goes with it. The generated scripts alias the modules before importing mhi.pscad, which costs nothing on a newer PSCAD.
  • pscad.settings() is not safe to depend on. Under Automation Library 3.x on an install with no ProductList.xml it raises, so nothing in the runtime reads or writes PSCAD's own settings through it. Set the compiler and the version when you connect instead.
  • A build that works and a run that does not. When NoDefaultCurrentDirectoryInExePath is set in the environment, PSCAD inherits it and so do the .bat files EMTDC generates, which call <case>.exe with no path. The case compiles and the run dies with "is not recognized". The generated scripts clear that variable for the run.
  • Everything a script writes is written in the case. There is no read-only session: px_set, enabling a component and changing a substitution all change the model on disk when it is saved. Keep UNDO_CHANGES = True, or work on a copy of the case.
  • A licence is checked at the run, not at the connect. app.licensed() false means PSCAD holds no certificate: the script connects and builds, then the run fails. Open PSCAD and check the licence, or acquire a certificate before running.
  • A built case leaves a build folder beside the .pscx. It holds the generated Fortran, the object files and the executable, and it is large. Keep cases you script outside a repository, or ignore the build folder.

Questions

How do I run PSCAD from Python?

Install the Automation Library with pip install mhi-pscad, then import mhi.pscad and either mhi.pscad.launch() to start a PSCAD or mhi.pscad.connect() to use the one that is open. Open a case on the application object, build it, run it, and read the channels the run recorded. The Workflow Builder writes all of that for you.

Can I run a load flow or a short circuit in PSCAD with Python?

No. PSCAD is an electromagnetic transient simulator: the only calculation is a time-domain run, so there is no load flow command and no short-circuit calculation to call. A fault level comes out of running a fault in the time domain and reading the current, and a steady state comes out of running until the case settles.

Which Python do I need for the PSCAD Automation Library?

Python 3.7 or newer, and 64-bit to match PSCAD's own bitness. The library is an ordinary pip package, so any interpreter you can pip into will do; it does not have to be a Python that ships with PSCAD.

Do I need a special licence to script PSCAD?

The automation server is part of PSCAD, so no separate scripting licence is needed, but PSCAD still needs a licence certificate before a simulation will start. app.licensed() says whether it holds one.

Related

Every parameter name a script writes is in the component reference, read out of PSCAD itself. The Workflow Builder writes the Python from blocks.