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:
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).
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.
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.
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.
<inst>Expose
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:
yield sr.waitCmd(...)
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.
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)
for expTypeTimeNum in typeTimeNumList: expType, expTime, numExp = expTypeTimeNum
Adding a few refinements to the DISCals script makes a script worth adapting for your own uses.
The improvements are as follows:
ExposeStatusWdg
disExpose
formatExpCmd
class ScriptClass(object):
def init(sr)
def __init__(self, sr)
def run(sr)
def run(self, sr)
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.
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", )
__init__
run
end
self
self.expModel =...
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:
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", )
wdgFrame
gr = RO.Wdg.Gridder(wdgFrame)
raise sr.ScriptError(...)
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)
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.)
getModel()
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.
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), )
sr.getKeyVar
yield sr.waitKeyVar
yield sr.waitKeyVar(disModel.cmdLambdas, ind=0) blambda = sr.value
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?
sr