Tutorial¶
Requirements¶
Applications¶
The PSCAD Automation Library requires the following tools are installed:
- PSCAD 4.6.3
- Python 3.x (such as Python 3.7.1)
- PIP - Python’s package manager (included with Python 3.4 and later)
- PyWin32 - Python extensions for Microsoft Windows
Installation of the above programs is beyond the scope of this tutorial.
Automation Library¶
You can check which version of the Automation Library is installed by executing:
py -m mhrc.automation
If multiple versions of Python have been installed, each installation will need its own copy of the Automation Library, so you may want specify the Python version being queried:
py -3.7 -m mhrc.automation
PSCAD’s “Update Client” will automatically install the Automation Library
into the latest Python 3.x installation.
If you wish to add the Automation Library to a different Python 3.x installation,
you would execute a PIP install
command similar to the following
from the C:\Program Files (x86)\PSCAD\AutomationLibrary\463\Installs\PyAL
directory:
py -3.4 -m pip install mhrc_automation-1.2.4-py3-none-any.whl
My First Automation Script¶
Running the Tutorial¶
The following script
is the “simplest” PSCAD
automation script that does something useful.
It:
- launches PSCAD,
- loads the “Tutorial” workspace,
- runs all simulation sets in the “Tutorial” workspace, and
- quits PSCAD.
To keep this first example simple, no error checking of any kind is done. Also, no feedback from the simulation is retrieved for the same reason.
#! python3
import mhrc.automation
# Launch PSCAD
pscad = mhrc.automation.launch_pscad()
# Load the tutorial workspace
pscad.load(r"C:\Users\Public\Documents\PSCAD\4.6.3\Examples x64\tutorial\Tutorial.pswx")
# Run all the simulation sets in the workspace
pscad.run_all_simulation_sets()
# Exit PSCAD
pscad.quit()
Many assumptions are being made here.
Certificate licensing is used, and a license certificate can be automatically acquired.
Ensure “Retain certificate” is selected under “Certificate Licensing ➭ Termination Behaviour” and ensure you hold a valid certificate when you exit PSCAD before running the above script.
The “Public Documents” directory is located at C:\Users\Public\Documents.
The 64-bit version PSCAD 4.6.3 is installed.
Finding the Tutorial¶
We can relax the last two restrictions by retrieving the correct location of the “Public Documents” directory from PyWin, and searching inside that directory for the “Tutorial” workspace. Doing so will allow the scripts in this tutorial to be downloaded and executed without modification by the reader.
Alter the import statements at the top of the script to read:
import mhrc.automation, os
from win32com.shell import shell, shellcon
To retrieve the “Public Documents” folder, add the following statement below the imports:
# Find "Public Documents" folder
# Probably "C:\Users\Public\Documents"
public_dir = shell.SHGetFolderPath(0, shellcon.CSIDL_COMMON_DOCUMENTS, None, 0)
Beneath this statement, add another which will search inside the “PSCAD” folder for a subdirectory which contains the desired Tutorial.pswx file:
# Find the "Tutorial" workspace
# Probably "<Public Documents>\PSCAD\4.6.3\Examples\tutorial\Tutorial.pswx"
tutorial_dir = next(root for root, _, files in os.walk(
os.path.join(public_dir, "PSCAD") ) if "Tutorial.pswx" in files)
Finally, change the pscad.load() statement to load the Tutorial.pscx workspace from that directory:
# Load the tutorial workspace
pscad.load(os.path.join(tutorial_dir, "Tutorial.pswx"))
See the resulting script
.
Logging¶
Add logging
to the current imports,
and add the logging.basicConfig
statement below:
import mhrc.automation, os, logging
from win32com.shell import shell, shellcon
# Log 'INFO' messages & above. Include level & module name.
logging.basicConfig(level=logging.INFO,
format="%(levelname)-8s %(name)-26s %(message)s")
If you now run the script
,
you should see something similar to following output.
The paths, port numbers, and process ID’s may be different:
INFO mhrc.automation.controller Launching PSCAD 4.6.3 (x64): C:\Program Files (x86)\PSCAD463 x64 Testing\bin\win64\Pscad.exe
INFO mhrc.automation.pscad Server socket bound to 0.0.0.0 port 51312
INFO mhrc.automation.pscad Process ID = 3836
INFO mhrc.automation.pscad Waiting for connection on ('0.0.0.0', 51312)
INFO mhrc.automation.pscad Connected to ('127.0.0.1', 51313)
INFO mhrc.automation.pscad Loading ('C:\\Users\\Public\\Documents\\PSCAD\\4.6.3\\Examples x64\\tutorial\\Tutorial.pswx',)
INFO mhrc.automation.pscad Run all simulation sets
INFO mhrc.automation.pscad Quiting PSCAD
In the next steps, we’ll be adding additional logging to our script.
The logging already built into the automation library can be squelched to warnings and above,
so it is easier to see our own log messages.
After the logging.basicConfig( … )
line, add the following:
# Ignore INFO msgs from automation (eg, mhrc.automation.controller, ...)
logging.getLogger('mhrc.automation').setLevel(logging.WARNING)
LOG = logging.getLogger('main')
Next, add logging lines to print out the location of the “Public Documents” and “tutorial” directories:
LOG.info("Public Documents : %s", public_dir)
LOG.info("Tutorial directory: %s", tutorial_dir)
If you now run the script
,
you should see something similar to following output.
Again, the paths may be different, which is of course the whole point of this exercise:
INFO main Public Documents : C:\Users\Public\Documents
INFO main Tutorial directory: C:\Users\Public\Documents\PSCAD\4.6.3\Examples x64\tutorial
Error Handling¶
If the automation controller cannot launch PSCAD,
it will log the reason why to the console and return the value None
.
Other problems may be signalled by an “Exception” being raised.
A well behaved script must catch these exceptions, and properly clean up.
This cleanup should include closing PSCAD.
Replace the final 3 commands in the script
with the following:
if pscad:
try:
# Load the tutorial workspace
pscad.load(os.path.join(tutorial_dir, "Tutorial.pswx"))
# Run all the simulation sets in the workspace
pscad.run_all_simulation_sets()
finally:
# Exit PSCAD
pscad.quit()
else:
LOG.error("Failed to launch PSCAD")
Whether or not an exception occurs during the loading of the workspace
or running all simulation sets, due to the try: ... finally: ...
block,
PSCAD will always be terminated before the script terminates.
Launch Options¶
Minimize PSCAD¶
When PSCAD is started, it normally opens the PSCAD window. During automation, it is sometimes desirable to launch PSCAD with its window “minimized”, reducing the amount “screen flickering”, as well as the chance a stray mouse click could “poison” a running automation.
Add minimized=True
to the launch_pscad()
call:
# Launch PSCAD
pscad = mhrc.automation.launch_pscad(minimize=True)
Run this new script
.
You should still see log messages in the Python Shell, and PSCAD appear in the Task Bar,
but no PSCAD window should open.
Note
PSCAD remembers where it is on the screen, including whether or not it is “maximized”. When it is launched, it restores itself to that position and, if required, maximized state. The automation library cannot override the remembered “maximized” state. If PSCAD remembers that it was last “maximized”, it will maximize itself, regardless of whether it was launched in a “minimized” state by the automation library. Manually launching PSCAD, de-maximizing it, and quitting it will fix this issue.
PSCAD Version¶
The launch_pscad()
method does a bit of work behind the scenes.
If more than one version of PSCAD is installed, it tries to pick the best version to run.
“Best” in this context means:
- not an “Alpha” version, if other choices exist, then
- not a “Beta” version, if other choices exist, then
- not a 32-bit version, if other choices exist, finally
- the “lexically largest” version of the choices that remain.
Instead of letting launch_pscad()
choose the version,
the script can specify the exact version to use with the
pscad_version=...
parameter.
For example, to launch the 64-bit version of PSCAD 4.6.3, use:
pscad = mhrc.automation.launch_pscad(pscad_version='PSCAD 4.6.3 (x64)')
A controller object may be queried to determine which versions of PSCAD are installed.
Replacing the launch_pscad()
statement with the following will mimic
its selection process:
controller = mhrc.automation.controller()
versions = controller.get_paramlist_names('pscad')
LOG.info("PSCAD Versions: %s", versions)
# Skip any 'Alpha' versions, if other choices exist
vers = [ver for ver in versions if 'Alpha' not in ver]
if len(vers) > 0:
versions = vers
# Skip any 'Beta' versions, if other choices exist
vers = [ver for ver in versions if 'Beta' not in ver]
if len(vers) > 0:
versions = vers
# Skip any 32-bit versions, if other choices exist
vers = [ver for ver in versions if 'x86' not in ver]
if len(vers) > 0:
versions = vers
LOG.info(" After filtering: %s", versions)
# Of any remaining versions, choose the "lexically largest" one.
version = sorted(versions)[-1]
LOG.info(" Selected PSCAD version: %s", version)
# Launch PSCAD
LOG.info("Launching: %s", version)
pscad = mhrc.automation.launch_pscad(pscad_version=version, minimize=True)
With the above changes to the script
, the output may be:
INFO main Public Documents : C:\Users\Public\Documents
INFO main Tutorial directory: C:\Users\Public\Documents\PSCAD\4.6.3\Examples x64\tutorial
INFO main PSCAD Versions: ['PSCAD 4.6.2 (x64)', 'PSCAD 4.6.3 (x64)', 'PSCAD Beta (x64)']
INFO main After filtering: ['PSCAD 4.6.2 (x64)', 'PSCAD 4.6.3 (x64)']
INFO main Selected PSCAD version: PSCAD 4.6.3 (x64)
INFO main Launching: PSCAD 4.6.3 (x64)
Fortran Version¶
In a similar fashion, the controller object may be queried for which versions of FORTRAN are installed.
Before the launch_pscad()
line, add the following:
# Get all installed FORTRAN compiler versions
fortrans = controller.get_paramlist_names('fortran')
LOG.info("FORTRAN Versions: %s", fortrans)
# Skip 'GFortran' compilers, if other choices exist
vers = [ver for ver in fortrans if 'GFortran' not in ver]
if len(vers) > 0:
fortrans = vers
LOG.info(" After filtering: %s", fortrans)
# Order the remaining compilers, choose the last one (highest revision)
fortran = sorted(fortrans)[-1]
LOG.info(" Selected FORTRAN version: %s", fortran)
Add the , fortran_version=fortran
parameter to the launch_pscad()
call:
# Launch PSCAD
LOG.info("Launching: %s FORTRAN=%r", version, fortran)
pscad = mhrc.automation.launch_pscad(pscad_version=version, minimize=True,
fortran_version=fortran)
With the above changes to the script
, the output may be:
INFO main Public Documents : C:\Users\Public\Documents
INFO main Tutorial directory: C:\Users\Public\Documents\PSCAD\4.6.3\Examples x64\tutorial
INFO main PSCAD Versions: ['PSCAD 4.6.2 (x64)', 'PSCAD 4.6.3 (x64)', 'PSCAD Beta (x64)']
INFO main After filtering: ['PSCAD 4.6.2 (x64)', 'PSCAD 4.6.3 (x64)']
INFO main Selected PSCAD version: PSCAD 4.6.3 (x64)
INFO main FORTRAN Versions: ['GFortran 4.2.1', 'GFortran 4.6.2']
INFO main After filtering: ['GFortran 4.2.1', 'GFortran 4.6.2']
INFO main Selected FORTRAN version: GFortran 4.6.2
INFO main Launching: PSCAD 4.6.3 (x64) FORTRAN='GFortran 4.6.2'
Matlab Version¶
In a similar fashion, the controller object may be queried for which versions of Matlab are installed. Matlab, unlike FORTRAN, is not required for PSCAD to run, so there may be no versions of Matlab installed.
Before the launch_pscad()
line, add the following:
# Get all installed Matlab versions
matlabs = controller.get_paramlist_names('matlab')
LOG.info("Matlab Versions: %s", matlabs)
# Get the highest installed version of Matlab:
matlab = sorted(matlabs)[-1] if matlabs else None
LOG.info(" Selected Matlab version: %s", matlab)
Add the , matlab_version=matlab
parameter to the launch_pscad()
call:
# Launch PSCAD
LOG.info("Launching: %s FORTRAN=%r Matlab=%r", version, fortran, matlab)
pscad = mhrc.automation.launch_pscad(pscad_version=version, minimize=True,
fortran_version=fortran,
matlab_version=matlab)
With the above changes to the script
, the output may be:
INFO main Public Documents : C:\Users\Public\Documents
INFO main Tutorial directory: C:\Users\Public\Documents\PSCAD\4.6.3\Examples x64\tutorial
INFO main PSCAD Versions: ['PSCAD 4.6.2 (x64)', 'PSCAD 4.6.3 (x64)', 'PSCAD Beta (x64)']
INFO main After filtering: ['PSCAD 4.6.2 (x64)', 'PSCAD 4.6.3 (x64)']
INFO main Selected PSCAD version: PSCAD 4.6.3 (x64)
INFO main FORTRAN Versions: ['GFortran 4.2.1', 'GFortran 4.6.2']
INFO main After filtering: ['GFortran 4.2.1', 'GFortran 4.6.2']
INFO main Selected FORTRAN version: GFortran 4.6.2
INFO main Matlab Versions: []
INFO main Selected Matlab version: None
INFO main Launching: PSCAD 4.6.3 (x64) FORTRAN='GFortran 4.6.2' Matlab=None
PSCAD Settings¶
To ensure reliable, repeatable tests, the automation library instructs PSCAD to use the default settings, ignoring any stored user settings.
The pscad.settings()
method
must be used to change any application settings to the desired value.
The following script
may be used to determine
what settings exist, and their current values:
#! python3
import mhrc.automation
pscad = mhrc.automation.launch_pscad(minimize=True)
for key, value in sorted(pscad.settings().items()):
print("%33s: %s" % (key, value))
pscad.quit()
And produces output similar to:
AvoidLocalExec: 1
DetectedCoreCount:
EMTDC_Version:
Folder:
LCP_MaxConcurrentExec: 8
LCP_Version:
MEDIC_Version:
MaxConcurrentSim: 8
VersionText: Not installed
active_graphics: 2
agent_show: true
backup_enable: true
backup_folder: $(LocalDir)\FileBackups
backup_freq: 60
⋮ ⋮ ⋮ ⋮
To change settings from their defaults, pass one or more key=value
arguments
to the pscad.settings()
method:
pscad.settings(MaxConcurrentSim=8, LCP_MaxConcurrentExec=8)
Note
All settings are automatically converted to strings before passing to PSCAD.
There is no functional difference between pscad.settings(MaxConcurrentSim=8)
and pscad.settings(MaxConcurrentSim="8")
.
PSCAD returns all settings as strings.
If a numeric setting is expected, the Python script is responsible for converting
the string into an integer.
Booleans may be returned as "true"
or "false"
, without the first letter
capitalized; again the Python script must accept responsibility for converting
the strings into Python boolean values True
and False
if required.
Projects¶
To run all loaded projects, one at a time, we first obtain a list of all projects.
From this list, we filter out any libraries, which are not runnable.
Then, we retrieve a Project
controller for each project,
and call Project.run()
on each one, in turn.
Replace the pscad.run_all_simulation_sets()
line with the following:
# Get a list of all projects
projects = pscad.list_projects()
# Filter out libraries; only keep cases.
cases = [prj for prj in projects if prj['type'] == 'Case']
# For each case ...
for case in cases:
project = pscad.project(case['name'])
LOG.info("Running '%s' (%s)", case['name'], case['description'])
project.run();
LOG.info("Run '%s' complete", case['name'])
This script
would produce:
INFO main Public Documents : C:\Users\Public\Documents
INFO main Tutorial directory: C:\Users\Public\Documents\PSCAD\4.6.3\Examples x64\tutorial
INFO main PSCAD Versions: ['PSCAD 4.6.2 (x64)', 'PSCAD 4.6.3 (x64)', 'PSCAD Beta (x64)']
INFO main After filtering: ['PSCAD 4.6.2 (x64)', 'PSCAD 4.6.3 (x64)']
INFO main Selected PSCAD version: PSCAD 4.6.3 (x64)
INFO main FORTRAN Versions: ['GFortran 4.2.1', 'GFortran 4.6.2']
INFO main After filtering: ['GFortran 4.2.1', 'GFortran 4.6.2']
INFO main Selected FORTRAN version: GFortran 4.6.2
INFO main Matlab Versions: []
INFO main Selected Matlab version: None
INFO main Launching: PSCAD 4.6.3 (x64) FORTRAN='GFortran 4.6.2' Matlab=None
INFO main Running 'chatter' (Simple case with chatter elimination)
INFO main Run 'chatter' complete
INFO main Running 'fft' (Harmonic Impedance and FFT)
INFO main Run 'fft' complete
INFO main Running 'inputctrl' (Input Control Components)
INFO main Run 'inputctrl' complete
INFO main Running 'interpolation' (Simple case illustrating interpolation)
INFO main Run 'interpolation' complete
INFO main Running 'legend' (Use of macros)
INFO main Run 'legend' complete
INFO main Running 'vdiv' (Single Phase Voltage Divider)
INFO main Run 'vdiv' complete
INFO main Running 'simpleac' (A Simple AC Power System)
INFO main Run 'simpleac' complete
INFO main Running 'multirun' (A Simple Multiple Run Example)
INFO main Run 'multirun' complete
INFO main Running 'pagearray' (Page Inside a Page, Arrays)
INFO main Run 'pagearray' complete
Simulation Sets¶
When a workspace contains multiple projects, they may be required to run together or in a particular sequence. A simulation set is often used to control the collection of projects.
Instead of blindly running all projects in the workspace, or simply all simulation sets, we may retrieve the list of simulations sets and run each simulation set individually, under the control of our script. If no simulation sets are found, we can fall back to running each project separately.
Replace the code we just added, with this code instead:
workspace = pscad.workspace()
# Get the list of simulation sets in the workspace
sim_sets = workspace.list_simulation_sets()
if len(sim_sets) > 0:
LOG.info("Simulation sets: %s", sim_sets)
# For each simulation set ...
for sim_set_name in sim_sets:
# ... run it
LOG.info("Running simulation set '%s'", sim_set_name)
sim_set = workspace.simulation_set(sim_set_name)
sim_set.run()
LOG.info("Simulation set '%s' complete", sim_set_name)
else:
# Get a list of all projects
projects = pscad.list_projects()
# Filter out libraries; only keep cases.
cases = [prj for prj in projects if prj['type'] == 'Case']
# For each case ...
for case in cases:
project = pscad.project(case['name'])
LOG.info("Running '%s' (%s)", case['name'], case['description'])
project.run();
LOG.info("Run '%s' complete", case['name'])
This version of the script
runs all projects simultaneously,
producing the following output:
INFO main Public Documents : C:\Users\Public\Documents
INFO main Tutorial directory: C:\Users\Public\Documents\PSCAD\4.6.3\Examples x64\tutorial
INFO main PSCAD Versions: ['PSCAD 4.6.2 (x64)', 'PSCAD 4.6.3 (x64)', 'PSCAD Beta (x64)']
INFO main After filtering: ['PSCAD 4.6.2 (x64)', 'PSCAD 4.6.3 (x64)']
INFO main Selected PSCAD version: PSCAD 4.6.3 (x64)
INFO main FORTRAN Versions: ['GFortran 4.2.1', 'GFortran 4.6.2']
INFO main After filtering: ['GFortran 4.2.1', 'GFortran 4.6.2']
INFO main Selected FORTRAN version: GFortran 4.6.2
INFO main Matlab Versions: []
INFO main Selected Matlab version: None
INFO main Launching: PSCAD 4.6.3 (x64) FORTRAN='GFortran 4.6.2' Matlab=None
INFO main Simulation sets: ['default']
INFO main Running simulation set 'default'
INFO main Simulation set 'default' complete
If, instead of loading Tutorial.pswx
, we just loaded vdiv.pscx
project:
# Load only the 'voltage divider' project
pscad.load(os.path.join(tutorial_dir, "vdiv.pscx"))
there are no simulation sets, so the else:
path is taken in our script
,
and the vdiv
project is run:
INFO main Public Documents : C:\Users\Public\Documents
INFO main Tutorial directory: C:\Users\Public\Documents\PSCAD\4.6.3\Examples x64\tutorial
INFO main PSCAD Versions: ['PSCAD 4.6.2 (x64)', 'PSCAD 4.6.3 (x64)', 'PSCAD Beta (x64)']
INFO main After filtering: ['PSCAD 4.6.2 (x64)', 'PSCAD 4.6.3 (x64)']
INFO main Selected PSCAD version: PSCAD 4.6.3 (x64)
INFO main FORTRAN Versions: ['GFortran 4.2.1', 'GFortran 4.6.2']
INFO main After filtering: ['GFortran 4.2.1', 'GFortran 4.6.2']
INFO main Selected FORTRAN version: GFortran 4.6.2
INFO main Matlab Versions: []
INFO main Selected Matlab version: None
INFO main Launching: PSCAD 4.6.3 (x64) FORTRAN='GFortran 4.6.2' Matlab=None
INFO main Running 'vdiv' (Single Phase Voltage Divider)
INFO main Run 'vdiv' complete
Event Monitoring¶
Event Handlers¶
The Automation Library will allow you to observe the communications between the automation library and the PSCAD process.
At the top of the file, add the following “Handler” class:
import xml.etree.ElementTree as ET
class Handler:
def send(self, msg):
if msg is None:
LOG.info("Tick")
else:
event = msg.find('event')
if event is not None:
detail = str(ET.tostring(event), 'utf-8')
LOG.info("%s", detail)
def close(self):
pass
Following the successful launch of PSCAD, we can create an instance of our Handler
class,
and attach it to the PSCAD instance. After the if pscad:
, add the following two lines:
if pscad:
handler = Handler()
pscad.add_handler(handler)
After downgrading some of the earlier LOG.info(...)
messages to LOG.debug(...)
message,
if we run this script
, we might see:
INFO main Launching: PSCAD 4.6.3 (x64) FORTRAN='GFortran 4.6.2' Matlab=None
INFO main <event type="LoadEvent"><type Task="1662" file-type="files" status="BEGIN" /></event>
INFO main <event type="LoadEvent"><type Task="1688" file-type="workspace" status="BEGIN" /><file>C:\Users\Public\Documents\PSCAD\4.6.3\Examples x64\tutorial\vdiv.pscx</file></event>
INFO main <event type="LoadEvent"><type Task="1688" file-type="workspace" status="END" /><file>C:\Users\Public\Documents\PSCAD\4.6.3\Examples x64\tutorial\vdiv.pscx</file><project Description="Single Phase Voltage Divider" name="vdiv" type="Case" /></event>
INFO main <event type="LoadEvent"><type Task="1662" file-type="files" status="END" /></event>
INFO main Running 'vdiv' (Single Phase Voltage Divider)
INFO main <event type="BuildEvent"><type Task="1765" name="Workspace" status="BEGIN" /></event>
INFO main <event type="BuildEvent"><project name="vdiv" /><type Task="1767" name="Project Builder" status="BEGIN" /></event>
INFO main <event type="BuildEvent"><project name="vdiv" /><type Task="1768" name="Project Compile" status="BEGIN" /></event>
INFO main <event type="BuildEvent"><project name="vdiv" /><type Task="1768" name="Project Compile" status="END" /></event>
INFO main <event type="BuildEvent"><project name="vdiv" /><type Task="9288" name="EMTDC MAKE" status="BEGIN" /></event>
INFO main Tick
INFO main <event type="BuildEvent"><project name="vdiv" /><type Task="9288" name="EMTDC MAKE" status="END" /></event>
INFO main <event type="BuildEvent"><project name="vdiv" /><type Task="1767" name="Project Builder" status="END" /></event>
INFO main <event type="BuildEvent"><project name="vdiv" /><type Task="1787" name="Project Solver" status="BEGIN" /></event>
INFO main <event type="BuildEvent"><project name="vdiv" /><type Task="1787" name="Project Solver" status="END" /></event>
INFO main <event type="BuildEvent"><project name="vdiv" /><rank value="1" /><type Task="1788" name="Mediator" status="BEGIN" /></event>
INFO main <event type="BuildEvent"><project name="vdiv" /><type Task="14008" name="EMTDC RUN" status="BEGIN" /></event>
INFO main <event type="BuildEvent"><project name="vdiv" /><rank value="1" /><type Task="1788" name="Mediator" status="END" /></event>
INFO main <event type="BuildEvent"><project name="vdiv" /><type Task="14008" name="EMTDC RUN" status="END" /></event>
INFO main Run 'vdiv' complete
As messages are received from PSCAD, the automation library sends the message to each registered handler,
by calling handler.send(msg)
.
The handler can do pretty much whatever it wants with the message.
If it consumes the message, and doesn’t want any subsequent handler from seeing it,
the handler should return True
.
After a period of time when no messages have been sent back from PSCAD,
the automation library also sends a blank message to the handler, by calling handler.send(None)
.
This allows the handler a chance to do additional processing.
For instance, the Automated Test Suite’s ProgressHandler
uses those opportunities
to send a get-run-status
command to the various projects, in order to track % complete
.
Removing Handlers¶
If the script decides a certain handler is no longer required,
the script can remove it by calling pscad.remove_handler( handler )
.
Automatic Removal¶
Since the handler is receiving messages from PSCAD,
it is usually in the best position to determine when a particular process is complete.
The handler can indicate this to the automation library, by returning the special value StopIteration
,
or by raising the StopIteration
exception.
When this happens, the automation library will automatically remove the handler.
For example, the Load
process is complete when a load event is found
with a file-type
of files
and a status
of END
.
Change the Handler’s send()
method to the following code:
def send(self, msg):
if msg is not None:
event = msg.find('event')
if event is not None:
detail = str(ET.tostring(event), 'utf-8')
LOG.info("%s ...", detail[:70])
event_type = event.find('type')
file_type = event_type.get('file-type')
status = event_type.get('status')
if file_type == 'files' and status == 'END':
LOG.info("Load must be complete.")
return StopIteration
When this new script
is run, this much shorter output results:
INFO main Launching: PSCAD 4.6.3 (x64) FORTRAN='GFortran 4.6.2' Matlab=None
INFO main <event type="LoadEvent"><type Task="1662" file-type="files" status="BE ...
INFO main <event type="LoadEvent"><type Task="1688" file-type="workspace" status ...
INFO main <event type="LoadEvent"><type Task="1688" file-type="workspace" status ...
INFO main <event type="LoadEvent"><type Task="1662" file-type="files" status="EN ...
INFO main Load must be complete.
INFO main Running 'vdiv' (Single Phase Voltage Divider)
INFO main Run 'vdiv' complete
Build Events¶
Build Events are generated when PSCAD builds and runs cases. A build event handler might be installed just before executing the run command, and removed just after the run command finished. Code to do thing might look like this:
handler = BuildEventHandler()
pscad.add_handler(handler)
project.run()
pscad.remove_handler(handler)
This is a lot of boiler-plate code.
As a convenience, the automation library will perform the add_handler
and remove_handler
calls itself,
if the handler is passed to the run( )
command directly:
project.run( BuildEventHandler() )
Note
When used this way, it is the handler’s auto-termination that indicates the run( )
command is complete.
The handler must properly detect the completion of the run task.
Add an import for mhrc.automation.handler
:
from win32com.shell import shell, shellcon
import mhrc.automation, os, logging
import mhrc.automation.handler
import xml.etree.ElementTree as ET
Replace the Handler
code from the previous section with the following:
class BuildEventHandler(mhrc.automation.handler.BuildEvent):
def _build_event(self, msg, event):
elapsed = int(msg.get('elapsed'))
etype = event.find('type')
phase = etype.get('name')
status = etype.get('status')
project = event.find('project')
prj_name = project.get('name') if project is not None else None
LOG.info("BuildEvt: [%s] %s/%s %d", prj_name, phase, status, elapsed)
return super()._build_event(msg, event)
Here, we are extending a standard BuildEvent
handler.
Its send()
method already looks for build event messages, and forwards them to the _build_event()
method.
Its _build_event()
method looks for matching BEGIN
and END
messages,
and returns StopIteration
when the final END
message is found.
This standard BuildEvent
handler in turn extends an AbstractHandler
,
which implements the required do-nothing close()
method.
As such, most of the required work has already been done for us.
We just need to override _build_event()
, and add return super()._build_event(msg, event)
at the end.
Remove the previous pscad.add_handler( )
call,
and replace the project.run()
call with:
project.run( BuildEventHandler() )
When this script
is run, the following output is produced:
INFO main Launching: PSCAD 4.6.3 (x64) FORTRAN='GFortran 4.6.2' Matlab=None
INFO main Running 'vdiv' (Single Phase Voltage Divider)
INFO main BuildEvt: [None] Workspace/BEGIN 1
INFO main BuildEvt: [vdiv] Project Builder/BEGIN 4
INFO main BuildEvt: [vdiv] Project Compile/BEGIN 4
INFO main BuildEvt: [vdiv] Project Compile/END 71
INFO main BuildEvt: [vdiv] EMTDC MAKE/BEGIN 81
INFO main BuildEvt: [vdiv] EMTDC MAKE/END 388
INFO main BuildEvt: [vdiv] Project Builder/END 397
INFO main BuildEvt: [vdiv] Project Solver/BEGIN 428
INFO main BuildEvt: [vdiv] Project Solver/END 447
INFO main BuildEvt: [vdiv] Mediator/BEGIN 468
INFO main BuildEvt: [vdiv] EMTDC RUN/BEGIN 475
INFO main BuildEvt: [vdiv] Mediator/END 598
INFO main BuildEvt: [vdiv] EMTDC RUN/END 729
INFO main BuildEvt: [None] Workspace/END 729
INFO main Run 'vdiv' complete
Our handler is just displaying the events as they occur.
It could do more interesting things.
For instance, when it receives a BEGIN
message, it could record the elapsed
time;
when it receives a matching END
message,
it could subtract the elapsed
time from the time it recorded earlier,
giving the time required for each task:
class BuildEventHandler(mhrc.automation.handler.BuildEvent):
def __init__(self):
super().__init__()
self._start = {}
def _build_event(self, msg, event):
elapsed = int(msg.get('elapsed'))
etype = event.find('type')
phase = etype.get('name')
project = event.find('project')
prj_name = project.get('name') if project is not None else "[All]"
key = "{} {}".format(prj_name, phase)
if etype.get('status') == 'BEGIN':
self._start[key] = elapsed
else:
msec = elapsed - self._start[key]
LOG.info("%s: %d ms", key, msec)
return super()._build_event(msg, event)
Running this revised script
script would produce:
INFO main Launching: PSCAD 4.6.3 (x64) FORTRAN='GFortran 4.6.2' Matlab=None
INFO main Running 'vdiv' (Single Phase Voltage Divider)
INFO main vdiv Project Compile: 72 ms
INFO main vdiv EMTDC MAKE: 293 ms
INFO main vdiv Project Builder: 395 ms
INFO main vdiv Project Solver: 50 ms
INFO main vdiv Mediator: 128 ms
INFO main vdiv EMTDC RUN: 249 ms
INFO main [All] Workspace: 772 ms
INFO main Run 'vdiv' complete
The handler can store information collected during the run, that may be accessed afterwards. Of course, to do so, you would need to hold onto a reference to the handler:
handler = BuildEventHandler()
project.run( handler )
info = handler.get_interesting_data()
Build & Run Messages¶
Build Messages¶
When PSCAD builds a project, the build messages are recorded. The script can retrieve these build messages as an XML document.
After the project.run(…)
line, add the following:
project.run( BuildEventHandler() )
LOG.info("Run '%s' complete", case['name'])
messages = project.messages()
for msg in messages:
print("%s %s %s" % (msg.scope, msg.status, msg.text))
This script
would produce:
⋮ ⋮ ⋮ ⋮
INFO main vdiv EMTDC RUN: 249 ms
INFO main [All] Workspace: 772 ms
INFO main Run 'vdiv' complete
vdiv normal Generating network and source code 'C:\Users\Public\Documents\PSCAD\4.6.3\Examples x64\tutorial\vdiv.gf46\Station.f'.
vdiv normal Generating network and source code 'C:\Users\Public\Documents\PSCAD\4.6.3\Examples x64\tutorial\vdiv.gf46\Main.f'.
vdiv normal Generating 'C:\Users\Public\Documents\PSCAD\4.6.3\Examples x64\tutorial\vdiv.gf46\vdiv.map'.
vdiv normal The total number of live nodes: 1
vdiv normal Time for Compile: 46ms Make: 0ms
vdiv normal Will execute (1): call C:\Program Files (x86)\GFortran\4.6\bin\gf46vars.bat
vdiv normal Will execute (1): call "C:\Program Files (x86)\GFortran\4.6\bin\gf46vars.bat"
vdiv normal Will execute (2): make -f vdiv.mak
vdiv normal Will execute (2): "C:\Users\Public\Documents\PSCAD\4.6.3\Examples x64\tutorial\vdiv.gf46\vdiv.mak.bat"
vdiv normal Creating EMTDC executable...
vdiv normal C:\Users\Public\Documents\PSCAD\4.6.3\Examples x64\tutorial\vdiv.gf46>call "C:\Program Files (x86)\GFortran\4.6\bin\gf46vars.bat"
vdiv normal Compiling 'Station.f' into object code.
vdiv normal Compiling 'Main.f' into object code.
vdiv normal Linking objects and libraries into binary 'vdiv.exe'
vdiv normal Solve Time = 47ms
Here, we are just extracting the scope, status, and text of the messages. Other fields, such as label and component references could also be extracted.
Run Messages¶
After EMTDC has run the project, the run messages may also be retrieved. Unlike the build messages, the run messages are returned as an unstructured blob of text.
Immediately after the above code, add the following lines:
print("-"*60)
output = project.get_output_text()
print(output)
When run, this change to the script
adds the run messages to the output:
⋮ ⋮ ⋮ ⋮
vdiv normal C:\Users\Public\Documents\PSCAD\4.6.3\Examples x64\tutorial\vdiv.gf46>call "C:\Program Files (x86)\GFortran\4.6\bin\gf46vars.bat"
vdiv normal Linking objects and libraries into binary 'vdiv.exe'
vdiv normal Solve Time = 47ms
------------------------------------------------------------
Initializing Simulation Run
Executing > "C:\Users\Public\Documents\PSCAD\4.6.3\Examples x64\tutorial\vdiv.gf46\\vdiv.30065.bat"
C:\Users\Public\Documents\PSCAD\4.6.3\Examples x64\tutorial\vdiv.gf46>call "C:\Program Files (x86)\GFortran\4.6\bin\gf46vars.bat"
Communications: Connection established.
EMTDC(tm) Version 4.60 R#93051001
Current locale = C
Requested locale = english-us
Current locale = English_United States.1252
****
* EMTDC for PSCAD 4.6: Build# 20180314
****
Time Summary: Start Date: 10-16-2018
Time Summary: Start Time: 13:10:06
The actual plot step: '250.000000'
Number of Matrix Switchings in SS# 1 is: 1
Input file: vdiv.map
Time Summary: Stop Time: 13:10:06
Time Summary: Total CPU Time: 47ms.
Terminating connection.
EMTDC run completed.
Simulation has ended. Status code = 0