Fault ride through and critical clearing time in PSCAD with Python
A fault study in PSCAD is a loop around a fault that is already drawn in the case: the script writes when it starts and how long it lasts, runs the case, and reads how the voltage dipped and the machines swung. Do that over a range of durations and it is a fault ride through study; halve the gap between a duration that passes and one that fails and it is a critical clearing time.
How a fault is applied from a script
PSCAD has no event list. A fault is the Three Phase Fault component (master:tpflt) drawn at the point to be faulted, and it decides which phases are faulted and through what resistance. When it is applied comes from the logic wired into it, usually a Timed Fault Logic block (master:tfaultn), which holds the start time TF and the duration DF.
So a script times a fault by writing those two parameters, and the Three Phase Fault itself has nothing to time: in PSCAD 5.0.0 it has no time parameters at all. That is the one thing most first fault scripts get wrong.
flt = project.find_all("master:tfaultn")[0]
flt.parameters(TF="1.0 [s]", DF="0.12 [s]") # on at 1 s, cleared at 1.12 s
fault = project.find_all("master:tpflt")[0]
fault.parameters(A="YES", B="YES", C="YES", G="YES", RON="0.01 [ohm]") # what is faulted
The method
- Let the case settle first. Put the fault well after the start-up transients have died away, and run long enough after it for the machines and controls to show whether they recover.
- Loop over the durations to test, or search for the longest one that passes.
- Each pass: write
TFandDF, run, read the channels. - Check and record: the lowest voltage, the speed nadir or the highest speed, whether the case rode through.
- Put the fault back after each pass, so the next one starts from the case as it was.
Fault ride through over durations
for duration in num_range(0.08, 0.24, 0.04):
mark = px_mark()
set_fault(app, "master:tfaultn", "", at=1.0, duration=duration)
run = px_run(app, duration=5, step=50, plot_step=250)
v = run.series("Vpcc")
table("FRT").add({"Fault duration (s)": duration,
"Lowest voltage": peak(v, kind="min").value})
px_undo(mark)
set_fault writes the start and duration on whatever fault component it is given, by the parameter names PSCAD uses for them, and gives back the start and clearing times so the checks and the graphs know where the event was. Given a component with no timing parameters, such as the Three Phase Fault, it stops and lists the parameters that component does have.
The critical clearing time by bisection
A critical clearing time is the longest fault the case survives. Rather than sweep thirty durations, try the two ends, then keep halving the gap between the longest that passed and the shortest that failed. From 0.05 s to 0.5 s that is about eight runs to 5 ms.
def one_try(duration):
trial = Trial({"Fault duration (s)": duration}, "min")
mark = px_mark()
try:
set_fault(app, "master:tfaultn", "", at=1.0, duration=duration)
run = px_run(app, duration=5, step=50, plot_step=250)
highest = peak(run.series("Machine Speed"), kind="max").value
trial.report(ok=highest <= 1.05) # stays in step
finally:
px_undo(mark)
return trial
cct = find_limit(one_try, 0.05, 0.5, 0.005, passes="low")
print(cct.limit, cct.status) # the last duration that passed
What counts as staying in step is the study's own decision: a speed band, a rotor angle, a voltage that comes back within a time. Make the test the one your connection requirement states.
Open this in the Builder. The Fault ride through over fault durations 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 scripts from the Builder
The Fault ride through Quick Start runs the case for each fault duration from 0.08 s to 0.24 s and tables the voltage dip and the machine speed nadir, with every run's speed on one graph. Set the channel names to the ones your case records. This is the file with the case set to C:/Studies/wind_farm/wind_farm.pscx:
fault_ride_through.py as the Builder writes it: the settings and the workflow⬇ Download the full file (.py, 3,567 lines)
#!/usr/bin/env python3
# Fault ride through
"""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 fault_ride_through.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 = "Fault ride through"
# REQUIRED. The PSCAD case to work on: a .pscx path, or the name of a case already in the
# workspace.
PROJECT = "C:/Studies/wind_farm/wind_farm.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: Set the Fault component to the timed fault logic that drives the fault in your own case (master:tfaultn, or the older master:tfault: they hold the start time TF and the duration DF, which the Three...
def main():
preflight(["matplotlib"])
app = px_connect()
try:
if CHECK_ONLY:
check_elements(app, [])
return
# -- Step 1: Start (Case: wind_farm.pscx) --
# -- Step 2: Fault duration (s) (0.08 to 0.24, step 0.04) --
fault_duration_s_values = num_range(0.08, 0.24, 0.04)
for fault_duration_s_pass_no, fault_duration_s in enumerate(fault_duration_s_values, 1):
ctx_push("Fault duration (s)", fault_duration_s, fault_duration_s_pass_no, len(fault_duration_s_values))
mark = px_mark()
try:
# -- Step 3: Fault (master:tfaultn: 1 to 1.12 s) --
fault = set_fault(app, "master:tfaultn", "", 1, fault_duration_s, True, "one", None, None)
# -- Step 4: Run simulation (5 s, 50 us step) --
run = px_run(app, 5, 50, 250, None, events=[(1, "Fault")], build="auto", read="out", keep_files=False)
# Did not build: skips to the next pass of Fault duration (s)
if not run.ok and (run.status == "failed"):
case_skipped("The case did not build")
continue
# Stopped early: skips to the next pass of Fault duration (s)
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 "FRT", saved at the end automatically) --
voltage = run.series("Vpcc", "Voltage") # Read a channel
voltage_dip = peak(voltage, kind="min", after=None, before=None) # Voltage dip
speed = run.series("Machine Speed", "Speed") # Read a channel
speed_nadir = nadir(speed, t_event=None, nominal=None, delay=0, window=10, min_recovery=0.05, tail_guard=1, floor_below=5, ufls_below=1, requires_ufls=True, extend_to=None) # Speed nadir
table("FRT").add({
"Lowest voltage": voltage_dip.value,
"Speed nadir": speed_nadir.value,
"Reached (s)": run.t_end,
}, status=True)
# -- Step 6: Draw time series graph (Every pass on one graph) --
collect_series("n10", [speed])
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(["FRT"])
draw_collected("n10", title="", ylabel="Machine speed", unit=None, limits=[], events=True, stem="speed")
except StopWorkflow as stop:
log(f"Stopped: {stop}")
finally:
finish(app)
The file goes on with 3,411 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
- The loop is
num_range(0.08, 0.24, 0.04), both ends included. Each pass opens a mark and puts every parameter back in thefinally, so a pass never inherits the previous one's fault. set_fault(app, "master:tfaultn", ...)writes the duration of this pass on the Timed Fault Logic. The event it returns goes topx_runso the checks know the fault time and the graph marks it.- A pass that does not build or stops early is skipped with a note in the table, rather than ending the study.
peak(voltage, kind="min")is the voltage dip;nadir(speed, ...)the speed nadir, with whether it turned around.collect_serieskeeps each pass's speed curve anddraw_collecteddraws them on one graph at the end, one line per duration.
The Critical clearing time Quick Start does the search: Find limit tries 0.05 s and 0.5 s, then halves towards the boundary until it is within 5 ms, and Stays in step passes a try whose highest machine speed is at most 1.05 p.u.:
The Critical clearing time Quick Start, as the Builder writes it⬇ Download the full file (.py, 2,688 lines)
#!/usr/bin/env python3
# Critical clearing time
"""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 critical_clearing_time.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 = "Critical clearing time"
# REQUIRED. The PSCAD case to work on: a .pscx path, or the name of a case already in the
# workspace.
PROJECT = "C:/Studies/plant/plant.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: Find limit halves the gap between a duration that passes and one that fails, so it finds the critical clearing time in about eight runs instead of a sweep of thirty. Set the pass test to what your...
def main():
preflight([])
app = px_connect()
try:
if CHECK_ONLY:
check_elements(app, [])
return
# -- Step 1: Start (Case: plant.pscx) --
# -- Step 2: Fault duration (s) (0.05 to 0.5, within 0.005) --
def try_n2(fault_duration_s):
trial = Trial({"Fault duration (s)": fault_duration_s}, "min")
ctx_push("Fault duration (s)", fault_duration_s)
mark = px_mark()
try:
# -- Step 3: Fault (master:tfaultn: 1 to 1.12 s) --
fault = set_fault(app, "master:tfaultn", "", 1, fault_duration_s, True, "one", None, None)
# -- Step 4: Run simulation (5 s, 50 us step) --
run = px_run(app, 5, 50, 250, None, events=[(1, "Fault")], build="auto", read="out", keep_files=False)
# Did not build: ends this try of Fault duration (s)
if not run.ok and (run.status == "failed"):
case_skipped("The case did not build")
return trial
# Stopped early: ends this try of Fault duration (s)
if not (run.ok or (run.status == "failed")):
case_skipped("The simulation stopped before the end")
return trial
# -- Step 5: Stays in step (Value ≤ 1.05, goes on either way) --
machine_speed = run.series("Machine Speed", "") # Read a channel
peak_result = peak(machine_speed, kind="max", after=None, before=None) # Peak
stays_in_step_result = verdict(peak_result.value, "le", 1.05, 0)
if stays_in_step_result.ok is None:
log("Stays in step: NO DATA (" + str(stays_in_step_result.get("reason") or "nothing to check") + "), so it did not pass.", "warn")
# -- Step 6: Add row to results table (Into "CCT", saved at the end automatically) --
table("CCT").add({"Highest speed": peak_result.value}, status=True)
trial.report(ok=stays_in_step_result.ok)
except StopWorkflow:
raise
except Exception as err:
case_failed(err, trial)
finally:
try:
px_undo(mark)
except Exception as fin:
log(f"Could not put things back after this pass: {fin}", "error")
ctx_pop(["CCT"])
return trial
fault_duration_s_result = find_limit(try_n2, 0.05, 0.5, 0.005, max_steps=20, passes="low", bracket="given", on_unknown="stop", label="Fault duration (s)", integer=False)
except StopWorkflow as stop:
log(f"Stopped: {stop}")
finally:
finish(app)
Pitfalls
- Writing the time on the Three Phase Fault. It has no time parameters; the time is on the Timed Fault Logic wired to it. The Builder's Set a fault block names
master:tfaultnfor that reason. - A fault during the start-up. A case starts from zero and needs time to reach its steady state. A fault at 0.1 s tests the initialisation, not the plant. Look at a run with no fault first and put the fault after it has settled, or start from a snapshot.
- Speed in per unit, a limit in hertz. A machine speed channel is usually per unit, so a stay-in-step test of 1.05 is 5 % over speed, not 1.05 Hz. Check the unit the channel carries (
series.unit). - A search that stops early. A try that gives no answer (a run that did not build, a check with no data) ends a bisection with status
UNKNOWNby default. Read the status, not only the limit. - The case changed after the study. Every write is in the case. Keep the mark and undo around each pass, and
UNDO_CHANGES = Truefor the run as a whole.
Questions
How do I apply a fault in PSCAD from Python?
Write the start time and the duration on the timing logic that drives the fault, usually Timed Fault Logic (master:tfaultn, parameters TF and DF), then run the case. The Three Phase Fault component sets which phases are faulted and the fault resistance.
How do I find the critical clearing time in PSCAD?
Run the case with a fault of a given duration and test whether it stays stable, then halve the gap between the longest duration that passed and the shortest that failed until it is narrow enough. That takes about eight runs instead of a sweep of thirty.
Why does my fault not change between runs?
Most often the time is being written on a component that does not use it, or the parameter is written without its unit. Read the value back after writing it, and write the time on the Timed Fault Logic.
Related
- Automate PSCAD studies with Python: every guide
- PSCAD automation with Python: getting started
- Build and run a PSCAD case from Python
- Sweep a PSCAD parameter with Python
- Harmonics and THD from a PSCAD run
- Snapshots, simulation sets and batch runs
- Control tuning: overshoot, rise time and damping
Every parameter name a script writes is in the component reference, read out of PSCAD itself. The Workflow Builder writes the Python from blocks.