Build and run a PSCAD case from Python

A PSCAD case is not a data file a solver reads: it is Fortran that EMTDC compiles and then runs. So a script has two steps where other simulators have one, a build with errors of its own and a run, and then a third: reading back the channels the run wrote. This page covers all three, and what goes wrong in each.

What building and running a case means

When PSCAD builds a case it writes each page of the model out as Fortran, compiles it with the Fortran compiler PSCAD is set to (GFortran or Intel), and links an executable. The run is that executable: EMTDC steps the network in the time domain and writes the signals that have an output channel on them to result files.

Three things follow for a script. A build can fail where nothing has run yet, with errors that name a component and a page. A run can stop before its end time, which is a result of the study, not a crash of the script. And the only results there are come from output channels: a signal with no output channel on it is not recorded, whatever the graphs show while PSCAD is open.

The method

  1. Build with project.build(), or clean() first to throw the old build away. Then read project.messages(): PSCAD replaces the list at the start of every build and run, so what is there is this build's.
  2. Set the run: duration, time step and plot step in the project's settings.
  3. Run with project.run(), which builds first if the case needs it.
  4. Read the channels from the .out files in the build folder, by the names the .inf index gives them.

Building, and reading the errors

project.clean()                         # optional: start the build from nothing
project.build()
for m in project.messages():            # this build's messages only
    print(m.status, m.text)             # its status (error, warning, normal) and text

The Builder's runtime wraps this as px_build(app, clean=False), which gives back .ok, .errors, .warnings and .seconds, and remembers that the case is built so a loop does not compile it again on every pass.

The run settings

SettingUnitWhat it does
durationsHow long the run simulates. With a snapshot start it carries on from the snapshot time.
stepusThe EMTDC solution time step. 50 us is usual for a network; power electronics wants 1 to 10 us.
plot_stepusHow often a channel is written. It sets the highest frequency the recorded waveform holds, and the size of the files.
build·"auto" builds once per script run, "always" before every run, "never" not at all.
read·"out" reads the .out files, "curves" the samples off the case's graphs, "none" nothing.
run = px_run(app, duration=2.0, step=50, plot_step=250)
print(run.status)        # "ok", "early" (stopped before the end) or "failed" (did not build or start)
print(run.t_end)         # the last time written, s
print(run.names())       # every channel the run recorded

Reading a channel

EMTDC writes ten channels to each .out file, with a .inf index that names them in order. A three phase channel becomes three, named after its title with :1, :2 and :3. run.series(name) finds the right column in the right file and gives back the waveform in the channel's own unit; a name the run did not record gives an empty series and a warning that lists the closest names it did record.

va = run.series("Vbus:1")           # phase A of a three phase channel titled Vbus
print(len(va), va.unit, va.y[-1])   # samples, unit, the last value
worst = peak(va, kind="abs")        # .value, .time

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 Build the case Quick Start compiles the case and writes one table row either way: built, with the time and the warnings, or not built, with the compiler's errors. It is the first file to run on a case you have not scripted before. This is the file with the case set to C:/Studies/feeder/feeder.pscx:

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:/Studies/feeder/feeder.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: feeder.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

  • px_build(app, None, True, False) is a clean build: the previous one is thrown away first.
  • The block's Built and Did not build branches each write their own row into the same table, so the table says what happened even when the case does not compile.
  • The errors column holds PSCAD's own messages, which name the component and the page. A floating input, a signal used before it is defined and a missing library are the usual ones.
  • Nothing is run: this is the quick check. The other Quick Starts call px_run, which builds once and then runs.

Pitfalls

  • A signal shown on a graph is not necessarily recorded. Only signals with an output channel are written to the result files. Add the channel in the case, or the script reads nothing for it.
  • The plot step limits the waveform. A 250 us plot step holds frequencies up to about 2 kHz; anything faster in the run is not in the file. Lower it for harmonics and switching, at the cost of larger files.
  • A run that stops early is a result. EMTDC stops when the network has no solution or a component asks it to, and the script gets status "early" with the time it reached. Record it rather than treating it as an error.
  • Old result files are read as new ones if a run fails before it writes. The runtime clears the case's .out files before each run so a failed run reads as empty, not as the previous run.
  • A build and a run that works, then "is not recognized". When NoDefaultCurrentDirectoryInExePath is set in the environment, the batch files EMTDC generates cannot start the case's own executable. The generated scripts clear it.

Questions

How do I build a PSCAD project from Python?

Call build() on the project, after clean() when you want a full rebuild, then read messages() for the errors and warnings. run() builds first when the case needs it, so a script that only runs does not have to build separately.

Where does PSCAD write the simulation results?

To .out files in the case's build folder, ten channels to a file, with a .inf file that names each channel in order. The folder is the case's build folder beside the .pscx, whose name ends in the compiler's (.gf46 for GFortran 4.6); the project's temp_folder gives its path.

Why is my channel missing from the results?

It has no output channel component on it, it is disabled, or its title is not the name the script asks for. run.names() lists every channel the run recorded.

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.