Source code for id3c.devices.laser_optics

"""
Laser optics bundle (us / ds axes) with IN/OUT state.

The ``us`` and ``ds`` axes are upstream and downstream positioners of the
laser optics.  At nominal positions they are either
fully ``IN`` (in the beam path) or fully ``OUT`` (retracted).

This device declares its two axes as
:class:`~id3c.devices.interlocked_motor.InterlockedEpicsMotor`.  The
actual interlock callable (against ``sample_stage.omega``) is wired
late, in ``id3c.startup``.

Configuration Components (plain :class:`ophyd.Signal`, ``kind="config"``):

* ``in_position``   -- nominal IN location (mm), applied to both axes
* ``out_position``  -- nominal OUT location (mm), applied to both axes
* ``tolerance``     -- +/- window (mm) for IN/OUT comparison
* ``settle_time``   -- post-move delay (s) in ``move_in``/``move_out``

Derived Components (:class:`ophyd.signal.AttributeSignal`, ``kind="omitted"``):

* ``out_status`` -- mirrors :attr:`is_out`

Derived signals are subscribable, which is what the mid-motion
interlock watcher on ``sample_stage.omega`` uses.  Note however that
``AttributeSignal`` itself does not emit on EPICS updates; the
watcher should subscribe to the underlying ``us.user_readback`` and
``ds.user_readback`` signals (which it does, by wiring in
``startup.py``).  ``out_status`` is exposed for
manual ``.get()`` queries and for any code that just wants the
boolean.
"""

from __future__ import annotations

import logging

from bluesky import plan_stubs as bps
from bluesky.utils import plan
from ophyd import Component as Cpt
from ophyd import MotorBundle
from ophyd import Signal
from ophyd.signal import AttributeSignal

from .interlocked_motor import InterlockedEpicsMotor
from .interlocked_motor import MotionInterlock

[docs] logger = logging.getLogger(__name__)
[docs] class LaserOptics(MotorBundle): """Retractable laser optics with IN/OUT state and motion plans."""
[docs] us = Cpt(InterlockedEpicsMotor, "m1", labels=["motors"])
[docs] ds = Cpt(InterlockedEpicsMotor, "m2", labels=["motors"])
# Tunable configuration. Defaults match the beamline note in # devices.yml: IN = +75 mm, OUT = -75 mm, tolerance = +/- 1 mm.
[docs] in_position = Cpt(Signal, value=75.0, kind="config")
[docs] out_position = Cpt(Signal, value=-75.0, kind="config")
[docs] tolerance = Cpt(Signal, value=1.0, kind="config")
[docs] settle_time = Cpt(Signal, value=0.0, kind="config")
# Derived state, exposed as signal so other code can subscribe # or .get() without poking at properties.
[docs] out_status = Cpt(AttributeSignal, attr="is_out", kind="omitted")
# ------------------------------------------------------------------ # Property logic def _within(self, axis, reference: Signal) -> bool: """True if ``axis.user_readback`` is within tolerance of ``reference``.""" return abs(axis.user_readback.get() - reference.get()) <= self.tolerance.get() @property
[docs] def is_out(self) -> bool: """True iff both axes are within tolerance of ``out_position``.""" ds_out = self._within(self.ds, self.out_position) us_out = self._within(self.us, self.out_position) return us_out and ds_out
# ------------------------------------------------------------------ # Plan methods @plan
[docs] def move_out(self): """Move both axes to ``out_position`` and verify. Such as:: yield from laser_optics.move_out() """ target = self.out_position.get() yield from bps.mv(self.us, target, self.ds, target) settle = self.settle_time.get() if settle > 0: yield from bps.sleep(settle) if not self.is_out: raise MotionInterlock( f"{self.name}.move_out: axes did not reach OUT " f"({target} +/- {self.tolerance.get()} mm). " f"us={self.us.user_readback.get()}, " f"ds={self.ds.user_readback.get()}." )