Control tuning in PSCAD with Python: overshoot, rise time and damping
A control review asks a few numbers of every response: how far a step overshoots, how fast it rises, and whether the oscillation after a disturbance dies away. In PSCAD all three come out of the recorded waveform. A script that reads them can retune a gain, run again, and keep going until the numbers pass.
What a control check reads from a run
A step response is a set-point change or a disturbance that a controlled quantity follows to a new value. Its overshoot is how far it goes past the final value, as a percentage of the step, and its rise time how long it takes from 10 % to 90 % of the step.
A ringdown is the oscillation after a disturbance. Its peaks fall away along an exponential when it is damped and grow when it is not. The damping ratio is read from that exponential and the frequency from the spacing of the peaks: a negative ratio is an oscillation that is growing, which is what a converter control interaction or a subsynchronous oscillation looks like in an EMT run.
The method
- Disturb the case with what the requirement names: a set-point step on a constant, a short fault, a switching.
- Run long enough to see several periods of the slowest mode you care about.
- Start the analysis after the disturbance itself, so its own step is not read as the first peak.
- Check and record: overshoot and rise time against their limits, a damping ratio above the minimum.
Overshoot and rise time of a step
ref = px_find(app, "master:const", "Vref")
px_set(ref, "Value", 1.02) # the step, on a constant read by the control
run = px_run(app, duration=3, step=50, plot_step=100)
r = overshoot(run.series("Vrms"), t_event=1.0)
print(r.value, r.rise) # overshoot in %, rise time in s
A step needs a time: the constant here is written before the run, so the step is where the case's own logic switches to it. Wire the set-point through a timed component, or step it from a snapshot.
Damping of a ringdown
set_fault(app, "master:tfaultn", "", at=1.0, duration=0.05) # a short disturbance
run = px_run(app, duration=10, step=50, plot_step=500)
d = damping(run.series("Machine Speed"), t_start=1.2, t_end=None, min_peaks=3)
print(d.value, d.frequency, d.stable) # ratio, Hz, ratio above zero
Open this in the Builder. The Damping of a ringdown Quick Start loads in the PSCAD Workflow Builder, ready for your own case. Change any block and download the Python file.
Open in the BuilderThe full Python script from the Builder
The Damping of a ringdown Quick Start applies a 50 ms disturbance at 1 s, runs for 10 s, reads the damping ratio and the frequency of the oscillation from 1.2 s on, and passes the run when the ratio is at least 0.03. This is the file with the case set to C:/Studies/statcom/statcom.pscx:
damping_of_a_ringdown.py as the Builder writes it: the settings and the workflow⬇ Download the full file (.py, 2,473 lines)
#!/usr/bin/env python3
# Damping check
"""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 damping_check.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
# ==== SETTINGS (edit here) ====
# The name of this workflow: used for the run folder and in the log.
WORKFLOW_NAME = "Damping check"
# REQUIRED. The PSCAD case to work on: a .pscx path, or the name of a case already in the
# workspace.
PROJECT = "C:/Studies/statcom/statcom.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 control interaction, a subsynchronous oscillation or a badly tuned loop shows up as an oscillation that does not decay. Start the damping window after the disturbance itself, so its own step is n...
def main():
preflight([])
app = px_connect()
try:
if CHECK_ONLY:
check_elements(app, [])
return
# -- Step 1: Start (Case: statcom.pscx) --
# -- Step 2: Disturbance (master:tfaultn: 1 to 1.05 s) --
fault = set_fault(app, "master:tfaultn", "", 1, 0.05, True, "one", None, None)
# -- Step 3: Run simulation (10 s, 50 us step) --
run = px_run(app, 10, 50, 500, None, events=[(1, "Disturbance")], build="auto", read="out", keep_files=False)
# Did not build: ends the workflow
if not run.ok and (run.status == "failed"):
log("The case did not build. The workflow ends here.", "warn")
return
# Stopped early: ends the workflow
if not (run.ok or (run.status == "failed")):
log("The simulation stopped before the end. The workflow ends here.", "warn")
return
# -- Step 4: Well damped (Value ≥ 0.03) --
machine_speed = run.series("Machine Speed", "") # Read a channel
damping_result = damping(machine_speed, t_start=1.2, t_end=None, min_peaks=3) # Damping of a ringdown
well_damped_result = verdict(damping_result.value, "ge", 0.03, 0)
if well_damped_result.ok is None:
log("Well damped: NO DATA (" + str(well_damped_result.get("reason") or "nothing to check") + "), so it takes Fail.", "warn")
# -- Step 5: Add row to results table (Into "Damping", saved at the end automatically) --
table("Damping").add({
"Damping ratio": damping_result.value,
"Mode (Hz)": damping_result.frequency,
})
except StopWorkflow as stop:
log(f"Stopped: {stop}")
finally:
finish(app)
The file goes on with 2,371 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
set_fault(app, "master:tfaultn", "", 1, 0.05, ...)is the disturbance: a 50 ms fault on the Timed Fault Logic, put back at the end.- The 500 us plot step keeps a 10 s run small, and is fine for electromechanical modes of a few hertz. Lower it for a faster control mode.
damping(machine_speed, t_start=1.2, ...)starts 0.15 s after the fault clears and needs at least three peaks to fit; with fewer it says the signal may not be oscillating rather than giving a number.verdict(damping_result.value, "ge", 0.03, 0)is the pass test: 3 % damping or more. A ratio of none (nothing to fit) is reported as no data and takes the fail side.
Pitfalls
- The disturbance read as the first peak. Start the window after the fault has cleared, or the ratio describes the fault, not the oscillation.
- Two modes at once. The fit assumes one dominant oscillation. When two modes beat, the peaks do not lie on one exponential; filter the signal or read a quantity where one mode dominates.
- A plot step too coarse for the mode. Each period needs many samples for its peaks to be found. A 500 us plot step suits a mode of a few hertz, not a control mode of a few hundred.
- A run too short. Fewer than three peaks after the window starts gives no answer. Run until the slowest mode has shown several periods.
Questions
How do I measure damping in PSCAD?
Record the quantity that oscillates, run past the disturbance for several periods, find the peaks after the disturbance has cleared and fit them with an exponential. The decay rate and the period give the damping ratio; a negative ratio is a growing oscillation.
How do I measure overshoot and rise time from a PSCAD run?
Find the value before the step and the final value after it, then the highest point past the final value as a percentage of the step (the overshoot), and the time from 10 % to 90 % of the step (the rise time). The Builder's Overshoot and rise time block does it from any recorded channel.
Can a script retune a controller in PSCAD?
Yes. Write the gain on its component, run, read the response, and repeat. The Builder's Optimise block searches the gains for you against the checks you wire into it.
Related
- Automate PSCAD studies with Python: every guide
- PSCAD automation with Python: getting started
- Build and run a PSCAD case from Python
- Fault studies in PSCAD with Python: ride through and critical clearing time
- Sweep a PSCAD parameter with Python
- Harmonics and THD from a PSCAD run
- Snapshots, simulation sets and batch runs
Every parameter name a script writes is in the component reference, read out of PSCAD itself. The Workflow Builder writes the Python from blocks.