whit.

Restructuring PSSE scripts: a simple tutorial

I came across this PSSE python script recently and thought it would serve as a good example to show how simply restructuring the code will make it more readable.

Writing readable code is important, especially if you plan to share your script with someone in another department or organisation. The person you share with can understand and use your code quickly, they may even further improve your code by adding new features. Conversely a difficult to read script is a chore to work with. Potential bugs are difficult to spot and you may miss the meaning of the code.

Can you work out the purpose of the script? What about the major steps it takes? Are the generators added as in-service or out of service generators? Are there any exceptions to this rule? My apologies for the line width of this original file, it won’t fit into many of your browsers. You can download the original file using the “view raw” link at the bottom of the Gist.

addgens_fromcsv.py
from __future__ import with_statement
import os
import sys
import csv

# Location of libraries
sys.path.append("C:\\Code\\trunk\\libs\\psselib")           # psse_utilities
sys.path.append("C:\\Code\\trunk\\libs\\filelib")           # file utils
sys.path.append("C:\\program files\\pti\\psse32\\pssbin")   # psspy

import psspy

def machines():
    """
    Add machines (gens and svcs) to the working case.
    For gens we add all to the case with status off. ECDI will take care of status.
    For SVCs we only want them to be in service in and after the year specified in the CSV.
    (Neither ECDI nor OPF [since fict SVCs control their own bus] will control status of fict SVCs)
    """
    csv_file = 'genlist.csv'
    fict_gens = list(csv.reader(open(csv_file, "r")))
    Qlimit = 700

    for i in range(len(fict_gens)-1):
        busno = int(fict_gens[i+1][0])
        genid = int(fict_gens[i+1][1])
        pgen = int(fict_gens[i+1][2])
        pmax = int(fict_gens[i+1][3])
        status = int(fict_gens[i+1][4])
        excluded = fict_gens[i+1][5]
        vsched = fict_gens[i+1][6]
        print vsched

        if(excluded == 'FALSE'):
            ierr, rval = psspy.busdat(busno ,'PU')
            if(rval > 1.085):
                rval = 1.085
            if(vsched <> ""): # if a value for vsched is written in the csv, use it.
                rval = float(vsched)
            ierr = psspy.plant_data(busno,0,[ rval, 100.0])
            psspy.bus_data_2(busno,[2,_i,_i,_i],[_f,_f,_f],_s)

            # file name will be something like:
            # "C:\WorkingFiles\GenProject\WP_201112_and_beyond\WP_201112_lofl.sav"
            savefilename, snpfilnam = psspy.sfiles()

            # Need to extract the case year from savefilename
            # front part of path deleted to leave something like: "WP_201112_lofl.sav"
            case_year = savefilename.split("\\")
            case_year = case_year[len(case_year)-1]
            # Now extract number from case name. PROVIDED case name starts "WP_201112..."
            case_year = case_year[4:10]
            #ERROR saved file name not getting updated (stays at what is selected)!!!!!!!!
            print "saved file is................................ %s" % savefilename

            # fictitious SVC defined by machine with Pmax == 0
            if(pmax == 0):
                if(int(case_year) < (status-101)): # Subtract 101 from 'status' so SVC will be on in correct year (e.g. 1415 when adding to 1314 case) 
                    psspy.machine_data_2(busno,str(genid),[0,1,0,0,0,0],[pgen,0.0, Qlimit, -Qlimit, pmax, 0.0, 100.0,0.0, 1.0,0.0,0.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0])
                    print "SVC %s is o.o.s.....until year %s..................." % (busno, status)
                else:
                    psspy.machine_data_2(busno,str(genid),[1,1,0,0,0,0],[pgen,0.0, Qlimit, -Qlimit, pmax, 0.0, 100.0,0.0, 1.0,0.0,0.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0])
                    print "SVC %s in service......................." % busno
            else: #Assuming a gen; defined by machine with Pmax != 0
                psspy.machine_data_2(busno,str(genid),[0,1,0,0,0,0],[pgen,0.0, Qlimit, -Qlimit, pmax, 0.0, 100.0,0.0, 1.0,0.0,0.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0])

            psspy.fnsl([1,1,0,1,0,0,99,0])

if __name__ == "__main__":

    machines()

The revised version is a quick attempt at applying some of the following principals to code restructuring:

PEP8 Coding style

We use Python Enhancement Proposal 8 (PEP8) compliant coding techniques for an instant readability boost. Most Python developers write code that adheres to PEP8’s guidelines. Following it yourself will ensure that your code will look more like Python code, and can be picked up by someone else familiar with PEP8.

Accurate Comments

Should be used to document difficult parts of the code. Be careful not to confuse complex code with bad code that should be rewritten. If a section of code requires an inordinate amount of comments to explain how it works, perhaps that section could be rewritten in a simpler manner.

Make sure comments are updated along with any code that is being rewritten. It saves everyone time when the comments match what the code is doing. It is better to have no comments than it is to have incorrect comments.

Magic Number Removal

Numbers that are written throughout the code without any meaning attached. They are confusing to colleagues because it is not immediately obvious to a new reader of the code why the magic number exists. The original code had a significant number of magic numbers, we removed some of them but many remain.

Smaller Functions

We tried to identify common tasks and move that code into its own function. Smaller functions are often easier to understand and test and can be documented as completing one specific task. Here are some warning signs that your function is too long and needs to be split up into smaller functions:

  • Many levels of indentation; and
  • Your function is longer than 200 lines of code.
restructured_psse_script.py
"""
Add machines (gens and svcs) to the working case from a CSV file named:

``genslist.csv ``

This csv file **must** exist in the current directory.  For
gens we add all to the case with status off. ECDI will take care of status.
For SVCs we only want them to be in service in and after the year specified in
the CSV.  (Neither ECDI nor OPF [since fict SVCs control their own bus] will
control status of fict SVCs) NOTE: Script only works correctly IF case name
starts with "WP_201112..." (otherwise case_year will be wrong)
"""

from __future__ import with_statement
import os
import sys
import csv
import logging

logging.basicConfig(
        level=logging.DEBUG,
        format='%(asctime)s %(levelname)s %(message)s',
        filename="add_gens_from_csv.log",
        filemode="w"
)

# Location of libraries
sys.path.append("C:\\Code\\trunk\\libs\\psselib") # psse_utilities
sys.path.append("C:\\Code\\trunk\\libs\\filelib") # file utils
sys.path.append("C:\\program files\\pti\\psse32\\pssbin") # psspy

import psspy
psspy.throwPsseExceptions = True

#----------------------------------------------------------Exceptions
class UnexpectedFileName(Exception):
    """Raised if the psse save file does not look like WP_201112_lofl.sav"""


#---------------------------------------------------------Main event.

def machines(gens_list_csv_filename):
    """
    Add machines (gens and svcs) to the working case.  For gens we add all
    to the case with status off. ECDI will take care of status.  For SVCs we
    only want them to be in service in and after the year specified in the CSV.
    (Neither ECDI nor OPF [since fict SVCs control their own bus] will control
    status of fict SVCs) NOTE: Script only works correctly IF case name starts
    with "WP_201112..." (otherwise case_year will be wrong)
    """

    Qlimit = 700

    full_savfilename, _ = psspy.sfiles()
    case_year = get_year_from_filename(full_savefilename)

    with open(gens_list_csv_filename) as open_csv:
        for row in csv.reader(open_csv):

            busno, genid, pgen, pmax, status = map(int, row[:5])
            excluded, vsched = row[5], row[6]

            # ignore entries marked as excluded.
            if excluded == "TRUE":
                continue

            set_vsched_for_bus(busno, vsched)

            add_machine({
                'busno': busno,
                'genid': str(genid),
                'pgen': pgen,
                'Qlimit': Qlimit,
                'pmax': pmax}, case_year < (status - 101))

            psspy.fnsl([1,1,0,1,0,0,99,0])


#---------------------------------------------------------Helper functions.

def set_vsched_for_bus(busno, vsched):
    """
set the scheduled voltage for the bus to the value supplied, otherwise set
it to the current bus voltage.
"""
    try:
        rval = float(vsched)
    except ValueError
        ierr, rval = psspy.busdat(busno, 'PU')
        rval = max(rval, 1.085)

    logging.debug("setting %s vsched to %0.2f" % (busno, rval))
    psspy.plant_data(busno, 0, [rval, 100.0])
    psspy.bus_data_2(busno, intgar1=2)

def get_year_from_filename(filename):
    """
    Return the case year from a filename. Provided the filename is of the
    format:

    WP_201112_lofl.sav

    raises UnexpectedFileName if filename doesnt fit the format.
    """
    casename = os.path.basename(full_savefilename)
    try:
        caseyear = int(casename.split('_')[1])
    except (IndexError, ValueError):
        logging.error("raising UnexpectedFileName error, couldn't get a date"
                        " from filename %s" % casename)
        raise UnexpectedFileName(
                "I expected a file like WP_201112_lofl.sav"
                " instead I got %s" % casename)

    logging.debug("found year %d from filename %s"
                    % (caseyear, casename))
    return caseyear

def add_machine(machine_info, inservice=False):
    """
    Add machine data given in the dictionary machine_info.
    The machine may be an SVC or a generator.

    Generators have a non-zero pmax. Generators are added as out of service,
    however svcs may be in service.
    """
    machine = machine_info.get

    if get('pmax') == 0:
        logging.debug("SVC %s is %s service" % (
                machine('busno'), "in" if inservice else "out of"))
    else:
        # all generators are added as out of service.
        inservice = 0

    psspy.machine_data_2(
            machine('busno'),
            machine('genid'),
            [int(inservice),1,0,0,0,0],
            [machine('pgen'),0.0,
             machine('Qlimit'),
             -machine('Qlimit'),
             machine('pmax'),
             0.0, 100.0,0.0, 1.0,0.0,0.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0]
    )

if __name__ == "__main__":

    csv_file = 'genslist.csv'

    machines(csv_file)

The new script also boasts status logging to file and we have turned on PSSE exceptions. We will explain why turning on PSSE exceptions is a good idea in future post. Please spend some time comparing both of the files, we’d love to hear if you think we could improve on our readability and why.