Device Interface
To make loading and updating lightpath as simple as possible, lightpath relies on two external sources of information:
A database definition of the device (static,
happi
)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 changesdevice.get_lightpath_state()
: a method that returns alightpath.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
.