}

Introduction

This tutorial introduces you to the joys and pains of writing scripts for TUI.

Scripts are written in Python, and this tutorial does not attempt to teach the language. I strongly recommend the python tutorial at Python's home. Python is a simple and powerful language, and a little knowledge can be a tremendous asset.

The topics are as follows:

  • Basics (Hello script) Your first script. What's with "yield"?
  • Send Commands (DISDark script) Control an instrument. Format variables in a string.
  • Loops (DISCals script) Use a loop to simplify repetitive commands.
  • Refinements (NiceDISCals script) Add a few refinements to DISCals to make a script worth emulating.
  • UserInput (DISDark v2 script) Add user input widgets to your script.
  • Getting Info (DISConfig script) Get current information about an instrument or the telescope and use it in your scripts.
  • Conclusion Where to go for more information.

The sample scripts from this tutorial can be found in tui_root/TUI/Scripts/Tutorial (you may have to ask your sysadmin for the location of tui_root).

Basics

Basics - Hello script

Let's start with the traditional greeting. Create a text file "hello.py" in your home directory (or anywhere convenient) containing the following text. Use tabs for indentation:


def run(sr):
    sr.showMsg("Hello")
    yield sr.waitMS(1000)

Start TUI (if it is not already running) and load your script using Open... in the Scripts menu. A new script window should appear; press "Start" to run your script. This causes the status bar to show "Hello" for 1 second, after which your script ends and the status bar shows "Done".

To reload a script (i.e. after modifying it), select Reload from the contextual pop-up menu for the status bar or any of the buttons along the bottom (Start, etc.). This is useful for developing scripts: you can modify the script in your favorite editor, then reload it in TUI and try it out.

What's With "yield"?

Whenever your script calls an sr.wait... function (i.e. wants to wait for anything), it must use yield, as in:

    yield sr.wait...(...)

This is a painful, but it could be worse. Most languages would force you to break your script into many small functions, each of which would have to be registered as a separate callback function. That style of programming is fine for user interfaces but is needlessly complicated for scripts.

If you forget the "yield", your script will plow ahead instead of waiting, which is a recipe for trouble. However, TUI will catch this problem the next time you call an sr.wait... function, at which point TUI will kill your script, print a message to the status bar and print details to the error log.

Send Commands

Send Commands

Sending commands to an instrument or other actor is straightforward. You need to know the name of the actor (e.g. "dis", "echelle", "tcc") and the command you wish to send. See APO Documentationfor documentation for the instruments, TCC and hub.

This script simply takes a few darks. It is not actually useful (the DIS Expose window can already do this and more) and lacks adequate feedback, but it is a start.

Warning: all exposures should be taken using the hub's <inst>Expose actor rather than by talking directly to the instrument. This makes sure that your images are put in a location where you can find them. It also offers a uniform interface to the instruments. Other commands (e.g. for configuration) can be sent directly to the instrument.

DISDarks script

Be sure you have permission to use DIS before running this example!!!


def run(sr):
    """Sample script to take a series of DIS darks
    and demonstrate using <inst>Expose to take exposures.
    The exposure times and  # of iterations are short so the demo runs quickly.
    """
    expType = "dark"
    expTime = 1  # in seconds
    numExp = 3

    yield sr.waitCmd(
        actor = "disExpose",
        cmdStr = "%s time=%s n=%d name=dis%s" % \
            (expType, expTime, numExp, expType),
        abortCmdStr = "abort",
    )

Notes:

  • Using variables to hold the configuration information may be excessive for such a simple script, but I did it for several reasons:
    • It demonstrates using to % to format data as a string. This is the main way data is formatted as a string in Python, so you should learn it.
    • The settings are at the top, where they are easy to see and document
    • The script easy to adapt, e.g. to run in a loop using different settings each time (as per the next lesson).
  • yield sr.waitCmd(...) starts the expose command and waits for it to finish.
    • If the command fails then the script will terminate. (Early termination will not affect this script because executing the command is the last step, but it can be very useful for longer scripts.)
    • If a user cancels the script while the command is executing, the abort command is sent. Specifying an abort command is strongly recommended for any command that can be aborted. Users are bound to abort your script and they should be able to do so safely and gracefully.

Loops

Loops

Often you will want to execute the same commands many times with different parameters. This is where python lists and for loops come in handy. This script also shows a simple but crude way of displaying more feedback while operating: it opens the DIS Expose window. We'll see how to display the feedback directly in the script window, plus a better way of formatting exposure commands, in the next lesson.

DISCals script

As always, get permission to use DIS before commanding it.


import TUI.TUIModel

def init(sr):
    """Open the DIS Expose window so the user can see what's going on."""
    tuiModel = TUI.TUIModel.getModel()
    tuiModel.tlSet.makeVisible("None.DIS Expose")

def run(sr):
    """Sample script to take a series of DIS calibration images
    and demonstrate looping through data in Python.
    The exposure times and  # of iterations are short so the demo runs quickly.
    """
    # typeTimeNumList is a list of calibration info
    # each element of the list is a list of:
    # - exposure type
    # - exposure time (sec)
    # - number of exposures
    typeTimeNumList = [
        ["flat", 1, 2],
        ["flat", 5, 2],
        ["bias", 0, 2],
        ["dark", 1, 2],
        ["dark", 5, 2],
    ]
    
    for expType, expTime, numExp in typeTimeNumList:
        if expType == "bias":
            # bias, so cannot specify time
            cmdStr = "%s n=%d name=dis%s" % (expType, numExp, expType)
        else:
            cmdStr = "%s time=%s n=%d name=dis%s" % (expType, expTime, numExp, expType)

        yield sr.waitCmd(
            actor = "disExpose",
            cmdStr = cmdStr,
            abortCmdStr = "abort",
        )

Comments:

  • init(sr) runs once, when the script is first loaded. It is often used to add widgets to the script display, but in this case it opens the DIS Expose window to give you feedback as your script runs. We'll use init(sr) to add feedback widgets (eliminating the need to open the DIS Expose window) in the Refinements lesson (NiceDISCals script).
  • The TUI model contains information about the internals of TUI, including tlSet, an object containing the main windows in TUI. To learn more about tlSet see Basic Architecture in the Programming manual.
  • The for loop is doing two things at once: extracting an item [exposure type, exposure time, number of exposures] from the main data list and "unpacking" that item into three separate variables. That is a common python idiom, equivalent to:
    	for expTypeTimeNum in typeTimeNumList:
    		expType, expTime, numExp = expTypeTimeNum
    	
  • The last item in a list may have a comma after it, if desired. I recommend including the comma when each item appears on its own line (as in this example), because it is much easier to add, delete or rearrange items when all lines end with a comma.
  • You must place \ at the end of any line being continued unless the line part of code in (), [] or {}. \ is optional (and usually omitted) for continuing lines that are in (), [] or {}. Thus I omitted it in when defining typeTimeNumList.

Refinements

Refinements

Adding a few refinements to the DISCals script makes a script worth adapting for your own uses.

The improvements are as follows:

Better feedback
The original DISCals script opened the DIS Expose window to give feedback, which is clumsy. Fortunately this easy to fix by borrowing the ExposeStatusWdg, the feedback widget used by exposure windows. ExposeStatusWdg updates its own displays, so it is very easy to use!
The original DISCals script also gave misleading feedback, because there was no hint that multiple sequences were going to occur. This version takes advantage of the "startNum" and "totNum" arguments to disExpose to report how many exposures truly remain in the script. Doing this requires computing the total number of exposures in advance, which requires looping through the data twice: first to count the number of exposures, then to execute the exposures.
Proper file numbering
File numbering should obey the "Seq by File" preference. You could fix this by reading the sequence number preference manually, but the formatExpCmd function in the instrument exposure model takes care of this for you and also handles the details of formatting. formatExpCmd is the recommended way to generate exposure commands for any instrument.
Support for debug mode
Adding sr.debug = False to the initialization lets you easily toggle this value when you want to debug your script. In debug mode, the script only pretends to operate, it doesn't actually talk to the hub or any instruments. Your script can also read this value, which is useful for providing fake data (as you'll see later in the the "Get Info" lesson).
Define your script as a class
If your script needs initialization or cleanup, write it as a class. This allows you to easily pass data between the various functions. Fortunately, conversion is very easy:
  • Indent everything one level
  • Add class ScriptClass(object): to the beginning
  • Change def init(sr) to def __init__(self, sr) (there are two underscrores on either side of "init"; it may look funny, but it's standard python)
  • Change def run(sr) to def run(self, sr) and the same for all other functions.

The resulting script still has no provision for user input, but often that is fine. We'll deal with user input in the next lesson.

NiceDISCals script

As always, get permission to use DIS before commanding it.


from TUI.Inst.ExposeStatusWdg import ExposeStatusWdg
import TUI.Inst.ExposeModel

class ScriptClass(object):
    """Sample script to take a series of DIS calibration images
    and demonstrate looping through data in Python.
    The exposure times and  # of iterations are short so the demo runs quickly.
    """
    def __init__(self, sr):
        """Display the exposure status panel.
        """
        # if True, run in debug-only mode (which doesn't DO anything, it just pretends)
        sr.debug = False
        
        expStatusWdg = ExposeStatusWdg(
            master = sr.master,
            instName = "DIS",
        )
        expStatusWdg.grid(row=0, column=0)
        
        # get the exposure model
        self.expModel = TUI.Inst.ExposeModel.getModel("DIS")
    
    def run(self, sr):
        """Run the script"""
        # typeTimeNumList is a list of calibration info
        # each element of the list is a list of:
        # - exposure type
        # - exposure time (sec)
        # - number of exposures
        typeTimeNumList = [
            ["flat", 1, 2],
            ["flat", 5, 2],
            ["bias", 0, 2],
            ["dark", 1, 2],
            ["dark", 5, 2],
        ]
        
        # compute the total number of exposures
        totNum = 0
        for expType, expTime, numExp in typeTimeNumList:
            totNum += numExp
        
        # take the exposures
        startNum = 1
        for expType, expTime, numExp in typeTimeNumList:
            fileName = "dis_" + expType
            cmdStr = self.expModel.formatExpCmd(
                expType = expType,
                expTime = expTime,
                fileName = fileName,
                numExp = numExp,
                startNum = startNum,
                totNum = totNum,
            )
            startNum += numExp
    
            yield sr.waitCmd(
                actor = self.expModel.actor,
                cmdStr = cmdStr,
                abortCmdStr = "abort",
            )

Comments:

  • This is the first script that displays any widgets. In this case it is just one widget, and one that is smart enough to take care of its own display. But the pattern is always the same:
    • Use "sr.master" as the master, or parent, widget. This is an empty frame intended for your script's widgets. It appears above the status bar and control buttons.
    • Grid or pack widgets to make them show up. Never mix gridding and packing in the same master widget; it usually causes Tk to go into an infinite loop.
    • To pass data between __init__, run and end, use self, as in self.expModel =....
  • Every instrument has its own exposure model, with its own formatExpCmd function. Simply call TUI.ExposeModel.getModel(instName) to get it.

User Input

User Input

This example shows how to add a few input widgets to a script and how to get data from them. This is a fairly artificial example in that one would normally just use the DIS Expose window for this purpose. However, it demonstrates a few useful techniques, including:

  • How to handle a d:m:s time widget with "live" units display
  • How to get data from a widget
  • Some techniques for laying out widgets
  • How to specify help that shows up in the status bar

DISDarks v2 script

Be sure you have permission to use DIS before running this example!!!


import RO.Wdg
import Tkinter
import TUI.Inst.ExposeModel as ExposeModel
from TUI.Inst.ExposeStatusWdg import ExposeStatusWdg

class ScriptClass(object):
    """Take a series of DIS darks with user input.
    """
    def __init__(self, sr):
        """Display exposure status and a few user input widgets.
        """
        # if True, run in debug-only mode (which doesn't DO anything, it just pretends)
        sr.debug = False

        expStatusWdg = ExposeStatusWdg(
            master = sr.master,
            instName = "DIS",
        )
        expStatusWdg.grid(row=0, column=0, sticky="w")
        
        wdgFrame = Tkinter.Frame(sr.master)
     
        gr = RO.Wdg.Gridder(wdgFrame)
        
        self.expModel = ExposeModel.getModel("DIS")
    
        timeUnitsVar = Tkinter.StringVar()
        self.timeWdg = RO.Wdg.DMSEntry (
            master = wdgFrame,
            minValue = self.expModel.instInfo.minExpTime,
            maxValue = self.expModel.instInfo.maxExpTime,
            isRelative = True,
            isHours = True,
            unitsVar = timeUnitsVar,
            width = 10,
            helpText = "Exposure time",
        )
        gr.gridWdg("Time", self.timeWdg, timeUnitsVar)
        
        self.numExpWdg = RO.Wdg.IntEntry(
            master = wdgFrame,
            defValue = 1,
            minValue = 1,
            maxValue = 999,
            helpText = "Number of exposures in the sequence",
        )
        gr.gridWdg("#Exp", self.numExpWdg)
        
        wdgFrame.grid(row=1, column=0, sticky="w")
        
    def run(self, sr):
        """Take a series of DIS darks"""
        expType = "dark"
        expTime = self.timeWdg.getNum()
        numExp = self.numExpWdg.getNum()
    
        fileName = "dis_" + expType
        
        if expTime <= 0:
            raise sr.ScriptError("Specify exposure time")
    
        cmdStr = self.expModel.formatExpCmd(
            expType = expType,
            expTime = expTime,
            fileName = fileName,
            numExp = numExp,
        )
    
        yield sr.waitCmd(
            actor = "disExpose",
            cmdStr = cmdStr,
            abortCmdStr = "abort",
        )

Comments:

  • This is a slightly awkward set of widgets to lay out. There is a large status widget above, with a few small input widgets below. I have chosen to create a new frame wdgFrame to hold the input widgets. This frame is used as the master widget for the input widgets. The input widgets are gridded into the widget frame and the widget frame is gridded into sr.master. Using a hierarchy of widgets like this does have a few pitfalls:
    • It is easy to forget which parent to use, which can lead to a very confusing display or worse (see the next item).
    • You must never try to grid and pack widgets into the same master. This leads to an infinite loop. In fact if your script freezes while loading, that is the first thing to look for.
    • The previous two points together lead me to recommend that you stick to using the gridder and avoid the packer.
  • gr = RO.Wdg.Gridder(wdgFrame) creates a "gridder" for the widget frame. Gridders specialize in laying out rows of input and status widgets, saving you some of the headache of using Tk's raw gridder or packer.
  • The widgets are passed from __init__ to run via self.
  • raise sr.ScriptError(...) will report an error to the status bar and halt your script. You can raise any other exception if you prefer, but then you will log the error message and a traceback to the error log (which is excellent for debugging unexpected errors but intrusive for reporting "normal" errors).
  • The script window will expand as needed to show your widgets. However, the script window is never resizable (though a future version may offer resizability as an option if it is strongly requested).
  • RO.Wdg offers various versions of standard tk widgets including DMSEntry and IntEntry (both used in this script). These offer many advantages over a simple Entry widget including: numerical entry (with input verification and limits) and help text that automatically shows up in the status bar,

Advanced Topic

It is actually possible to create a gridder on sr.master and use it to lay out the status widget and the input widgets. I present the code without comment for folks who understand the Tk gridder and are willing to read the help for RO.Wdg.Gridder.

    gr = RO.Wdg.Gridder(sr.master)
    
    expStatusWdg = ExposeStatusWdg(
        master = sr.master,
        instName = "DIS",
    )
    gr.gridWdg(False, expStatusWdg, colSpan=4)
    sr.master.grid_columnconfigure(3, weight=1)
    ...
    gr.gridWdg("Time", timeWdg, timeUnitsVar)
    ...
    gr.gridWdg("#Exp", numExpWdg)

Get Info

Get Info

It is easy to incorporate information from an instrument or the telescope into your scripts. Most information you might want is available in various "models" within TUI. These include a model for each instrument (TUI.Inst.DIS.DISModel, etc.), a telescope model (TUI.TCC.TCCModel) and the exposure models you have already seen (TUI.Inst.ExposeModel).

Most models primarily consist of a set of "keyword variables", one per keyword that instrument outputs. Keyword variables may be read using sr.getKeyVar or sr.waitKeyVar. Models often also include a few constants, preferences and/or convenience functions (such as formatExpCmd). To obtain a model, import the appropriate code and call getModel() as per the example here. To find out what is in a model, I suggest you read its code. (There are also good ways to get help from within Python; see the Programming manual for details.)

Note that the telescope model contains some important of information about the current instrument, including its name, image scale and size—all information that is not available from the instrument model.

The following example shows a good use of querying information. DIS always does something when you ask its motors to move, even if they are already in the right place. In particular, the gratings are always homed, which is very slow, and the turret detent is temporarily retracted. In the following example, current DIS status is queried and only items that need to be changed are moved.

Note that whenever you read input, you need to think about debug mode (because in that mode you only get None back). In this case, if we're in debug mode then we don't check that the current instrument is DIS. In this example we don't have to mess with the getKeyVar statements because getKeyVar returns None in debug mode, making the commands run, which is perfect.

DISConfig script

Be sure you have permission to use DIS before running this example!!!


    import TUI.TCC.TCCModel
    import TUI.Inst.DIS.DISModel
    from TUI.Inst.DIS.StatusConfigInputWdg import StatusConfigInputWdg
    
    InstName = "DIS"
    
    class ScriptClass(object):
        """Simple script to configure DIS.
        """
        def __init__(self, sr):
            """Display DIS configuration."""
            # if True, run in debug-only mode (which doesn't DO anything, it just pretends)
            sr.debug = False
    
            statusWdg = StatusConfigInputWdg(sr.master)
            statusWdg.grid(row=0, column=0)
        
        def run(self, sr):
            """Configure DIS
            
            It is inefficient to tell DIS to move something that is already
            in the right location, so check each item before moving.
            """
            disModel = TUI.Inst.DIS.DISModel.getModel()
            tccModel = TUI.TCC.TCCModel.getModel()
        
            # settings
            turretPos = 1  # grating set 1 is typically high blue/high red
            maskID = 1
            filterID = 1  # 1 is clear
            rlambda = 7300  # in Angstroms
            blambda = 4400  # in Angstroms
    
            # Make sure the current instrument is DIS
            if not sr.debug:
                currInstName = sr.getKeyVar(self.tccModel.instName)
                if not currInstName.lower().startswith(InstName.lower()):
                    raise sr.ScriptError("%s is not the current instrument!" % InstName)
        
            # notes:
            # - set turret before setting gratings to make sure that
            # disModel.cmdLambdas is for the correct turret.
            # - DIS only moves one motor at a time,
            # so the following code is about as efficient as it gets
            
            if turretPos != sr.getKeyVar(disModel.turretPos):
                yield sr.waitCmd(
                    actor = "dis",
                    cmdStr = "motors turret=%d" % turretPos,
                )
            
            if maskID != sr.getKeyVar(disModel.maskID):
                yield sr.waitCmd(
                    actor = "dis",
                    cmdStr = "motors mask=%d" % maskID,
                )
            
            if filterID != sr.getKeyVar(disModel.filterID):
                yield sr.waitCmd(
                    actor = "dis",
                    cmdStr = "motors filter=%d" % filterID,
                )
            
            # test against disModel.cmdLambdas, not disModel.actLambdas,
            # because the gratings cannot necessarily go *exactly* where requested
            # but do the best they can
            if blambda != sr.getKeyVar(disModel.cmdLambdas, ind=0):
                yield sr.waitCmd(
                    actor = "dis",
                    cmdStr = "motors b%dlambda=%d" % (turretPos, blambda),
                )
            
            if rlambda != sr.getKeyVar(disModel.cmdLambdas, ind=1):
                yield sr.waitCmd(
                    actor = "dis",
                    cmdStr = "motors r%dlambda=%d" % (turretPos, rlambda),
                )
    

Comments:

  • sr.getKeyVar returns the current value of the specified "keyword variable" without waiting. If you prefer to wait (e.g. to get the next value or to make sure there is a valid value) then use yield sr.waitKeyVar instead. Note that sr.wait functions return values in sr.value (because there's no other way when using yield); thus, for example:
            yield sr.waitKeyVar(disModel.cmdLambdas, ind=0)
            blambda = sr.value
            

Conclusion

Conclusion

You have seen how to write a variety of scripts, including scripts that use current status information from instruments and the telescope and that add widgets for user input and output. Now what?

  • Write some scripts! Nothing beats experience, and scripts can save you precious observing time.
  • Read the Scripting Manual. This is a detailed manual for the script runner sr and includes such handy topics as:
    • Where to put your scripts so they are automatically loaded.
    • A simple, handy debug mode.
    • How your script can reliably clean up after itself using the end function.
    • How you can break your script into sub-tasks (functions that wait that you write).
  • Look through existing scripts and perhaps existing TUI code.
  • Look through Programming. It gives you an overview of what is going on "under the hood" in TUI and helps you learn more. It also describes how to create your own TUI windows and where to put them so they load automatically. Writing your own TUI windows is not hard, once you have written some scripts.