Sweep a PSCAD parameter with Python

A sweep is the simplest study there is: write a value, run, read, put it back, once for each value. PSCAD adds two ways of holding variants that a sweep can go over as well: layers, which switch whole groups of components in and out, and global substitution sets, which give every $(name) in the case a different value at once.

What a parameter sweep in PSCAD is

Every component in a PSCAD case has parameters, read and written from Python as a dictionary: comp.parameters() gives them, comp.parameters(R="12.5 [ohm]") writes one. A sweep writes one parameter over a range of values and runs the case for each, recording what an output channel did.

Two things make a sweep in PSCAD different from a hand edit. A value that carries a unit is written with it, and PSCAD keeps the unit it is given. And each write changes the case on disk when it is saved, so the script records each value it replaces and puts it back.

PSCAD's own Multiple Run component sweeps up to six variables inside one run of the executable. A Python sweep is slower to start each run, but it can change anything in the case, including what Multiple Run cannot reach: a layer, a substitution set, a component's definition, the run settings.

The method

  1. Find the component by definition and Name, and check the parameter's name and unit in the component reference.
  2. Loop over the values. Each pass opens a mark, writes the value, runs, reads, records a row, and undoes the mark.
  3. Graph the result against the swept value from the table.

One parameter over a range

resistor = px_find(app, "master:resistor", "Rload")
for r in num_range(1, 10, 1):
    mark = px_mark()
    px_set(resistor, "R", r, unit="ohm")      # written in the unit R already carries
    run = px_run(app, duration=1, step=50, plot_step=250)
    table("Sweep").add({"R (ohm)": r, "Peak": peak(run.series("Vbus"), kind="abs").value})
    px_undo(mark)
plot_table("Sweep", "R (ohm)", ["Peak"])

Every layer, one at a time

A layer is a named group of components that is enabled or disabled together, so one case can hold several network arrangements: a line in or out, a second transformer, a different filter. Going over the layers puts each one in while the others are out.

for name in each_layer(app, layer_list(app, "*"), "Enabled", "Disabled"):
    run = px_run(app, duration=2, step=50, plot_step=250)
    table("Arrangements").add({"Peak": peak(run.series("Vbus"), kind="abs").value})

Every global substitution set

A global substitution is a named value that any component parameter written as $(name) takes, and a set holds one value for each. Switching the active set changes every one of them at once, which is how one case covers several parameter sets: a weak and a strong grid, summer and winter.

for name in each_gs_set(app, [n for n in gs_sets(app) if n]):
    run = px_run(app, duration=2, step=50, plot_step=250)
    table("Sets").add({"Final value": final_value(run.series("Vbus"))})

Make the sets once in PSCAD, or load them from a CSV with gs_load. A case with no named sets has one set whose name is empty, which is why the loop leaves it out.

Open this in the Builder. The Sweep one parameter 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 scripts from the Builder

The Sweep one parameter Quick Start steps a resistor's R from 1 to 10, runs the case for each value and tables the peak of a channel, with a graph of the peak against the value. Change the component, the parameter and the channel to your own. This is the file with the case set to C:/Studies/filter/filter.pscx:

sweep_one_parameter.py as the Builder writes it: the settings and the workflow⬇ Download the full file (.py, 3,383 lines)

#!/usr/bin/env python3
# Parameter sweep
"""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 parameter_sweep.py
Results and run.log go to a new folder next to this file (or in OUTPUT_DIR).
"""
# ==== IMPORTS ====
import bisect
import csv
import fnmatch
import importlib
import math
import os
import re
import sys
import time
import traceback
import warnings

# ==== SETTINGS (edit here) ====
# The name of this workflow: used for the run folder and in the log.
WORKFLOW_NAME = "Parameter sweep"
# REQUIRED. The PSCAD case to work on: a .pscx path, or the name of a case already in the
# workspace.
PROJECT = "C:/Studies/filter/filter.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

# How every graph of this run looks: set in the Builder (select nothing, then Graph style).
# Colours are hex codes. A value that cannot be used falls back to the Adjie Brotools style.
GRAPH_STYLE = {
    "template": "brotools",                         # where this style started (a name only)
    "colors": ["#5A91E8", "#B45309", "#4A9E87", "#C46A7E", "#9B59B6"],  # in order; more lines repeat them, paler
    "ramp": "blue",                                 # blue, night, viridis, cividis, magma, coolwarm, greys, classic
    "background": "#FFFFFF",
    "plot_background": "#FFFFFF",
    "text": "#14203A",
    "muted": "#4C5B76",
    "grid": "#C8D4E8",
    "axis": "#C8D4E8",
    "font": "DM Sans",
    "font_size": 9.5,                               # points, for the axis numbers (the rest follows)
    "title_size": 13,                               # points
    "title_bold": True,                             # True or False
    "title_align": "center",                        # center, left
    "grid_axes": "both",                            # both, y, x, none
    "grid_style": "solid",                          # solid, dashed, dotted
    "frame": "left-bottom",                         # left-bottom, box, bottom, none
    "ticks": "out",                                 # out, in, none
    "thousands": True,                              # True: 1,200 rather than 1200
    "line_width": 2,                                # points
    "markers": "auto",                              # auto, always, never
    "line_styles": "solid",                         # solid, vary
    "legend": "bottom",                             # bottom, top, right, inside, none
    "legend_frame": False,                          # True or False
    "width": 10,                                    # inches
    "height": 5.6,                                  # inches
    "dpi": 150,                                     # the PNG is width x dpi pixels wide
    "transparent": False,                           # True: no background (for slides)
    "also_save": "",                                # "", svg, pdf
}

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


# ==== WORKFLOW ====
# Note: Choose the component and the parameter to sweep (the Component block takes a definition such as master:resistor and, when the case has several, a Name), and the channel to read. Each pass writes th...
def main():
    preflight(["matplotlib"])
    app = px_connect()
    try:
        if CHECK_ONLY:
            check_elements(app, [("master:resistor", "", "one")])
            return
        # -- Step 1: Start (Case: filter.pscx) --
        # -- Step 2: Value (1 to 10, step 1) --
        value_values = num_range(1, 10, 1)
        for value_pass_no, value in enumerate(value_values, 1):
            ctx_push("Value", value, value_pass_no, len(value_values))
            mark = px_mark()
            try:
                # -- Step 3: Set parameter (master:resistor.R = 1) --
                resistor = px_find(app, "master:resistor", "", "one")  # Component
                px_set(resistor, "R", value, None)
                # -- Step 4: Run simulation (1 s, 50 us step) --
                run = px_run(app, 1, 50, 250, None, events=(), build="auto", read="out", keep_files=False)
                # Did not build: skips to the next pass of Value
                if not run.ok and (run.status == "failed"):
                    case_skipped("The case did not build")
                    continue
                # Stopped early: skips to the next pass of Value
                if not (run.ok or (run.status == "failed")):
                    case_skipped("The simulation stopped before the end")
                    continue
                # -- Step 5: Add row to results table (Into "Sweep", saved at the end automatically) --
                vbus = run.series("Vbus", "")  # Read a channel
                peak_result = peak(vbus, kind="abs", after=None, before=None)  # Peak
                table("Sweep").add({"Peak": peak_result.value}, status=True)
            except StopWorkflow:
                raise
            except Exception as err:
                case_failed(err)
            finally:
                try:
                    px_undo(mark)
                except Exception as fin:
                    log(f"Could not put things back after this pass: {fin}", "error")
                ctx_pop(["Sweep"])
        # -- Step 6: Draw graph from results table (Peak against Value) --
        plot_table("Sweep", "Value", ["Peak"], kind="line", group=None, title="", ylabel="", yunit="", limits=[], stem="sweep")
    except StopWorkflow as stop:
        log(f"Stopped: {stop}")
    finally:
        finish(app)

The file goes on with 3,233 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_find(app, "master:resistor", "", "one") wants exactly one resistor. Give a Name when the case has several; with none, the error lists the Names it found.
  • px_set(resistor, "R", value, None) writes the value in the unit R already carries and records the old one.
  • A pass that does not build or stops early is skipped with a note in the table, and the next value is tried.
  • plot_table("Sweep", "Value", ["Peak"]) draws the table's Peak column against the swept value when the loop ends.

The Run every network arrangement Quick Start goes over the layers instead: each layer in turn is enabled and the others disabled, and each arrangement is run and tabled:

The Run every network arrangement Quick Start, as the Builder writes it⬇ Download the full file (.py, 2,371 lines)

#!/usr/bin/env python3
# Every arrangement
"""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 every_arrangement.py
Results and run.log go to a new folder next to this file (or in OUTPUT_DIR).
"""
# ==== IMPORTS ====
import bisect
import csv
import fnmatch
import importlib
import math
import os
import re
import sys
import time
import traceback

# ==== SETTINGS (edit here) ====
# The name of this workflow: used for the run folder and in the log.
WORKFLOW_NAME = "Every arrangement"
# REQUIRED. The PSCAD case to work on: a .pscx path, or the name of a case already in the
# workspace.
PROJECT = "C:/Studies/substation/substation.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 layer switches a whole group of components in or out of the build together, so one case can hold several network arrangements. Type the layer names of your own case, or leave a single * to...
def main():
    preflight([])
    app = px_connect()
    try:
        if CHECK_ONLY:
            check_elements(app, [])
            return
        # -- Step 1: Start (Case: substation.pscx) --
        # -- Step 2: Arrangement (Each layer in the case) --
        arrangement_layers = list(layer_list(app, "*"))
        if not arrangement_layers:
            log("Arrangement: no layer to loop over.", "warn")
        for arrangement_pass_no, arrangement in enumerate(each_layer(app, arrangement_layers, "Enabled", "Disabled"), 1):
            ctx_push("Arrangement", arrangement, arrangement_pass_no, len(arrangement_layers))
            try:
                # -- Step 3: Run simulation (2 s, 50 us step) --
                run = px_run(app, 2, 50, 250, None, events=(), build="auto", read="out", keep_files=False)
                # Did not build: skips to the next pass of Arrangement
                if not run.ok and (run.status == "failed"):
                    case_skipped("The case did not build")
                    continue
                # Stopped early: skips to the next pass of Arrangement
                if not (run.ok or (run.status == "failed")):
                    case_skipped("The simulation stopped before the end")
                    continue
                # -- Step 4: Add row to results table (Into "Arrangements", saved at the end automatically) --
                vbus = run.series("Vbus", "")  # Read a channel
                peak_result = peak(vbus, kind="abs", after=None, before=None)  # Peak
                table("Arrangements").add({
                    "Peak": peak_result.value,
                    "Reached (s)": run.t_end,
                }, status=True)
            except StopWorkflow:
                raise
            except Exception as err:
                case_failed(err)
            finally:
                ctx_pop(["Arrangements"])
    except StopWorkflow as stop:
        log(f"Stopped: {stop}")
    finally:
        finish(app)

Pitfalls

  • A unit you did not mean. PSCAD stores a written unit as given: write "2500 [mohm]" and the parameter is in mohm from then on. px_set writes in the unit the parameter already carries, converting from the one you name.
  • A value read as a plain number loses its unit. A unit-carrying value comes back as a number whose unit is only in its text. Read it as text first, or use px_get with a unit.
  • A Choice written as its words. A Choice takes its symbol, the capitals in the reference, such as "YES" or "RR_L", not the words in the dialog.
  • A component that is in the case twice. find_all gives every instance, on every page and inside every module. Name the one to change, or the sweep writes on all of them.
  • Enabling a component needs PSCAD 5.0.2. Component.enabled is not there on 5.0.0; enable() and disable() are, which is what the runtime uses.

Questions

How do I change a component parameter in PSCAD from Python?

Find the component with project.find_all("master:resistor") (or by its Name), then call comp.parameters(R="12.5 [ohm]"). comp.parameters() with no arguments gives every parameter and its current value.

What is the difference between a Python sweep and Multiple Run?

Multiple Run steps up to six variables inside the case, in one go of the executable. A Python sweep starts a run per value and can change anything in the case between runs, including layers, substitution sets and the run settings.

How do I switch layers in PSCAD from a script?

Set the layer's state on the project, enabled or disabled, then run. The Builder's runtime does it with layer_set(app, name, "Enabled") and puts every layer back the way it found it at the end.

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.