Actor#

The actor functionality is implemented as a base CLU actor in ArchonBaseActor. An AMQP version of the actor is available as ArchonActor. It’s possible to subclass ArchonBaseActor with other specific actor implementations, for example

from archon.actor import ArchonBaseActor
from clu.legacy import LegacyActor

class LegacyArchonActor(ArchonBaseActor, LegacyActor):
    pass

The actor must be instantiated with a list of ArchonController instances in addition to the usual actor parameters.

actor = ArchonActor('archon', [controller], host='localhost', port=5672, version='1.2.3')
await actor.start()

The controllers are unpacked into a controllers dictionary keyed by the name of the controller.

Normally the actor is instantiated using the classmethod from_config with the path to a configuration file. See the Configuration file section for more details.

actor = ArchonActor.from_config(config_file)
await actor.start()

Running the actor#

The actor can be run as usual by calling

await actor.run_forever()

A simple CLI interface is provided to run the actor.

$ archon --help
Usage: archon [OPTIONS] COMMAND [ARGS]...

  Archon controller

Options:
  -p, --profile [boss|lvm]  The profile to use. If not provided, infered
                            from the domain name.
  -c, --config FILE         Path to the user configuration file.
  -v, --verbose             Debug mode. Use additional v for more details.
                            [x>=0]
  --help                    Show this message and exit.

Commands:
  actor*  Runs the actor.

To start the actor in detached (daemon) mode run archon actor start. This is roughly equivalent to the following script

archon_actor = ArchonActor.from_config(config_file)
await archon_actor.start()
await archon_actor.run_forever()

Use the flag --debug with archon actor start to prevent the process from detaching, which is useful for development and debugging.

When the actor starts it will use the fully qualified domain name to determine the profile and actor to load, along with the appropriate configuration. This can be overridden by passing the --profile flag.

Commands#

A set of default actor commands are attached to the actor as archon.actor.commands.parser. The full list of commands can be obtained by running the help command from the CLU command line

archon help
01:01:57.454 archon >
01:01:57.468 archon : {
    "help": [
        "Usage: archon [OPTIONS] COMMAND [ARGS]...",
        "",
        "Options:",
        "  --help  Show this message and exit.",
        "",
        "Commands:",
        "  config      Manages the configuration of the device.",
        "  expose      Exposes the cameras.",
        "  flush       Flushes controllers.",
        "  frame       Interacts with the controller buffer frame.",
        "  get_schema  Returns the schema of the actor as a JSON schema.",
        "  help        Shows the help.",
        "  init        Initialises a controller.",
        "  keyword     Prints human-readable information about a keyword.",
        "  lvm         Commands specific to LVM.",
        "  ping        Pings the actor.",
        "  reconnect   Restarts the socket connection to the controller(s).",
        "  reset       Resets the controllers and discards ongoing exposures.",
        "  status      Reports the status of the controller.",
        "  system      Reports the status of the controller backplane.",
        "  talk        Sends a command to the controller.",
        "  version     Reports the version."
    ]
}

The command list is also available here. Information on the usage of each command can be retrieved by passing --help to a specific command, for example expose --help.

To add a new command you must define it as a CLU click command.

from archon.actor.commands import parser

@parser.command()
async def new_command(command, controllers):
    pass

See the CLU documentation for more details. Note that every parser command receives the actor command as the first argument and the dictionary of controllers associated with the actor as the second one.

parallel_controllers#

It is sometimes useful to write a command in a way that is executed in parallel and concurrently for all the available Archon controllers. The parallel_controllers decorator simplifies this by allowing to replace the command callback function with a coroutine that will be executed for each controller.

from archon.actor.tools import error_controller, parallel_controllers
from archon.actor.commands import parser

@parser.command()
@parallel_controllers()
async def status(command, controller):
    """Reports the status of the controller."""

    try:
        status = await controller.get_device_status()
    except ArchonError as ee:
        return error_controller(command, controller, str(ee))

    command.info(
        status={
            "controller": controller.name,
            "status": controller.status.value,
            "status_names": [flag.name for flag in controller.status.get_flags()],
            **status,
        }
    )

    return True

Here the coroutine status receives the command and a singe controller executes certain code, outputting the status of the controller. We can use the helper error_controller to correctly format the output of an error. parallel_controllers will handle executing this code for each controller attached to the actor. Note that in this case we should not finish or fail the command, just return True or False respectively. parallel_controllers will finish the command successfully if each one of the controller callbacks returns True, or fail it if any of them returns False. If a callback returns False, all other callbacks are immediately cancelled.

parallel_controllers will check that each controller is connected and ready to receive commands so it’s not necessary to do that in the callback. This can be disabled by decorating with @parallel_controllers(check=False).

Exposing#

Exposing the CCDs is accomplish via the expose actor command. Normally one will issue the expose start command, which will start the exposure but will not automatically read the detectors. That step happens when expose finish is called. This division allows the user to execute additional actions between the beginning and end of the integration, for example to command an external shutter. For convenience one can integrate and read at once by calling expose start --finish. See expose start --help for more options.

The resulting files are saved to the location and file specified in the configuration file. Each CCD in a controller is saved as an independent file. A basic header is always attached to each FITS file, which can be expanded by defining additional keywords in the header section of the configuration.

When finishing the exposure, one can send a JSON-formatted mapping of keyword-values that will be added to the default header. The value for each keyword can be a single value or a list in which the first element is the value and the second the comment for the keyword.

archon expose finish --header '{"KEYWORD1": 1, "KEYWORD2": "Some value"}'
archon expose finish --header '{"KEYWORD1": [1, "A comment"]}'

Overriding the exposure delegate#

Internally the actor uses the ExposureDelegate to handle exposures. Subclasses of ArchonBaseActor will inherit this delegate. The delegate is designed to be modular so that it can be itself subclassed and its behaviour overridden.

One typical case in which we may want to override the delegate is to implement control for an external shutter. The delegate will call ExposureDelegate.shutter at the beginning of the integration and readout, but the default shutter method simply returns True. To change that behaviour we can do

from archon.actor.delegate import ExposureDelegate

class ShutterDelegate(ExposureDelegate):

    async def shutter(self, open):

        if open is True:
            await shutter_device.open()
        else:
            await shutter_device.close()

And then we set the new delegate as

class MyActor(ArchonActor):
    DELEGATE_CLASS = ShutterDelegate

Two other methods that are useful to override are readout_cotasks and post_process. The first is run concurrently with readout and it can be used to run time consuming tasks at that time, for example to read environmental sensor data that will be used for the header. readout_cotasks does not return anything, so any data must be stored in the instance itself.

class MyDelegate(ExposureDelegate):

    def __init__(self, actor):
        super().__init__(actor)
        self.extra_data = {}

    def reset(self):
        self.extra_data = {}
        return super().reset()

    async def readout_cotasks(self):
        temperature = sensor.read_temperature()
        self.extra_data['temperature'] = temperature

post_process runs after the buffer has been fetched and receives, for each controller associated with the actor, the controller instance and the list of FITS HDUs (one for each CCD associated with the controller). The HDUs contain the data read from the buffer for that CCD and the default header. The method must return the controller and HDUs after modifying them. Continuing our example before

class MyDelegate(ExposureDelegate):

    ...

    async def post_process(controller, hdus):
        for hdu in hdus:
            hdu.header['TEMP'] = self.extra_data['temperature']

Configuration#

To work properly, the actor requires some knowledge of the controller. This configuration is usually passed to from_config() when initialising the actor. If the actor is started from the command line, the appropriate configuration file will be selected and loaded.

The following is a sample configuration file with all the possible parameters.

# REQUIRED. A dictionary of controller name to Archon controller connection parameters.
controllers:
  sp1: # Name of the controller.
    host: 10.7.45.26
    port: 4242
    parameters: # Parameters for readout and fetching.
      lines: 2040 # Number of lines per amplifier.
      pixels: 2040 # Number of pixels per line.
      overscan_pixels: 20 # Number of overscan pixels.
      taps_per_detector: 4 # Number of amplifiers per detector.
      framemode: split # Archon frame mode. split or top.
    detectors: # List of detector per controller.
      r1: # The names of the detectors are required.
        serial: STA29687 # The parameters for each of them are optional.
        gain: 2.82
        readnoise: 3
        type: STA4850
      b1:
        serial: STA29602
        gain: 2.81
        readnoise: 3
        type: STA4850
      z1:
        serial: STA27875
        gain: 2.88
        readnoise: 3
        type: STA4850

# REQUIRED. The base directory where the data will be saved (and MJD subdirectory is
# always added) and the format of the saved filename. Available placeholders are
# {observatory} which is replaced with "apo" or "lco", {hemisphere} replaced with "n" if
# the observatory is "apo" and "s" if "lco", {controller} which is replaced with the
# name of the Archon controller defined above, {ccd} which is the name do the CCD
# (including the controller identifier), and {exposure} which is a never-repeating
# sequence identifier.
files:
  data_dir: '~/'
  template: 'sdR-{ccd}-{exposure_no:08d}.fits.gz'

# OPTIONAL. Additional header keywords to be obtained from the SYSTEM and STATUS
# commands. The {sensor} placeholder will be filled out with the sensor value for the
# detector. If the value of the header keyword is a list, it must be [value, comment].
# It can also be a mapping of detector name to [value, comment]. If a third element is
# used it will be considered the floating point precision.
header:
  status:
    CCDTEMP1:
      r1: ['status.mod2/tempa', 'Temperature of the sensor (HEATERX 2)', 2]
      b1: ['status.mod12/tempc', 'Temperature of the sensor (HEATERX 12)', 2]
      z1: ['status.mod12/tempa', 'Temperature of the sensor (HEATERX 12)', 2]
    CCDTEMP2:
      r1: ['status.mod2/tempb', 'LN2 temperature (HEATERX 2)', 2]
      b1: ['status.mod2/tempc', 'LN2 temperature (HEATERX 2)', 2]
      z1: ['status.mod12/tempb', 'LN2 temperature (HEATERX 12)', 2]

# REQUIRED. Actor congiguration for the actor class
actor:
  name: archon
  host: localhost
  port: 5672

# REQUIRED. Various timeout values.
timeouts:
  controller_connect: 5
  write_config_timeout: 2
  write_config_delay: 0.0001
  expose_timeout: 2
  readout_expected: 40
  readout_max: 60
  fetching_expected: 5
  fetching_max: 10
  flushing: 1.2

# REQUIRED.
archon:
  # Archon firmware parameters to be set when the controller is reset.
  default_parameters:
    HorizontalBinning: 1
    VerticalBinning: 1
  # This is the ACF configuration file to be loaded to the Archon including the timing
  # script. {archon_etc} gets completed with the path of the etc directory once
  # installed. If the path is not absolute, the root of the package is used as working
  # directory.
  archon:
    config_file: 'lvm/config/archon/LVM_100MHz.acf'