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.

  1. 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.

  2. The “Public Documents” directory is located at C:\Users\Public\Documents.

  3. 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