Observatory Control User Guide

https://img.shields.io/badge/GitHub-ts_observatory_control-green.svg https://img.shields.io/badge/Jenkins-ts_observatory_control-green.svg https://img.shields.io/badge/Jira-ts_observatory_control-green.svg

The Observatory Control package provides users and developers with tools to interact with the Rubin Observatory System. These tools are mainly Python classes/objects that represent major groups of components and encapsulate high-level operations involving those components. Control classes are generally separated according to which telescope system is being commanded.

In many instances, such as the telescope control system, the Auxiliary and Main telescope control system classes contain a common code base. Furthermore, there are aspects shared across all classes, such as identifying the required entities, or usages, for the given instantiation of a class. For these reasons, prior to instantiating any of the individual classes it is imperative that any sections associated to common code bases be read and understood prior to using the specific class, starting with Generic Class Functionality and Initialization and subsections therein.

If a common code documentation section exists for a certain class, it is referenced immediately at the top of the document. The following are the classes, grouped according to their common base classes and associated telescope:

TCS Related Classes

Main Telescope Related Classes

Auxiliary Telescope Related Classes

For an example of what these classes enable, the ATCS class combines functionality for all telescope-operation related components for the Vera Rubin Auxiliary Telescope. Actions like slewing the telescope, while making sure all involved components are in ENABLED state and waiting until all components are in position is encapsulate in a single command;

from lsst.ts.observatory.control.auxtel import ATCS

atcs = ATCS()

await atcs.start_task

await atcs.slew_object(name="Alf Pav")

More examples for this and other CSC groups are available in the generic TCS user guide.

In the following sections we provide some general guidance and insights in using these classes as well as a detailed description of the main operations available on each one of them.

Generic Class Functionality and Initialization

Controlling resources

When writing SAL Script and notebooks to perform high-level operations using many CSCs, one must be aware of resource consumption and allocation, as it may have substantial impact on the performance of the tasks.

Both SalObj and the high-level control modules provides simple ways to control these resources, that users can employ to improve their tasks. There are mainly two features that can be used for this purpose; Sharing DDS domain and Limiting Resources.

Sharing DDS domain

When using multiple classes, for instance to control telescope and instrument, it is highly recommended to share some resources, which can reduce the overhead when running SAL Script and Jupyter notebooks. The most common shareable resource is the DDS domain. When running from a Jupyter notebook, this can done by creating a salobj.Domain and passing it to the classes:

from lsst.ts import salobj
from lsst.ts.observatory.control.auxtel import ATCS, LATISS

domain = salobj.Domain()

atcs = ATCS(domain=domain)
latiss = LATISS(domain=domain)

await atcs.start_task
await latiss.start_task

When writing a SAL Script for the ScriptQueue it is possible to use the scripts own domain. In addition, it is also possible to pass the script log object so that logging from the class will also be published to SAL.

from lsst.ts import salobj
from lsst.ts.observatory.control.auxtel.atcs import ATCS


class MyScript(salobj.BaseScript):

  def __init__(self, index):

      super().__init__(index=index, descr="My script for some operation.")

      self.atcs = ATCS(domain=self.domain, log=self.log)

In the code above, note that self.log is not defined anywhere in the scope. The reason is that MyScript inherits it from salobj.BaseScript. This is actually a custom Python logging object that will publish log messages to DDS, allowing them to be stored in the EFD for debugging.

Limiting Resources

When writing a SAL Script, it is very likely that only a small subset of topics (commands, events and telemetry) from a CSC group will be required. For instance, a SAL Script that will enable all components on a group, will only require the components summaryState and configurationsAvailable events and the state transition commands. In this case, it is possible to save some time and resources by limiting the group class to subscribe only to the required set, instead of the full topics set.

The package allow users to do that through the intended_usage parameter.

By default intended_usage = None, which causes the class to load all the resources from the remotes, which is useful when using the classes from Jupyter notebooks.

Each class defines a specific group of “usages”, for different types of operations. It is possible to combine usages, using bitwise operation, in case more than one operation is needed, e.g.;

intended_usage = Usages.StateTransition | Usages.MonitorHeartBeat

There is also a Usages.All option that should include the topics for all the supported operations (not to be confused with the intended_usage = None, which loads all the topics from all the components in a group).

The feature is available when instantiating the class, for example;

from lsst.ts import salobj
from lsst.ts.observatory.control.auxtel import ATCS, ATCSUsages


class MyScript(salobj.BaseScript):

  def __init__(self, index):

      super().__init__(index=index, descr="My script for some operation.")

      self.atcs = ATCS(domain=self.domain, log=self.log, intended_usage=ATCSUsages.StateTransition)

Details of the available usages for each class is given furthermore.

Handling Authorization

When operating from a user environment (e.g. Jupyter notebook), users have to request authorization to command CSCs. To facilitate this procedure we implement a generic functionality that allows one to request authorization to command all CSCs from a group.

When using the observatory control package from a Jupyter notebook one can do:

from lsst.ts import salobj
from lsst.ts.observatory.control.auxtel import ATCS, LATISS

domain = salobj.Domain()

atcs = ATCS(domain=domain)
latiss = LATISS(domain=domain)

await atcs.start_task
await latiss.start_task

await atcs.request_authorization()
await latiss.request_authorization()

# Execute operations here...

await atcs.release_authorization()
await latiss.release_authorization()

Generic CSC Group behavior

All CSC Group classes are constructed on top of the RemoteGroup base class, which implement generic behavior for groups of CSCs. These generic methods and attributes will (mostly) work equally regardless of the group class.

Probably the most commonly used generic operations are the enable and standby methods.

The idea is that, after running enable, all CSCs in the group will be in the ENABLED state. Components that where already in ENABLED state will be left in that state. For components in any other state the method will make sure to send the required state transition commands to enable them. The method can run without any input argument.

Following are a couple of examples of how to use the enable method.

This will inspect the configurationsAvailable event from all CSCs in the ATCS group to determine the override for each one of them.

from lsst.ts.observatory.control.auxtel import ATCS

atcs = ATCS()

await atcs.start_task

await atcs.enable()

Override the default configuration for ATAOS.

await atcs.enable(overrides={"ataos": "constant_hex"})

And finally, override overrides for all the CSCs in the group. Note how some of them receive an empty string, which is a way of enabling the CSC with default overrides (and also work for when the CSC is not configurable).

await atcs.enable(
    overrides={
        "ataos": "current",
        "atmcs": "",
        "atptg": "",
        "atpneumatics": "",
        "athexapod": "current",
        "atdome": "test.yaml",
        "atdometrajectory": "",
    }
)

To send all components to STANDBY state;

await atcs.standby()

It is also possible to perform state transition in individual CSCs or in subgroups using the set_state method. To send the ATAOS into STANDBY state;

from lsst.ts import salobj
from lsst.ts.observatory.control.auxtel import ATCS

atcs = ATCS()

await atcs.start_task

await atcs.set_state(salobj.STANDBY, components=["ataos"])

To check what is the current state of a particular CSC in the group, one can use get_state method;

ataos_state = await atcs.get_state("ataos")
print(ataos_state)

Note that the method returns a salobj.State object, which is easier to understand (State.STANDBY is more informative than 5, which is what we get when using remotes to get the state from a CSC).

Another useful “generic” feature of the classes that users may rely on when working on Jupyter notebooks or when writing SAL Script, is that they include a Remote for all its CSCs. The remotes are included in a class attribute called rem. To access them simply do;

await atcs.rem.ataos.evt_heartbeat.next(flush=True, timeout=5)

The class also contains a list of the remote names, which is the name of the CSCs in lowercase. If the component is indexed, the name will be appended with an underscore followed by the index, e.g.; the component Test with index 1 becomes test_1. A good way of knowing what CSCs are part of the group you are working with is to print this list;

print(atcs.components)

It is also possible to use this list to access the remotes programmatically, e.g.;

# Get one heartbeat and print the state of each component in ATCS
for comp in atcs.components:
  await getattr(atcs.rem, comp).evt_heartbeat.next(flush=True, timeout=5)
  comp_state = await atcs.get_state(comp)
  print(f"{comp}: {comp_state!r}")

Generic Camera Operations

TBD