Tutorial

Requirements

Applications

The PSCAD Automation Library requires the following tools are installed:

  • PSCAD (such as PSCAD 5.0)

  • Python 3.x (such as Python 3.8.3)

    • PIP - Python’s package manager (included with Python 3.5 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 PSCAD Automation Library is installed by executing:

py -m mhi.pscad

If multiple versions of Python have been installed, each installation could have its own copy of the Automation Library, so you may want specify the Python version being queried:

py -3.7 -m mhi.pscad

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:\Users\Public\Documents\Manitoba Hydro International\Python\Packages directory:

py -3.5 -m pip install mhi_pscad-2.8.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.

#!/usr/bin/env python3
import mhi.pscad

# Launch PSCAD
pscad = mhi.pscad.launch()

# Load the tutorial workspace
pscad.load(r"C:\Users\Public\Documents\PSCAD\5.0\Examples\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. PSCAD 5.0 or later is installed.

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

Finding the Tutorial

We can relax the last restriction by retrieving the location of “Examples” directory from PSCAD, and retrieving the “Tutorial” workspace relative to that. Doing so will allow the scripts in this tutorial to be downloaded and executed without modification by the reader.

First, add os to the list of imports, so we can use the os.path.join() function. Then, before the pscad.load() statement, retrieve the examples_folder from PSCAD, and then append tutorial to that folder to get the tutorial folder path. Finally, change the pscad.load() statement to load "Tutorial.pscx", but with a folder=tutorial_folder option:

# Locate the tutorial directory
tutorial_dir = os.path.join(pscad.examples_folder, "tutorial")

# Load the tutorial workspace
pscad.load("Tutorial.pswx", folder=tutorial_dir)

See the resulting script.

Logging

Add logging to the current imports, and add the logging.basicConfig statement below:

import mhi.pscad, os, logging

# 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     mhi.pscad                  Automation server port: 54879
INFO     mhi.common.process         Launching C:\Program Files (x86)\PSCADBeta x64\bin\win64\PSCADBeta.exe
INFO     mhi.common.process             Args: ['/startup:au', '/port:54879', '/splash:false']
INFO     mhi.pscad                  Connecting to localhost:54879
INFO     mhi.pscad.pscad            Loading ['C:\\Users\\Public\\Documents\\Pscad\\Beta\\Examples\\tutorial\\Tutorial.pswx']
INFO     mhi.pscad.pscad            Run all simulation sets

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, mhi.pscad, mhi.common, ...)
logging.getLogger('mhi').setLevel(logging.WARNING)

LOG = logging.getLogger('main')

Next, add logging lines to print out the location of the “tutorial” directory:

LOG.info("Tutorial directory: %s", tutorial_dir)

If you now run the script, you should see something similar to following output. Again, the path may be different, which is of course the whole point of this exercise:

INFO     main                       Tutorial directory: C:\Users\Public\Documents\Pscad\Beta\Examples\tutorial

Error Handling

If the automation library 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 commands following the launch of PSCAD with the following, to create a more fault-tolerant script:

if pscad:

    # Locate the tutorial directory
    tutorial_dir = os.path.join(pscad.examples_folder, "tutorial")
    LOG.info("Tutorial directory: %s", tutorial_dir)

    try:
        # Load the tutorial workspace
        pscad.load("Tutorial.pswx", folder=tutorial_dir)

        # 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() call:

# Launch PSCAD
pscad = mhi.pscad.launch(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.

PSCAD Version

The launch() 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() choose the version, the script can specify the exact version to use with the version=... and/or x64=... parameters. For example, to launch the 64-bit version of PSCAD 5.0, use:

pscad = mhi.pscad.launch(version='5.0', x64=True)

The Automation Library may be queried to determine which versions of PSCAD are installed. Replacing the launch() statement with the following will mimic its selection process:

versions = mhi.pscad.versions()
LOG.info("PSCAD Versions: %s", versions)

# Skip any 'Alpha' versions, if other choices exist
vers = [(ver, x64) for ver, x64 in versions if ver != 'Alpha']
if len(vers) > 0:
    versions = vers

# Skip any 'Beta' versions, if other choices exist
vers = [(ver, x64) for ver, x64 in versions if ver != 'Beta']
if len(vers) > 0:
    versions = vers

# Skip any 32-bit versions, if other choices exist
vers = [(ver, x64) for ver, x64 in versions if x64]
if len(vers) > 0:
    versions = vers

LOG.info("   After filtering: %s", versions)

# Of any remaining versions, choose the "lexically largest" one.
version, x64 = sorted(versions)[-1]
LOG.info("   Selected PSCAD version: %s %d-bit", version, 64 if x64 else 32)

# Launch PSCAD
pscad = mhi.pscad.launch(minimize=True, version=version, x64=x64)

With the above changes to the script, the output may be:

INFO     main                       PSCAD Versions: [('4.6.3', True), ('5.0', True), ('5.0.0', True), ('Beta', True)]
INFO     main                          After filtering: [('4.6.3', True), ('5.0', True), ('5.0.0', True)]
INFO     main                          Selected PSCAD version: 5.0.0 64-bit
INFO     main                       Tutorial directory: C:\Users\Public\Documents\Pscad\5.0\Examples\tutorial

Fortran Version

In a similar fashion, the Automation Library may be queried for which versions of FORTRAN are installed.

Before the launch() line, add the following:

# Get all installed FORTRAN compiler versions
fortrans = mhi.pscad.fortran_versions()
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)

Then create a collection of settings to pass in to PSCAD in the launch call:

# Launch PSCAD
LOG.info("Launching: %s  FORTRAN=%r", version, fortran)
settings = { 'fortran_version': fortran }
pscad = mhi.pscad.launch(minimize=True, version=version, x64=x64,
                         settings=settings)

With the above changes to the script, the output may be:

INFO     main                       PSCAD Versions: [('4.6.3', True), ('5.0', True), ('5.0.0', True), ('Beta', True)]
INFO     main                          After filtering: [('4.6.3', True), ('5.0', True), ('5.0.0', True)]
INFO     main                          Selected PSCAD version: 5.0.0 64-bit
INFO     main                       FORTRAN Versions: ['GFortran 4.2.1', 'GFortran 4.6.2', 'GFortran 8.1']
INFO     main                          After filtering: ['GFortran 4.2.1', 'GFortran 4.6.2', 'GFortran 8.1']
INFO     main                          Selected FORTRAN version: GFortran 8.1
INFO     main                       Launching: 5.0.0  FORTRAN='GFortran 8.1'
INFO     main                       Tutorial directory: C:\Users\Public\Documents\Pscad\5.0\Examples\tutorial

Matlab Version

In a similar fashion, the Automation Library 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.

After the code to select the FORTRAN version, add the following:

# Get all installed Matlab versions
matlabs = mhi.pscad.matlab_versions()
LOG.info("Matlab Versions: %s", matlabs)

# Get the highest installed version of Matlab:
matlab = sorted(matlabs)[-1] if matlabs else ''
LOG.info("   Selected Matlab version: %s", matlab)

Then add matlab_version=matlab to the settings:

# Launch PSCAD
LOG.info("Launching: %s  FORTRAN=%r   Matlab=%r", version, fortran, matlab)
settings = { 'fortran_version': fortran, 'matlab_version': matlab }

With the above changes to the script, the output may be:

INFO     main                       PSCAD Versions: [('4.6.3', True), ('5.0', True), ('5.0.0', True), ('Beta', True)]
INFO     main                          After filtering: [('4.6.3', True), ('5.0', True), ('5.0.0', True)]
INFO     main                          Selected PSCAD version: 5.0.0 64-bit
INFO     main                       FORTRAN Versions: ['GFortran 4.2.1', 'GFortran 4.6.2', 'GFortran 8.1']
INFO     main                          After filtering: ['GFortran 4.2.1', 'GFortran 4.6.2', 'GFortran 8.1']
INFO     main                          Selected FORTRAN version: GFortran 8.1
INFO     main                       Matlab Versions: []
INFO     main                          Selected Matlab version: 
INFO     main                       Launching: 5.0.0  FORTRAN='GFortran 8.1'   Matlab=''
INFO     main                       Tutorial directory: C:\Users\Public\Documents\Pscad\5.0\Examples\tutorial

PSCAD Settings

To ensure reliable, repeatable tests, the automation library may instruct PSCAD to use the default settings, ignoring any settings stored in the user profile:

pscad = mhi.pscad.launch(load_user_profile=False)

In addition to passing in settings during launch, the pscad.settings() method may 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:

#!/usr/bin/env python3
import mhi.pscad

pscad = mhi.pscad.launch(minimize=True)

for key, value in sorted(pscad.settings().items()):
    print("%33s: %s" % (key, value))

pscad.quit()

And produces output similar to:

                   AvoidLocalExec: YES
                DetectedCoreCount: 
                    EMTDC_Version: 
                           Folder: 
            LCP_MaxConcurrentExec: 8
                      LCP_Version: 
                      LanguageDir: $(HomeDir)\Languages\en-us
                    MEDIC_Version: 
                 MaxConcurrentSim: 8
                  MenuTooltipFile: $(LanguageDir)\Tooltips\MenuTooltips.xml
                   SocketPortBase: 30000
          TerminateInactiveHelper: NO
                      VersionText: Not installed
                  active_graphics: 2_S_
                adv_changed_color: red
               adv_excluded_color: blue
              adv_highlight_color: #ffff7538
               adv_included_color: lime
                       agent_show: True
                autosave_interval: NO_ACTION
                    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)

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, for the remaining Project cases, we call the Project.run() method on each one, in succession.

Replace the pscad.run_all_simulation_sets() line with the following:

        # Get a list of all projects
        projects = pscad.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                       PSCAD Versions: [('4.6.3', True), ('5.0', True), ('5.0.0', True), ('Beta', True)]
INFO     main                          After filtering: [('4.6.3', True), ('5.0', True), ('5.0.0', True)]
INFO     main                          Selected PSCAD version: 5.0.0 64-bit
INFO     main                       FORTRAN Versions: ['GFortran 4.2.1', 'GFortran 4.6.2', 'GFortran 8.1']
INFO     main                          After filtering: ['GFortran 4.2.1', 'GFortran 4.6.2', 'GFortran 8.1']
INFO     main                          Selected FORTRAN version: GFortran 8.1
INFO     main                       Matlab Versions: []
INFO     main                          Selected Matlab version: 
INFO     main                       Launching: 5.0.0  FORTRAN='GFortran 8.1'   Matlab=''
INFO     main                       Tutorial directory: C:\Users\Public\Documents\Pscad\5.0\Examples\tutorial
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:

        # Get the list of simulation sets
        sim_sets = pscad.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 = pscad.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.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                       PSCAD Versions: [('4.6.3', True), ('5.0', True), ('5.0.0', True), ('Beta', True)]
INFO     main                          After filtering: [('4.6.3', True), ('5.0', True), ('5.0.0', True)]
INFO     main                          Selected PSCAD version: 5.0.0 64-bit
INFO     main                       FORTRAN Versions: ['GFortran 4.2.1', 'GFortran 4.6.2', 'GFortran 8.1']
INFO     main                          After filtering: ['GFortran 4.2.1', 'GFortran 4.6.2', 'GFortran 8.1']
INFO     main                          Selected FORTRAN version: GFortran 8.1
INFO     main                       Matlab Versions: []
INFO     main                          Selected Matlab version: 
INFO     main                       Launching: 5.0.0  FORTRAN='GFortran 8.1'   Matlab=''
INFO     main                       Tutorial directory: C:\Users\Public\Documents\Pscad\5.0\Examples\tutorial
INFO     main                       Simulation sets: ['default']
INFO     main                       Running simulation set 'default'
INFO     main                       Simulation set 'default' complete

If, instead of loading "Tutorial.pswx", we only loaded the "vdiv.pscx" project:

        # Load only the 'voltage divider' project
        pscad.load("vdiv.pscx", folder=tutorial_dir)

there are no simulation sets, so the else: path is taken in our script, and the vdiv project is run:

INFO     main                       PSCAD Versions: [('4.6.3', True), ('5.0', True), ('5.0.0', True), ('Beta', True)]
INFO     main                          After filtering: [('4.6.3', True), ('5.0', True), ('5.0.0', True)]
INFO     main                          Selected PSCAD version: 5.0.0 64-bit
INFO     main                       FORTRAN Versions: ['GFortran 4.2.1', 'GFortran 4.6.2', 'GFortran 8.1']
INFO     main                          After filtering: ['GFortran 4.2.1', 'GFortran 4.6.2', 'GFortran 8.1']
INFO     main                          Selected FORTRAN version: GFortran 8.1
INFO     main                       Matlab Versions: []
INFO     main                          Selected Matlab version: 
INFO     main                       Launching: 5.0.0  FORTRAN='GFortran 8.1'   Matlab=''
INFO     main                       Tutorial directory: C:\Users\Public\Documents\Pscad\5.0\Examples\tutorial
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:

class Handler:

    def send(self, evt):
        if evt:
            LOG.info("%s", evt)
        else:
            LOG.info("TICK")

    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 three lines:

if pscad:

    handler = Handler()
    pscad.subscribe('load-events', handler)
    pscad.subscribe('build-events', handler)

After downgrading some of the earlier LOG.info(...) messages to LOG.debug(...) message, if we run this script, we might see:

INFO     main                       PSCAD Versions: [('4.6.3', True), ('5.0', True), ('5.0.0', True), ('Beta', True)]
INFO     main                          After filtering: [('4.6.3', True), ('5.0', True), ('5.0.0', True)]
INFO     main                          Selected PSCAD version: 5.0.0 64-bit
INFO     main                       FORTRAN Versions: ['GFortran 4.2.1', 'GFortran 4.6.2', 'GFortran 8.1']
INFO     main                          After filtering: ['GFortran 4.2.1', 'GFortran 4.6.2', 'GFortran 8.1']
INFO     main                          Selected FORTRAN version: GFortran 8.1
INFO     main                       Matlab Versions: []
INFO     main                          Selected Matlab version: 
INFO     main                       Launching: 5.0.0  FORTRAN='GFortran 8.1'   Matlab=''
INFO     main                       Tutorial directory: C:\Users\Public\Documents\Pscad\5.0\Examples\tutorial
INFO     main                       {'event': 'LoadEvent', 'status': 'BEGIN', 'file-type': 'files'}
INFO     main                       {'event': 'LoadEvent', 'status': 'BEGIN', 'file-type': 'project', 'file': 'C:\\Users\\Public\\Documents\\Pscad\\5.0\\Examples\\tutorial\\vdiv.pscx'}
INFO     main                       {'event': 'LoadEvent', 'status': 'END', 'file-type': 'project', 'file': 'C:\\Users\\Public\\Documents\\Pscad\\5.0\\Examples\\tutorial\\vdiv.pscx', 'name': 'vdiv', 'type': 'Case', 'Description': 'Single Phase Voltage Divider'}
INFO     main                       {'event': 'LoadEvent', 'status': 'END', 'file-type': 'files'}
INFO     main                       Running 'vdiv' (Single Phase Voltage Divider)
INFO     main                       {'event': 'BuildEvent', 'name': 'Workspace', 'status': 'BEGIN'}
INFO     main                       {'event': 'BuildEvent', 'name': 'Project Builder', 'status': 'BEGIN', 'project': 'vdiv'}
INFO     main                       {'event': 'BuildEvent', 'name': 'Project Compile', 'status': 'BEGIN', 'project': 'vdiv'}
INFO     main                       {'event': 'BuildEvent', 'name': 'Project Compile', 'status': 'END', 'project': 'vdiv'}
INFO     main                       {'event': 'BuildEvent', 'name': 'Project Solver', 'status': 'BEGIN', 'project': 'vdiv'}
INFO     main                       {'event': 'BuildEvent', 'name': 'Project Solver', 'status': 'END', 'project': 'vdiv'}
INFO     main                       {'event': 'BuildEvent', 'name': 'EMTDC MAKE', 'status': 'BEGIN', 'project': 'vdiv'}
INFO     main                       {'event': 'BuildEvent', 'name': 'EMTDC MAKE', 'status': 'END', 'project': 'vdiv'}
INFO     main                       {'event': 'BuildEvent', 'name': 'Project Builder', 'status': 'END', 'project': 'vdiv'}
INFO     main                       {'event': 'BuildEvent', 'name': 'Mediator', 'status': 'BEGIN', 'project': 'vdiv', 'rank': 1}
INFO     main                       {'event': 'BuildEvent', 'name': 'EMTDC RUN', 'status': 'BEGIN', 'project': 'vdiv'}
INFO     main                       {'event': 'BuildEvent', 'name': 'Mediator', 'status': 'END', 'project': 'vdiv', 'rank': 1}
INFO     main                       {'event': 'BuildEvent', 'name': 'Workspace', 'status': 'END'}
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.unsubscribe( eventname ).

    finally:
        # Exit PSCAD
        pscad.unsubscribe('build-events')
        pscad.unsubscribe('load-events')
        pscad.quit()

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, evt):
        if evt:
            LOG.info("%s", evt)
            if evt['status'] == 'END' and evt.get('file-type', None) == 'files':
                raise StopIteration

        else:
            LOG.info("TICK")

When this new script is run, this output results:

INFO     main                       PSCAD Versions: [('4.6.3', True), ('5.0', True), ('5.0.0', True), ('Beta', True)]
INFO     main                          After filtering: [('4.6.3', True), ('5.0', True), ('5.0.0', True)]
INFO     main                          Selected PSCAD version: 5.0.0 64-bit
INFO     main                       FORTRAN Versions: ['GFortran 4.2.1', 'GFortran 4.6.2', 'GFortran 8.1']
INFO     main                          After filtering: ['GFortran 4.2.1', 'GFortran 4.6.2', 'GFortran 8.1']
INFO     main                          Selected FORTRAN version: GFortran 8.1
INFO     main                       Matlab Versions: []
INFO     main                          Selected Matlab version: 
INFO     main                       Launching: 5.0.0  FORTRAN='GFortran 8.1'   Matlab=''
INFO     main                       Tutorial directory: C:\Users\Public\Documents\Pscad\5.0\Examples\tutorial
INFO     main                       {'event': 'LoadEvent', 'status': 'BEGIN', 'file-type': 'files'}
INFO     main                       {'event': 'LoadEvent', 'status': 'BEGIN', 'file-type': 'project', 'file': 'C:\\Users\\Public\\Documents\\Pscad\\5.0\\Examples\\tutorial\\vdiv.pscx'}
INFO     main                       {'event': 'LoadEvent', 'status': 'END', 'file-type': 'project', 'file': 'C:\\Users\\Public\\Documents\\Pscad\\5.0\\Examples\\tutorial\\vdiv.pscx', 'name': 'vdiv', 'type': 'Case', 'Description': 'Single Phase Voltage Divider'}
INFO     main                       {'event': 'LoadEvent', 'status': 'END', 'file-type': 'files'}
INFO     main                       Running 'vdiv' (Single Phase Voltage Divider)
INFO     main                       {'event': 'BuildEvent', 'name': 'Workspace', 'status': 'BEGIN'}
INFO     main                       {'event': 'BuildEvent', 'name': 'Project Builder', 'status': 'BEGIN', 'project': 'vdiv'}
INFO     main                       {'event': 'BuildEvent', 'name': 'Project Compile', 'status': 'BEGIN', 'project': 'vdiv'}
INFO     main                       {'event': 'BuildEvent', 'name': 'Project Compile', 'status': 'END', 'project': 'vdiv'}
INFO     main                       {'event': 'BuildEvent', 'name': 'Project Solver', 'status': 'BEGIN', 'project': 'vdiv'}
INFO     main                       {'event': 'BuildEvent', 'name': 'Project Solver', 'status': 'END', 'project': 'vdiv'}
INFO     main                       {'event': 'BuildEvent', 'name': 'EMTDC MAKE', 'status': 'BEGIN', 'project': 'vdiv'}
INFO     main                       {'event': 'BuildEvent', 'name': 'EMTDC MAKE', 'status': 'END', 'project': 'vdiv'}
INFO     main                       {'event': 'BuildEvent', 'name': 'Project Builder', 'status': 'END', 'project': 'vdiv'}
INFO     main                       {'event': 'BuildEvent', 'name': 'Mediator', 'status': 'BEGIN', 'project': 'vdiv', 'rank': 1}
INFO     main                       {'event': 'BuildEvent', 'name': 'EMTDC RUN', 'status': 'BEGIN', 'project': 'vdiv'}
INFO     main                       {'event': 'BuildEvent', 'name': 'Mediator', 'status': 'END', 'project': 'vdiv', 'rank': 1}
INFO     main                       {'event': 'BuildEvent', 'name': 'Workspace', 'status': 'END'}
INFO     main                       Run 'vdiv' complete
WARNING  mhi.pscad.pscad            Not currently subscribed to load-events events

The “Not currently subscribed to load-events events” message is generated because we are still trying to unsubscribe to “load-events”, but the handler uninstalled itself.

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.subscribe("build-events", handler)
project.run()
pscad.unsubscribe("build-events")

This is a lot of boiler-plate code. As a convenience, the automation library will perform the subscribe and unsubscribe calls itself, if the handler is passed to the run( ) command directly:

project.run( BuildEventHandler() )

Add an import for mhi.pscad.handler:

import mhi.pscad, os, logging
import mhi.pscad.handler

Replace the Handler code from the previous section with the following:

class BuildEventHandler(mhi.pscad.handler.BuildEvent):

    def _build_event(self, phase, status, project, elapsed, **kwargs):

        LOG.info("BuildEvt: [%s] %s/%s %.3f", project, phase, status, elapsed)

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, as well as 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().

Remove the previous pscad.subscribe( ) call, and replace the project.run() call with:

                project.run(BuildEventHandler());

When this script is run, the following output is produced:

INFO     main                       PSCAD Versions: [('4.6.3', True), ('5.0', True), ('5.0.0', True), ('Beta', True)]
INFO     main                          After filtering: [('4.6.3', True), ('5.0', True), ('5.0.0', True)]
INFO     main                          Selected PSCAD version: 5.0.0 64-bit
INFO     main                       FORTRAN Versions: ['GFortran 4.2.1', 'GFortran 4.6.2', 'GFortran 8.1']
INFO     main                          After filtering: ['GFortran 4.2.1', 'GFortran 4.6.2', 'GFortran 8.1']
INFO     main                          Selected FORTRAN version: GFortran 8.1
INFO     main                       Matlab Versions: []
INFO     main                          Selected Matlab version: 
INFO     main                       Launching: 5.0.0  FORTRAN='GFortran 8.1'   Matlab=''
INFO     main                       Tutorial directory: C:\Users\Public\Documents\Pscad\5.0\Examples\tutorial
INFO     main                       Running 'vdiv' (Single Phase Voltage Divider)
INFO     main                       BuildEvt: [None] Workspace/BEGIN 0.187
INFO     main                       BuildEvt: [vdiv] Project Builder/BEGIN 0.352
INFO     main                       BuildEvt: [vdiv] Project Compile/BEGIN 0.354
INFO     main                       BuildEvt: [vdiv] Project Compile/END 0.892
INFO     main                       BuildEvt: [vdiv] Project Solver/BEGIN 1.144
INFO     main                       BuildEvt: [vdiv] Project Solver/END 1.185
INFO     main                       BuildEvt: [vdiv] EMTDC MAKE/BEGIN 1.812
INFO     main                       BuildEvt: [vdiv] EMTDC MAKE/END 4.444
INFO     main                       BuildEvt: [vdiv] Project Builder/END 4.576
INFO     main                       BuildEvt: [vdiv] Mediator/BEGIN 4.820
INFO     main                       BuildEvt: [vdiv] EMTDC RUN/BEGIN 4.832
INFO     main                       BuildEvt: [vdiv] Mediator/END 5.280
INFO     main                       BuildEvt: [None] Workspace/END 5.281
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(mhi.pscad.handler.BuildEvent):

    def __init__(self):
        super().__init__()
        self._start = {}

    def _build_event(self, phase, status, project, elapsed, **kwargs):

        key = (project, phase)
        if status == 'BEGIN':
            self._start[key] = elapsed
        else:
            sec = elapsed - self._start[key]
            name = project if project else '[All]'
            LOG.info("%s %s: %.3f sec", name, phase, sec)

Running this revised script script would produce:

INFO     main                       PSCAD Versions: [('4.6.3', True), ('5.0', True), ('5.0.0', True), ('Beta', True)]
INFO     main                          After filtering: [('4.6.3', True), ('5.0', True), ('5.0.0', True)]
INFO     main                          Selected PSCAD version: 5.0.0 64-bit
INFO     main                       FORTRAN Versions: ['GFortran 4.2.1', 'GFortran 4.6.2', 'GFortran 8.1']
INFO     main                          After filtering: ['GFortran 4.2.1', 'GFortran 4.6.2', 'GFortran 8.1']
INFO     main                          Selected FORTRAN version: GFortran 8.1
INFO     main                       Matlab Versions: []
INFO     main                          Selected Matlab version: 
INFO     main                       Launching: 5.0.0  FORTRAN='GFortran 8.1'   Matlab=''
INFO     main                       Tutorial directory: C:\Users\Public\Documents\Pscad\5.0\Examples\tutorial
INFO     main                       Running 'vdiv' (Single Phase Voltage Divider)
INFO     main                       vdiv Project Compile: 0.770 sec
INFO     main                       vdiv Project Solver: 0.122 sec
INFO     main                       vdiv EMTDC MAKE: 2.624 sec
INFO     main                       vdiv Project Builder: 5.004 sec
INFO     main                       vdiv Mediator: 0.482 sec
INFO     main                       [All] Workspace: 6.075 sec
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.

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                       Run 'vdiv' complete
vdiv  normal  Removed file: C:\Users\Public\Documents\Pscad\5.0\Examples\tutorial\vdiv.gf81_x86\LicenseConfig.cfg
vdiv  normal  Removed file: C:\Users\Public\Documents\Pscad\5.0\Examples\tutorial\vdiv.gf81_x86\Main.dta
vdiv  normal  Removed file: C:\Users\Public\Documents\Pscad\5.0\Examples\tutorial\vdiv.gf81_x86\Main.f
vdiv  normal  Removed file: C:\Users\Public\Documents\Pscad\5.0\Examples\tutorial\vdiv.gf81_x86\Main.o
vdiv  normal  Removed file: C:\Users\Public\Documents\Pscad\5.0\Examples\tutorial\vdiv.gf81_x86\Station.dta
vdiv  normal  Removed file: C:\Users\Public\Documents\Pscad\5.0\Examples\tutorial\vdiv.gf81_x86\Station.f
vdiv  normal  Removed file: C:\Users\Public\Documents\Pscad\5.0\Examples\tutorial\vdiv.gf81_x86\Station.o
vdiv  normal  Removed file: C:\Users\Public\Documents\Pscad\5.0\Examples\tutorial\vdiv.gf81_x86\vdiv.exe
vdiv  normal  Removed file: C:\Users\Public\Documents\Pscad\5.0\Examples\tutorial\vdiv.gf81_x86\vdiv.mak
vdiv  normal  Removed file: C:\Users\Public\Documents\Pscad\5.0\Examples\tutorial\vdiv.gf81_x86\vdiv.map
vdiv  normal  Removed file: C:\Users\Public\Documents\Pscad\5.0\Examples\tutorial\vdiv.gf81_x86\vdiv_1.config
vdiv  normal  Removed file: C:\Users\Public\Documents\Pscad\5.0\Examples\tutorial\vdiv.gf81_x86\vdiv_30000.bat
vdiv  normal  Removed file: C:\Users\Public\Documents\Pscad\5.0\Examples\tutorial\vdiv.gf81_x86\vdiv_mak.bat
vdiv  normal  Removed 13 files
vdiv  normal  Completed cleaning of temporary files and data.
vdiv  normal  Begin compiling with PSCAD Version 5.0.0
vdiv  normal  Generating 'C:\Users\Public\Documents\Pscad\5.0\Examples\tutorial\vdiv.gf81_x86\vdiv.map'.
vdiv  normal  The total number of live nodes: 1
vdiv  normal  Coding .mak file: 'C:\Users\Public\Documents\Pscad\5.0\Examples\tutorial\vdiv.gf81_x86\\vdiv.mak'
vdiv  normal  Time for Compile: 46ms  Make: 0ms
vdiv  normal  Solve Time = 47ms
vdiv  normal  Will execute (1): call C:\Program Files (x86)\GFortran\8.1\x86\bin\gf81vars.bat
vdiv  normal  Will execute (1): call "C:\Program Files (x86)\GFortran\8.1\x86\bin\gf81vars.bat"
vdiv  normal  Will execute (2): make -j -f vdiv.mak
vdiv  normal  Will execute (2): "C:\Users\Public\Documents\Pscad\5.0\Examples\tutorial\vdiv.gf81_x86\vdiv_mak.bat"
vdiv  normal  Creating EMTDC executable...
vdiv  normal  C:\Users\Public\Documents\Pscad\5.0\Examples\tutorial\vdiv.gf81_x86>call "C:\Program Files (x86)\GFortran\8.1\x86\bin\gf81vars.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  Finished compiling with PSCAD Version 5.0.0

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.output()
                print(output)

When run, this change to the script adds the run messages to the output:

⋮               ⋮               ⋮               ⋮
vdiv  normal  C:\Users\Public\Documents\Pscad\5.0\Examples\tutorial\vdiv.gf81_x86>call "C:\Program Files (x86)\GFortran\8.1\x86\bin\gf81vars.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  Finished compiling with PSCAD Version 5.0.0
------------------------------------------------------------
Initializing Simulation Run
Executing > "C:\Users\Public\Documents\Pscad\5.0\Examples\tutorial\vdiv.gf81_x86\\vdiv_30000.bat"
Communications: Connection established.
C:\Users\Public\Documents\Pscad\5.0\Examples\tutorial\vdiv.gf81_x86>call "C:\Program Files (x86)\GFortran\8.1\x86\bin\gf81vars.bat" 
EMTDC(tm) Version 5.00 R#93051001
Time Summary: Start Date: 10-16-2018
Time Summary: Start Time: 13:10:06
The actual plot step: '250.000000' 
Time Summary: Stop Time: 13:10:06
Time Summary: Elapsed Time: 00:00:00
Time Summary: Total CPU Time: 47ms.
Terminating connection.
EMTDC run completed.