Device Interface

To make loading and updating lightpath as simple as possible, lightpath relies on two external sources of information:

  1. A database definition of the device (static, happi)

  2. A snapshot of the device’s current status (dynamic, ophyd)

The first of these two is simple to understand, this information allows us to properly place the device in the facility relative to the other devices. The second, device status, allows us to see where the device is pointing and if it is permitting beam at a given point in time.

The static information is held in the happi database, while the dynamic information is read from the device’s ophyd representation.

Database Definition

An example of a valid lightpath happi definition:

+------------------+-------------------------------------------------------------------------+
| EntryInfo        | Value                                                                   |
+------------------+-------------------------------------------------------------------------+
| active           | True                                                                    |
| args             | ['{{prefix}}']                                                          |
| beamline         | LFE                                                                     |
| creation         | Mon Jul 18 16:05:21 2022                                                |
| device_class     | pcdsdevices.attenuator.FEESolidAttenuator                               |
| documentation    | None                                                                    |
| functional_group | N/A                                                                     |
| input_branches   | ['L0']                                                                  |
| ioc_arch         | None                                                                    |
| ioc_engineer     | None                                                                    |
| ioc_hutch        | None                                                                    |
| ioc_location     | None                                                                    |
| ioc_name         | None                                                                    |
| ioc_release      | None                                                                    |
| ioc_type         | None                                                                    |
| kwargs           | {'input_branches': '{{input_branches}}', 'name': '{{name}}',            |
|                  | 'output_branches': '{{output_branches}}'}                               |
| last_edit        | Mon Jul 18 16:05:21 2022                                                |
| lightpath        | True                                                                    |
| location_group   | N/A                                                                     |
| name             | at2l0                                                                   |
| output_branches  | ['L0']                                                                  |
| prefix           | AT2L0:XTES                                                              |
| stand            | L0S07                                                                   |
| type             | pcdsdevices.happi.containers.LCLSLightpathItem                          |
| z                | 734.50000                                                               |
+------------------+-------------------------------------------------------------------------+

An example container has been included in lightpath as lightpath.happi.containers.LightpathItem, and is discoverable via python entrypoints. In this example, the database information is used to instantiate the device by being passed in as a keyword argument.

LCLS modifies this container slightly, adding LCLS-relevant metadata and the option to omit the input_branches / output_branches keyword arguments for devices that do not implement the LightpathInterface. (see pcdsdevices.happi.containeres.LCLSLightpathItem)

Device Status (Lightpath Status)

What does your ophyd device need?

In the simplest sense, your device needs to present the following API:

  • device.lightpath_summary: a signal that changes whenever the Lightpath-state of the device changes

  • device.get_lightpath_state(): a method that returns a lightpath.LightpathState dataclass instance, containing the Lightpath-state of the device

How is this implemented?

The main point of interaction between Lightpath and a device’s dynamic information should be the get_lightpath_state() method. This method is expected to return a dataclass with the following fields:

  • inserted: if the device is inserted

  • removed: if the device is removed

  • output: a dictionary mapping an output branch name to the transmission being delivered to that branch

Lightpath also subscribes to a signal called lightpath_summary, which is expected to change whenever the lightpath status of the device has changed. When Lightpath sees a change in this signal, it will query get_lightpath_state() for the updated status of the device. (Lightpath may also query get_lightpath_state() at other points).

Putting all of this together, a minimal device definition that fulfills the lightpath interface requirements might look like:

from lightpath import LightpathState
from ophyd import Device, EpicsSignal
from ophyd import Component as Cpt

class MyDevice(Device):

    # if we were to only care about one signal
    lightpath_summary = Cpt(EpicsSignal, ':MY:PV')

    input_branches = ['K0']
    output_branches = ['K0']

    def get_lightpath_state(self):
        return LightpathState(
            inserted=True,
            removed=True,
            output={self.output_branches[0]: 1}
        )

This would work, strictly speaking, but is far from being optimized and easy to use.

To make things easier, LCLS has implemented this as an ophyd device mixin in pcdsdevices.interfaces.LightpathMixin. Notably, this mixin caches the lightpath state, so that calls to get_lightpath_state() do not overwhelm the ophyd callback queue with Channel Access requests. This was found to be necessary for beam paths with many devices.

In LCLS, you might see a device object definition that looks like the following:

from lightpath import LightpathState
from pcdsdevices.interface import LightpathMixin
from ophyd import Device

class BaseDevice(Device, LightpathMixin):
    """
    Base class for some specific device
    """
    # Mark as parent class for lightpath interface
    lightpath_cpts = ['xwidth.user_readback', 'ywidth.user_readback']

    nominal_aperature = 0.5

    # < ... unrelated methods snipped ... >

    def calc_lightpath_state(
        self,
        xwidth: float,
        ywidth: float
    ) -> LightpathState:
        widths = [xwidth, ywidth]
        self._inserted = (min(widths) < self.nominal_aperture)
        self._removed = not self._inserted
        self._transmission = 1.0 if self._inserted else 0.0

        return LightpathState(
            inserted=self._inserted,
            removed=self._removed,
            output={self.output_branches[0]: self._transmission}
        )

In this case we are leveraging the LightpathMixin class, which does most of the repetitive setup for us (creating lightpath_summary signal, subscribing to relevant components, setting up lightpath state caching, checking that the subclass is correctly configured, etc.). This mixin delegates the calculation of the lightpath state to the calc_lightpath_state method, which is to be written by the device creator. Furthermore, the mixin looks for a list of component names called lightpath_cpts, which will lightpath_summary will watch for changes. Upon a change in one of these signals, the LightpathMixin will get the values of each component and pass them to calc_lightpath_state.