whit.

Turn back time using Python and PSSE

Roads? Where we're going, we don't need roads.
DocBack to the Future II
I have a recurring problem, I spend time carefully adding a new load or generation to my case only to have it blow up when I try to solve. It turns out I wasn’t so careful and didn’t check that the voltage levels were similar or my new bus’ voltage angle was set to the default 0 degrees and the connecting bus was 15 degrees.

There are a lot of reasons why adding new equipment to an existing PSSE case or creating a new switching configuration might fail. If we are lucky, our script stops and we can fix the problem, though I have seen some scripts that keep marching on getting nonsense answers right until the end.

I’d like to take you through a new model for altering a PSSE saved case in your Python scripts that will allow to you literally* turn back time when the case blows up and continue with the program.

  • disclaimer: not literally, I don’t have a flux capacitor.

Ok give me two examples so I know what you are talking about

I have a list of 30 network augmentations that need to be made. Each is a proposed new generating asset or replacement of existing plant. I want to try to add all of them to my saved case, and write in a log file the ones that failed. I’ll manually apply those failed ones later - or reject their connection application (hehe).

Another example is writing my own QV curve generator, I want to add a ficticious generating plant, and play with the Q output and voltage set point to measure the system response. Afterwards I want to remove the ficticious plant without having completely ruined the integrity of my case forever. It is well known that PSSE can be a difficult master to re-tune after we do something stupid like add a synchronous condensor and set the voltage set point to 1.1 per unit and re-solve allowing tap stepping.

So how would I benefit by turning back time in those cases?

OK with our 30 augmentations, say there was one dud with data that was subtly insane. So when we add that dud case, PSSE blows up! Start again? No we don’t need to, just turn back time to the last good augmentation and continue with the rest.

The QV curve benefits are quite obvious, and can be translated to many activities outside of QV curve generation. Imagine if it was an every-day simple task to turn back time and get back your last known good solved case I’d pay good money for that, and I know you would too. Instead though read on and you can write your own time-machine for free.

Some theory about database transactions

This entire method of turning back time to the last known good system state was inspired by database theory of all things. Databases have something called a “transaction” which essentially means:

Either everything works according to plan, or we rollback and nothing gets done at all

There is no half-way, either it all works, or we use a giant undo button that removes the series of steps that led to failure. Of course, PSSE has no undo button, but together you and I will build one soon enough, keep reading.

This translates to the PSSE world in the following way:

  • We group a series of instructions like the ones required to add a new generator into a single function.
  • That function is run inside a transaction
  • We attempt to solve at the end or during the transaction
  • We write a check to see if the transaction was successful (e.g. no blow up, voltages healthy etc.)
  • If successful we continue (and laugh quietly at our success)
  • If not successful, we log our failure, rollback our changes and move on (still laughing quietly)

Enough, show me the code

Ok, here it is I have called the function transaction. But you can call it something fancy like de_lorean or time_machine.

transactions.py
"""
Transaction
-----------

Wrap your PSSe  Python code in this transaction and watch it rollback
when the case blows up. 
"""
from __future__ import with_statement
import contextlib
import os
import sys
import tempfile

#----------------------------------------------------------------------------------
# Some junk to import PSSE put in a library!

PSSE_VERSION = 32
PSSE_BASE = os.path.join('c:\\', 'program files', 'pti', 'psse%d' % PSSE_VERSION)
PSSE_LOCATION = os.path.join(PSSE_BASE, 'pssbin')
PSSE_EXAMPLES = os.path.join(PSSE_BASE, 'example')

sys.path.append(PSSE_LOCATION)
os.environ['PATH'] = os.environ['PATH'] + ';' + PSSE_LOCATION

import psspy
psspy.throwPsseExceptions = True


#----------------------------------------------------------------------------
# Transactions.
class TransactionError(psspy.PsseException):
    pass

@contextlib.contextmanager
def transaction(validator=None):
    """
    Begin by saving the current state to a saved
    file. Then read back from that saved file if the validator test
    at the end fails.

    Remember you must solve your case during the transaction. Otherwise
    many of your changes will not have a chance to affect the system yet.

    Arguments:
        validator - A function without arguments to call that will return True
            if the case is valid. Otherwise the case will be rolled back.

            if no validator is supplied, the case will always be rolled back.

    >>> with transaction():
            add_generator()
            # remember to solve your case.
            psspy.fnsl()

    """

    # create a temporary file securely on the operating system.
    (file_handle, temp_save_filename) = tempfile.mkstemp(suffix=".sav")
    os.close(file_handle)
    try:
        ierr = psspy.save(temp_save_filename)
        if ierr: raise TransactionError('Couldnt psspy.save %d' % ierr)

        yield

        # if validator == True don't rollback.
        if validator and validator():
            return

        # ok lets rollback because validator is None or False.
        ierr = psspy.case(temp_save_filename)
        if ierr: raise TransactionError('Couldnt psspy.case %d' % ierr)

    finally:

        try:
            os.remove(temp_save_filename)
        except OSError:
            pass

Here is a short example in the wild

The example is quite minimal. We create a function which adds a generator and expect that one to succeed. However the second function which changes the swing bus to a type 2, we expect that to fail.

example_transaction.py
from __future__ import with_statement
import os

from transactions import transaction, psspy, PSSE_EXAMPLES
psspy.throwPsseExceptions = False

def validate_solved():
    """
    Return true if the last psse case has solved.

    I am counting iteration limit exceeded as not solved in this case.
    """
    return psspy.solved() == 0

def change_swing_bus_no_reason():
    """
    Change the swing bus to a type 2 for no reason other
    than to show the rollback in action.
    """
    psspy.bus_data_2(3011, intgar1=2)

def add_new_generator():
    """
    Add a new fake generator behind a short line to a new
    bus near the swing bus.
    """
    psspy.bus_data_2(
            3012,
            intgar1=2,   # type 2 bus.
            realar1=22)  # base kV.

    psspy.branch_data(
            i=3011,
            j=3012,
            ckt='NW',
            realar2=0.02) # line reactance X)

    psspy.plant_data(
            3012)

    psspy.machine_data_2(
            i=3012,
            id="01",
            realar1=0,   # active gen MW.
            realar3=100) # reactive gen max MVAr.
    

def main():
    psspy.psseinit(8000)
    psspy.case(os.path.join(PSSE_EXAMPLES, 'savnw.sav'))

    # adding the new generator should sovle and pass.
    with transaction(validate_solved):
        add_new_generator()
        psspy.fnsl()

    # changing the swing bus shouldn't solve and will be
    # rolled back.
    with transaction(validate_solved):
        change_swing_bus_no_reason()
        psspy.fnsl()

    print psspy.solved()

if __name__ == "__main__":
    main()

Have a play around with the example as a starting point and see what else you can get the transaction to do.