SAL Scripts

SAL scripts are programs that perform coordinated telescope and instrument control operations, such as “slew to a target and take an image”, or “take a series of flats”. SAL scripts are similar to CSCs in that they communicate via SAL messages, but a SAL script is run once and then it quits, whereas CSCs may run for months at a time. SAL scripts are typically written in Python; see Python SAL Scripts for more information.

Technically a SAL script is any SAL component that supports the Script API defined in ts_xml and follows these rules:

  • The script is a command-line executable that takes a single required command line argument: the index of the SAL component.

  • When run, the script must start in the lsst.ts.idl.enums.Script.ScriptState.UNCONFIGURED state and output the description event.

  • When the script is in the lsst.ts.idl.enums.Script.ScriptState.UNCONFIGURED state it can be configured with the configure command (and the command must be rejected in any other state):

    • If the configure command succeeds, the script must report the metadata event and state lsst.ts.idl.enums.Script.ScriptState.CONFIGURED.

    • If the configure command fails the script must report state lsst.ts.idl.enums.Script.ScriptState.FAILED and exit.

  • Once the state is lsst.ts.idl.enums.Script.ScriptState.CONFIGURED, the script can have its group ID set with the setGroupId command (and the command must be rejected in any other state):

    • If the setGroupId command succeeds, the script must output a state event with the groupId field set to the new group ID. Note that the script state remains lsst.ts.idl.enums.Script.ScriptState.CONFIGURED. Thus the setGroupId command may be issued multiple times; this allows the script queue to set the group ID, clear it, then set it again.

  • Once the group ID is set (not blank), the script can be run with the run command (which must be rejected if the state is not lsst.ts.idl.enums.Script.ScriptState.CONFIGURED or the group ID is blank):

    • When the script starts running it must report state lsst.ts.idl.enums.Script.ScriptState.RUNNING.

    • As the script starts starts cleaning up, it should report one of these states:

      • lsst.ts.idl.enums.Script.ScriptState.ENDING if the main execution succeeded (cleanup might still fail).

      • lsst.ts.idl.enums.Script.ScriptState.STOPPING if stopping by request.

      • lsst.ts.idl.enums.Script.ScriptState.FAILING if stopping because an error occurred.

    • When the run command finishes the script must exit, after reporting one of three states:

      • lsst.ts.idl.enums.Script.ScriptState.DONE on success

      • lsst.ts.idl.enums.Script.ScriptState.STOPPED if stopped by request

      • lsst.ts.idl.enums.Script.ScriptState.FAILED if an error occurred

  • The script must also support the command line option --schema which prints the configuration schema to stdout and quits. This option ignores the index, but the index argument is still required.

BaseScript provides a base class for Python SAL Scripts.

Script Packages

All SAL scripts should go into the scripts directory of one of the following packages, so the ScriptQueue can find them:

  • ts_standardscripts: scripts that are approved for regular use. These must have unit tests and will be subject to strict version control.

  • ts_externalscripts: scripts for experimentation and one-off tasks.

Python SAL Scripts

Each SAL Script written in Python should consist of three parts:

  • The script file itself, as an executable file in an appropriate subdirectory of scripts/ in ts_standardscripts or ts_externalscripts package.

  • An implementation in the matching subdirectory of python/lsst/ts/standardscripts (or externalscripts) in the same package. This almost always inherits from BaseScript. (Technically the implementation can be in the script file, but that makes it much more difficult to test the code or run it from a Jupyter notebook.)

  • A unit test in the matching subdirectory of tests/, whose name is test_ followed by the name of the script file.

For example the SlewTelescopeIcrs script has the following files in ts_standardscripts:

  • The script file: scripts/auxtel/slew_telescope_icrs.py

  • The implementation: python/lsst/ts/standardscripts/auxtel/slew_telescope_icrs.py

  • The unit test: tests/auxtel/test_slew_telescope_icrs.py

Python Script File

The script file should just import the script and run amain:

#!/usr/bin/env python
#...standard LSST boilerplate
import asyncio

from lsst.ts.standardscripts.auxtel import SlewTelescopeIcrs

asyncio.run(SlewTelescopeIcrs.amain())

Make your script executable using chmod +x <path>.

Python Script Implementation

See python/lsst/ts/standardscripts/auxtel/test_slew_telescope_icrs.py in the ts_standardscripts package for an example.

The script implementation does the actual work. Your script should be a subclass of BaseScript with the following methods (all required unless otherwise noted):

get_schema classmethod

Return a jsonschema defining the configuration of your script. See BaseScript.get_schema for details.

configure method

Configure your script using a configuration validated by get_schema. See BaseScript.configure for details.

Note that configure will always be called once before run and never again. Thus if configure sets attributes needed by run, there is no point to initializing those attributes in the constructor.

set_metadata method

Set metadata about your script, to be published in the metadata Script event. The metadata argument is an instance of metadata Script event data. You need only set the fields that are relevant to your script.

This method is called after configure, allowing you to set metadata based on the configuration.

run method

Perform the main work of the script. See BaseScript.run for details.

If run needs to run a slow computation, either call await asyncio.sleep(0) occasionally to give other coroutines a chance to run (0 is sufficient to free the event loop), or run the computation in a thread using run_in_executor e.g.:

def slow_computation(self):
    ...

loop = asyncio.get_running_loop()
result = await loop.run_in_executor(None, slow_computation)

or if you wish to do other things while you wait:

loop = asyncio.get_running_loop()
thread_task = asyncio.create_task(loop.run_in_executor(None, slow_computation))

# do other work here...
# then eventually you must wait for the background task
result = await thread_task

checkpoints

In your run method you may call await self.checkpoint(name_of_checkpoint) to specify a point at which users can pause or stop the script. By providing a diferent name for each checkpoint you allow users to specify exactly where they would like the script to pause or stop. In addition, each checkpoint is reported as the lastCheckpoint attribute of the state event, so providing informative names can be helpful in tracking the progress of a script. We suggest you make checkpoint names fairly short, obvious and unique, but none of these rules is enforced. If you have a checkpoint in a loop you may wish to modify the name for each iteration, e.g.:

for iter in range(num_exposures):
    await self.checkpoint(f"start exposure {iter}")
    ...

This allows the user to pause or stop at any particular iteration, and makes the state event more informative.

cleanup method (optional)

When your script is ending, after run finishes, is stopped early, or raises an exception, BaseScript calls asynchronous method cleanup for final cleanup. See BaseScript.cleanup for details. In some sense cleanup is like the finally clause of a try/finally block.

The default implementation does nothing, but you are free to override it. If your cleanup code cares about why the script is ending, examine self.state.state; it will be one of:

  • lsst.ts.idl.enums.Script.ScriptState.ENDING: the run method ran normally.

  • lsst.ts.idl.enums.Script.ScriptState.STOPPING: the script was commanded to stop.

  • lsst.ts.idl.enums.Script.ScriptState.FAILING: the run method raised an exception.

If your cleanup code needs additional knowledge about the script’s state, you can add one or more instance variables to your script class and set them in the run method.

other methods

You may define other methods as well, but be careful not to shadow BaseScript methods.

Python Unit Test

See tests/auxtel/test_slew_telescope_icrs.py in the ts_standardscripts package for an example.

There are two basic parts to testing a script: testing configuration and testing the run method.

Testing configuration is straightforward:

  • Write a test method that calls configure with different sorts of invalid data and make sure that configure raises a suitable exception.

  • Write one or more test methods that calls configure with valid data and test that your script is now properly configured.

Testing the run method is more work. My suggestion:

  • Make a trivial class for each controller that your script commands. The class should execute a callback for each commands your script sends. Each callback should record any command data you want to check later, and output any events and telemetry that your script relies on.

  • Configure the script by sending it the do_configure command. This is important because it puts the script into the lsst.ts.idl.enums.Script.ScriptState.CONFIGURED state.

  • Run the script by sending it the do_run command.

  • Check that the final state is lsst.ts.idl.enums.Script.ScriptState.DONE.

  • Check recorded data to see that it matches your expectations.