Writing PSOUT Files#

Setup#

Writing data to *.psout files is supported beginning with version 2.0 of the mhi.psout module.

Use py -m pip install --upgrade mhi.psout to install or upgrade the mhi.psout module.

Creating a new File#

To read an existing *.psout file, only the name of file is required. To create a new file, additional parameters are needed.

Open Type#

The OpenType enumeration controls what happens when the named file already exists.

Open Types#

OpenType

Description

CREATE_NEW

Create a brand new file; fails if name already exists

OPEN_CREATE

Create a new file or open an existing one for appending

CREATE_OVERWRITE

Create a new file; delete any existing file

CREATE_RENAME

Create a new file, using a new name if given name exists

Page Size#

The PageSize enumeration determines the size of pages (blocks of memory) used to access the file. Using larger pages can be faster, but may result in wasted space.

Page sizes are in powers of 2, starting at 1024 bytes, and ending as 65536.

Reserve Size#

The ReserveSize enumeration determines the initial size of the file in pages. A larger initial size can improve performance, since it will postpone the first resizing operation, which is semi-expensive.

Reserve sizes are in powers of 2, starting at 1024 pages, and ending at 1048576 pages.

Growth Size#

The GrowthSize enumeration determines the incremental size of the file in pages. A larger growth size can improve performance, since fewer resizing operations will be required.

Reserve sizes are in powers of 2, starting at 256 pages, and ending at 1048576 pages.

Example#

Create “SinCos.psout”, overwriting any existing file, using a page size of 1024, and leaving reserve and growth sizes with their default values. Leave the file empty (for now):

from mhi.psout import File, OpenType, PageSize

with File('SinCos.psout', open_type=OpenType.CREATE_OVERWRITE,
          page_size=PageSize.PS_1024) as file:
    pass

Building a Call Tree#

Information in the PSOUT file is organized in a call tree. This allows information to be arranged in groups, with groups inside of groups, and groups inside of those groups, and so on. The grouping may be based on function, voltage levels, geographical areas, etc.

Each file automatically has one call node called the “root”. Additional child call nodes are first added to that node, and then grandchild call nodes can be added to the child nodes, and then great-grandchild call nodes can be added to the grandchild nodes, and so on.

The call tree nodes do not actually contain any data; they only describe of the data. The data will actually be store in traces attached to runs. The call tree merely provides organization of the data.

Types of Call Nodes#

Apart from the root call node, there are 2 other call node types:

Call Types#

CallType

Allows children

Maps to a trace

Created with

MODULE

Yes

No

Call.add_call()

TRACE

No

Yes

Call.add_trace_call()

Information#

Each call must have an ID number. The ID number must be unique among its siblings, but can be reused under a different parent call node.

Apart from the ID, every call node can have a set of variables, which are simply name-value pairs. In each name-value pair, the name must be a string, but the value may be a string, integer, float, boolean, or blob of binary data. There is no requirement for any specific name-value pairs to be used, but it is recommended to at least have a Name=”…” for each call node.

Note that while the ID numbers must be unique within a given parent call node, any Name=”…” names do not need to be.

Example#

Beneath the root node, we’ll create a call tree with 1 module and 3 traces. Two of the traces will be inside the module:

  • <ROOT>

    • a “time” trace

    • a “Trig functions” module

      • a “Sine” trace

      • a “Cosine” trace

from mhi.psout import File, OpenType, PageSize

with File('SinCos.psout', open_type=OpenType.CREATE_OVERWRITE,
            page_size=PageSize.PS_1024) as file:

    # Create Call Tree
    root = file.root
    time_call = root.add_trace_call(100, Name="Time", Unit="s")
    trig_module = root.add_call(200, Name="Trig functions")
    sin_call = trig_module.add_trace_call(1, Name="Sine")
    cos_call = trig_module.add_trace_call(2, Name="Cosine")

Adding a Run#

A run contains a set of traces, indexed by the trace call nodes in the call tree. However, each run does not need contain a trace for every trace call node in the call tree.

Each run must have a unique ID number. If no ID number is given, they are assigned sequentially starting from one.

Additionally, each run may have a set of variables, again, which are simply name-value pairs.

from mhi.psout import File, OpenType, PageSize

with File('SinCos.psout', open_type=OpenType.CREATE_OVERWRITE,
            page_size=PageSize.PS_1024) as file:

    # Create Call Tree
    root = file.root
    time_call = root.add_trace_call(100, Name="Time", Unit="s")
    trig_module = root.add_call(200, Name="Trig functions")
    sin_call = trig_module.add_trace_call(1, Name="Sine")
    cos_call = trig_module.add_trace_call(2, Name="Cosine")

    # Create a 50 Hz run
    run = file.add_run(Description="50 Hertz sin waves")

Add Traces to the Run#

A trace is simply a collection of values, which must all be of the same type.

from math import sin, cos, pi
from mhi.psout import File, OpenType, PageSize

with File('SinCos.psout', open_type=OpenType.CREATE_OVERWRITE,
            page_size=PageSize.PS_1024) as file:

    # Create Call Tree
    root = file.root
    time_call = root.add_trace_call(100, Name="Time", Unit="s")
    trig_module = root.add_call(200, Name="Trig functions")
    sin_call = trig_module.add_trace_call(1, Name="Sine")
    cos_call = trig_module.add_trace_call(2, Name="Cosine")

    # Create a 50 Hz run
    run = file.add_run(Description="50 Hertz sin waves")
    f = 50.0
    w = 2 * pi * f

    # Create a "time" domain
    time = [0.001 * i for i in range(201)]
    time_trace = run.add_trace(time_call, data=time)

    # Create "sin" and "cos" traces
    sine = [sin(w*t) for t in time]
    cosine = [cos(w*t) for t in time]

    run.add_trace(sin_call, domain=time_trace, data=sine)
    run.add_trace(cos_call, domain=time_trace, data=cosine)

Seeing the output#

We can see the results using pyplot from matplotlib. Use py -m pip install matplotlib to install matplotlib:

from matplotlib import pyplot as plt
from mhi.psout import File

with File('SinCos.psout') as file:

    run = file.run(0)

    sine = run.call('Trig functions/Sine')
    cosine = run.call('Trig functions/Cosine')

    plt.plot(sine.domain.data, sine.data, label='Sin')
    plt.plot(cosine.domain.data, cosine.data, label='Cos')

plt.xlabel('s')
plt.legend()
plt.show()
../../_images/sin_cos.png

PSCAD Flavored PSOUT#

PSCAD outputs a more complicated call tree, with additional structural requirements. Enerplot 1.1 only knows how to read “PSCAD-Flavored” PSOUT files.

Requirements#

Call Tree

  • The call tree must have top-level MODULE call node with Name="Root", Description="Root", and Source="Module".

  • Beneath that “Root” node, MODULE call nodes with Name="..." and Source="Module" may be nested to any depth.

  • Inside of those MODULE call nodes, there may be MODULE call nodes with Name="...", Description="Output Channel", and Source="PGB"

    • Inside of the “PGB” node, there must be a MODULE call node with Name="Record", Description="", Source="Data"

      • Finally, inside of the “Record” call nodes, there can be 1 or more TRACE call nodes with Name="PGB:Data", Description="...:PGB:...:#", Source="Trace".

  • A top-level TRACE call node must exist wth Name="PGB:Domain", Description="PGB:Domain", which is used as the domain trace for above “PGB” traces.

Traces

  • The “PGB:Domain” trace must be created with Name="Domain", DataName="PGB:Domain", Group="Domain", Description="Domain", and Component="Domain".

  • The PGB traces must be created with DataName="PGB:Data", Description="Output Channel".

To simplify the creating of the PSOUT file conforming to the above requirements, the following functions have been added to the mhi.psout module:

Example#

Like the earlier example, we’ll create a “SinCos.psout” file, containing a “Trig Funcs” module. We’ll add two “PGB” traces to that module: “Sine” and “Cosine”.

Unlike the previous example, we’ll also add two “runs” to the file, one for “50 Hertz” and one for “60 Hertz”, to demonstrate that capability.

from math import sin, cos, pi
from mhi.psout import File, OpenType, PageSize

with File('SinCos.psout', open_type=OpenType.CREATE_OVERWRITE,
            page_size=PageSize.PS_1024) as file:

    # Create Call Tree
    root = file.make_pscad_root()
    time_call = file.make_pscad_domain(1234567, 'PGB')
    module = root.add_module(200, "Trig Funcs")
    sin_call = module.add_pgb_call(201, "Sine")
    cos_call = module.add_pgb_call(202, "Cosine")

    # Create a "time" samples for all runs
    time = [0.001 * i for i in range(201)]

    for freq in (50, 60):
        run = file.add_run(Description=f"{freq} Hertz sin waves")
        w = 2 * pi * freq

        # Time domain
        time_trace = run.add_pscad_domain(time_call, unit='s', data=time)

        # Create "sin" and "cos" traces
        sine = [sin(w*t) for t in time]
        cosine = [cos(w*t) for t in time]

        run.add_pscad_trace(sin_call, 'Trig', domain=time_trace, data=sine)
        run.add_pscad_trace(cos_call, 'Trig', domain=time_trace, data=cosine)

This SinCos.psout file may now be loaded in Enerplot and the traces plot, as seen here:

../../_images/pgbs.png