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_tmc_cdm.messages.central_node.assign_resources import AssignResourcesRequest
 13from ska_tmc_cdm.messages.central_node.common import DishAllocation as cdm_DishAllocation
 14from ska_tmc_cdm.schemas import CODEC as cdm_CODEC
 15from ska_oso_pdm.entities.dish.dish_allocation import DishAllocation as pdm_DishAllocation
 16from ska_oso_pdm.entities.common.sb_definition import SBDefinition
 17
 18from ska_oso_pdm.schemas import CODEC as pdm_CODEC
 19
 20from ska_oso_scripting.functions import devicecontrol, messages, environment
 21from ska_oso_scripting.functions import pdm_transforms
 22
 23LOG = logging.getLogger(__name__)
 24FORMAT = '%(asctime)-15s %(message)s'
 25
 26logging.basicConfig(level=logging.INFO, format=FORMAT)
 27
 28
 29def init(subarray_id: int):
 30    """
 31    Initialise the script, binding the sub-array ID to the script.
 32    """
 33    if environment.is_ska_low_environment():
 34        raise environment.ExecutionEnvironmentError(expected_env="SKA-mid")
 35    global main
 36    main = functools.partial(_main, subarray_id)
 37    LOG.info(f'Script bound to sub-array {subarray_id}')
 38
 39
 40def _main(subarray_id: int, sb_json, allocate_json=None):
 41    """
 42    Allocate resources to a target sub-array using a Scheduling Block (SB).
 43
 44    :param subarray_id: numeric subarray ID
 45    :param sb_json: file containing SB in JSON format
 46    :param allocate_json: name of configuration file
 47    :return:
 48    """
 49    LOG.info(f'Running allocate script in OS process {os.getpid()}')
 50    LOG.info(
 51        f'Called with main(sb_json={sb_json}, configuration={allocate_json}, subarray_id={subarray_id})')
 52
 53    if not os.path.isfile(sb_json):
 54        msg = f'SB file not found: {sb_json}'
 55        LOG.error(msg)
 56        raise IOError(msg)
 57
 58    if allocate_json:
 59        if not os.path.isfile(allocate_json):
 60            msg = f'CDM file not found: {allocate_json}'
 61            LOG.error(msg)
 62            raise IOError(msg)
 63
 64        cdm_allocation_request: AssignResourcesRequest = cdm_CODEC.load_from_file(AssignResourcesRequest, allocate_json)
 65    else:
 66        cdm_allocation_request = AssignResourcesRequest(subarray_id, None, None)
 67
 68    pdm_allocation_request: SBDefinition = pdm_CODEC.load_from_file(SBDefinition, sb_json)
 69
 70    # Configure PDM DishAllocation to the equivalent CDM DishAllocation
 71    pdm_dish = pdm_allocation_request.dish_allocations
 72    cdm_dish = convert_dishallocation(pdm_dish)
 73    LOG.info(f'Setting dish : {cdm_dish.receptor_ids} ')
 74
 75    # Configure PDM SDPConfiguration to the equivalent CDM SDPConfiguration
 76    pdm_sdp_config = pdm_allocation_request.sdp_configuration
 77    cdm_sdp_config = pdm_transforms.convert_sdpconfiguration_centralnode(pdm_sdp_config,
 78                                                                         pdm_allocation_request.targets)
 79    LOG.info(f'Setting SDP configuration for EB: {cdm_sdp_config.execution_block.eb_id} ')
 80
 81    cdm_allocation_request.dish = cdm_dish
 82    cdm_allocation_request.sdp_config = cdm_sdp_config
 83
 84    response = devicecontrol.assign_resources_from_cdm(subarray_id, cdm_allocation_request)
 85    LOG.info(f'Resources Allocated: {response}')
 86
 87    messages.send_message(topics.sb.lifecycle.allocated, sb_id=pdm_allocation_request.sbd_id)
 88
 89    LOG.info('Allocation script complete')
 90
 91
 92def convert_dishallocation(pdm_config: pdm_DishAllocation) -> cdm_DishAllocation:
 93    """
 94    Convert a PDM DishAllocation to the equivalent CDM DishAllocation.
 95    """
 96
 97    return cdm_DishAllocation(
 98        receptor_ids=pdm_config.receptor_ids
 99    )
100