Writing scripts for the OET

The Observation Execution Tool (OET) can run observing scripts in a headless non-interactive manner. For efficiency, OET script execution is split into two phases: an initialisation phase and an execution phase. Scripts that are expected to be run by the OET should be structured to have two entry points corresponding to these two phases, as the template below:

Observing script template
 1def init(subarray: int, *args, **kwargs):
 2    # Called by the OET when the script is loaded and initialised by someone
 3    # calling 'oet prepare'. Add your script initialisation code here. Note that
 4    # the target subarray is supplied to this function as the first argument.
 5    pass
 6
 7def main(*args, **kwargs):
 8    # Called by the OET when the prepared script is told to run by someone
 9    # calling 'oet start'. Add the main body of your script to this function.
10    pass

The initialisation phase occurs when the script is loaded and the script’s init function is called (if defined) to perform any preparation and/or initialisation. Expensive and slow operations that can be performed ahead of the main body of script execution can be run in the initialisation phase. Typical actions performed in init are I/O intensive operations, e.g., cloning a git repository, creating multiple Tango device proxies, subscribing to Tango events, etc. When run by the Observation Execution Tool (OET), the init function is passed an integer subarray ID declaring which subarray the control script is intended to control.

Subsequently, at some point a user may call oet start, requesting that the initialised script begin the main body of its execution. When this occurs, the OET calls the script’s main function, which should performs the main function of the script. For an observing script, this would involve the configuration and control of a subarray.

Below are two short but real example scripts. Further examples can be found in the scripts folder of this project.

Telescope Startup

Telescope startup does not require a sub-array ID so the subarray ID argument is ignored when the script is initialised.

Telescope start-up script
 1"""
 2Example script for telescope startup
 3"""
 4import logging
 5import os
 6
 7from ska_oso_scripting.objects import Telescope
 8
 9LOG = logging.getLogger(__name__)
10FORMAT = "%(asctime)-15s %(message)s"
11
12logging.basicConfig(level=logging.INFO, format=FORMAT)
13
14
15def init(subarray_id):
16    pass
17
18
19def main(*args, **kwargs):
20    """
21    Start up telescope.
22    """
23    LOG.info(f"Running telescope start-up script in OS process {os.getpid()}")
24
25    if args:
26        LOG.warning("Got unexpected positional args: %s", args)
27    if kwargs:
28        LOG.warning("Got unexpected named args: %s", kwargs)
29
30    telescope = Telescope()
31
32    LOG.info(f"Starting telescope...")
33    telescope.on()
34
35    LOG.info("Telescope start-up script complete")

SKA-MID : Allocate Resources

Allocating resources requires communication with a TMC CentralNode and TMC SubarrayNode and targets a specific subarray. This script’s init function pre-applies the subarray ID argument to the main function. Note that the script does not perform any Tango calls, with the assign_resources_from_cdm() library function called to perform all the required Tango interactions (command invocation; event subscriptions; event monitoring). Direct Tango calls could be performed in the script if the ska-oso-scripting library does not provide the helper functions needed.

Resource allocation script for an SKA MID subarray
  1"""
  2Example script for SB-driven observation resource allocation from file.
  3On default CDM file values (if CDM file is provided) will be overwritten
  4by values from the SB file. CDM file should be included if values are
  5added to the AssignResourcesRequest that are not available from the SB
  6"""
  7import functools
  8import logging
  9import os
 10
 11from ska_oso_oet.event import topics
 12from ska_oso_pdm import SBDefinition
 13from ska_oso_pdm.sb_definition import DishAllocation as pdm_DishAllocation
 14from ska_tmc_cdm.messages.central_node.assign_resources import AssignResourcesRequest
 15from ska_tmc_cdm.messages.central_node.common import (
 16    DishAllocation as cdm_DishAllocation,
 17)
 18from ska_tmc_cdm.schemas import CODEC as cdm_CODEC
 19
 20from ska_oso_scripting.functions import (
 21    devicecontrol,
 22    environment,
 23    messages,
 24    pdm_transforms,
 25)
 26
 27LOG = logging.getLogger(__name__)
 28FORMAT = "%(asctime)-15s %(message)s"
 29
 30logging.basicConfig(level=logging.INFO, format=FORMAT)
 31
 32
 33def init(subarray_id: int):
 34    """
 35    Initialise the script, binding the sub-array ID to the script.
 36    """
 37    if environment.is_ska_low_environment():
 38        raise environment.ExecutionEnvironmentError(expected_env="SKA-mid")
 39    global main
 40    main = functools.partial(_main, subarray_id)
 41    LOG.info(f"Script bound to sub-array {subarray_id}")
 42
 43
 44def _main(subarray_id: int, sb_json, allocate_json=None):
 45    """
 46    Allocate resources to a target sub-array using a Scheduling Block (SB).
 47
 48    :param subarray_id: numeric subarray ID
 49    :param sb_json: file containing SB in JSON format
 50    :param allocate_json: name of configuration file
 51    :return:
 52    """
 53    LOG.info(f"Running allocate script in OS process {os.getpid()}")
 54    LOG.info(
 55        f"Called with main(sb_json={sb_json}, configuration={allocate_json}, subarray_id={subarray_id})"
 56    )
 57
 58    if not os.path.isfile(sb_json):
 59        msg = f"SB file not found: {sb_json}"
 60        LOG.error(msg)
 61        raise IOError(msg)
 62
 63    if allocate_json:
 64        if not os.path.isfile(allocate_json):
 65            msg = f"CDM file not found: {allocate_json}"
 66            LOG.error(msg)
 67            raise IOError(msg)
 68
 69        cdm_allocation_request: AssignResourcesRequest = cdm_CODEC.load_from_file(
 70            AssignResourcesRequest, allocate_json
 71        )
 72    else:
 73        cdm_allocation_request = AssignResourcesRequest(subarray_id, None, None)
 74
 75    with open(sb_json, "r", encoding="utf-8") as fh:
 76        pdm_allocation_request: SBDefinition = SBDefinition.model_validate_json(
 77            fh.read()
 78        )
 79
 80    # Configure PDM DishAllocation to the equivalent CDM DishAllocation
 81    pdm_dish = pdm_allocation_request.dish_allocations
 82    cdm_dish = convert_dishallocation(pdm_dish)
 83    LOG.info(f"Setting dish : {cdm_dish.receptor_ids} ")
 84
 85    # Configure PDM SDPConfiguration to the equivalent CDM SDPConfiguration
 86    pdm_sdp_config = pdm_allocation_request.sdp_configuration
 87    cdm_sdp_config = pdm_transforms.convert_sdpconfiguration_centralnode(
 88        pdm_sdp_config, pdm_allocation_request.targets
 89    )
 90    LOG.info(
 91        f"Setting SDP configuration for EB: {cdm_sdp_config.execution_block.eb_id} "
 92    )
 93
 94    cdm_allocation_request.dish = cdm_dish
 95    cdm_allocation_request.sdp_config = cdm_sdp_config
 96
 97    response = devicecontrol.assign_resources_from_cdm(
 98        subarray_id, cdm_allocation_request
 99    )
100    LOG.info(f"Resources Allocated: {response}")
101
102    messages.send_message(
103        topics.sb.lifecycle.allocated, sb_id=pdm_allocation_request.sbd_id
104    )
105
106    LOG.info("Allocation script complete")
107
108
109def convert_dishallocation(pdm_config: pdm_DishAllocation) -> cdm_DishAllocation:
110    """
111    Convert a PDM DishAllocation to the equivalent CDM DishAllocation.
112    """
113
114    return cdm_DishAllocation(receptor_ids=pdm_config.receptor_ids)