示例#1
0
class SampleNested(Device):
    yolk = Cpt(EpicsSignal, ':YOLK', string=True)
    whites = Cpt(EpicsSignalRO, ':WHITES')
示例#2
0
 class MyDetector(SingleTrigger, SimDetector):
     stats1 = Cpt(StatsPlugin, 'Stats1:')
     proc1 = Cpt(ProcessPlugin, 'Proc1:')
     roi1 = Cpt(ROIPlugin, 'ROI1:')
示例#3
0
 class MyDetector(SingleTrigger, SimDetector):
     tiff1 = Cpt(FS_tiff, 'TIFF1:',
                 write_path_template=wpath,
                 read_path_template=rpath,
                 root=root, reg=fs)
示例#4
0
 class MyDetector(SingleTrigger, SimDetector):
     roi1 = Cpt(ROIPlugin, 'ROI1:')
     over1 = Cpt(OverlayPlugin, 'Over1:')
示例#5
0
 class PluginGroup(Device):
     tiff1 = Cpt(TIFFPlugin, 'TIFF1:')
示例#6
0
from elog import HutchELog
from ophyd.device import Component as Cpt
from ophyd.signal import Signal
from pcdsdevices.areadetector.detectors import PCDSDetector

import hutch_python.utils

# We need to have the tests directory importable to match what we'd have in a
# real hutch-python install
sys.path.insert(0, os.path.dirname(__file__))

TST_CAM_CFG = str(Path(__file__).parent / '{}camviewer.cfg')

for plugin in ('image', 'stats'):
    plugin_class = getattr(PCDSDetector, plugin).cls
    plugin_class.plugin_type = Cpt(Signal, value=plugin_class._plugin_type)


@contextmanager
def cli_args(args):
    """
    Context manager for running a block of code with a specific set of
    command-line arguments.
    """
    prev_args = sys.argv
    sys.argv = args
    yield
    sys.argv = prev_args


@contextmanager
示例#7
0
 class MyDet(SimDetector):
     p = Cpt(HDF5Plugin, suffix='HDF1:')
示例#8
0
 class LXE(LaserEnergyPositioner):
     motor = Cpt(Newport, '')
示例#9
0
 class USBEncoder(Device):
     tab_component_names = True
     set_zero = Cpt(EpicsSignal, ':ZEROCNT')
     pos = Cpt(EpicsSignal, ':POSITION')
     scale = Cpt(EpicsSignal, ':SCALE')
     offset = Cpt(EpicsSignal, ':OFFSET')
示例#10
0
class PCDSAreaDetectorTyphos(Device):
    """
    A 'bare' PCDS areadetector class specifically for Typhos screens.
    Implements only the most commonly used PVs for areadetector IOCS.
    Includes a simple image viewer. 
    """

    # Status and specifications
    manufacturer = Cpt(EpicsSignalRO, 'Manufacturer_RBV', kind='config')
    camera_model = Cpt(EpicsSignalRO, 'Model_RBV', kind='normal')
    sensor_size_x = Cpt(EpicsSignalRO, 'MaxSizeX_RBV', kind='config')
    sensor_size_y = Cpt(EpicsSignalRO, 'MaxSizeY_RBV', kind='config')
    data_type = Cpt(EpicsSignalWithRBV, 'DataType', kind='config')

    # Acquisition settings
    exposure = Cpt(EpicsSignalWithRBV, 'AcquireTime', kind='config')
    gain = Cpt(EpicsSignalWithRBV, 'Gain', kind='config')
    num_images = Cpt(EpicsSignalWithRBV, 'NumImages', kind='config')
    image_mode = Cpt(EpicsSignalWithRBV, 'ImageMode', kind='config')
    trigger_mode = Cpt(EpicsSignalWithRBV, 'TriggerMode', kind='config')
    acquisition_period = Cpt(EpicsSignalWithRBV, 'AcquirePeriod', kind='config')

    # Image collection settings
    acquire = Cpt(EpicsSignal, 'Acquire', kind='normal')
    acquire_rbv = Cpt(EpicsSignalRO, 'DetectorState_RBV', kind='normal')
    image_counter = Cpt(EpicsSignalRO, 'NumImagesCounter_RBV', kind='normal')

    # Image data
    ndimensions = Cpt(EpicsSignalRO, 'IMAGE2:NDimensions_RBV', kind='omitted') 
    width = Cpt(EpicsSignalRO, 'IMAGE2:ArraySize0_RBV', kind='omitted')
    height = Cpt(EpicsSignalRO, 'IMAGE2:ArraySize1_RBV', kind='omitted')
    depth = Cpt(EpicsSignalRO, 'IMAGE2:ArraySize2_RBV', kind='omitted')
    array_data = Cpt(EpicsSignal, 'IMAGE2:ArrayData', kind='omitted')
    cam_image = Cpt(NDDerivedSignal, derived_from='array_data',
                    shape=('height',
                           'width',
                           'depth'),
                    num_dimensions='ndimensions',
                    kind='normal')
示例#11
0
class PCDSAreaDetector(PCDSAreaDetectorEmbedded):
    """
    Standard area detector including all (*) standard PCDS plugins.

    Notable plugins:
        IMAGE2: reduced rate image, used for camera viewer
        Stats2: reduced rate stats

    (*) Currently excludes all PVAccess plugins:
        IMAGE1:Pva
        IMAGE2:Pva:
        THUMBNAIL:Pva

    Default port chains
    -------------------
    The default configuration of ports is as follows::

        CAM -> CC1
        CAM -> CC2
        CAM -> HDF51
        CAM -> IMAGE1:CC
        CAM -> IMAGE1:Proc
        CAM -> IMAGE1:ROI
        CAM -> IMAGE2:CC
        CAM -> IMAGE2:Over
        CAM -> IMAGE2:Proc
        CAM -> IMAGE2:ROI -> IMAGE2
        CAM -> JPEG1
        CAM -> NetCDF1
        CAM -> Over1
        CAM -> Proc1
        CAM -> ROI1 -> IMAGE1
        CAM -> ROI1 -> Stats1
        CAM -> ROI2
        CAM -> ROI3
        CAM -> ROI4
        CAM -> Stats2
        CAM -> Stats3
        CAM -> Stats4
        CAM -> Stats5
        CAM -> THUMBNAIL:CC
        CAM -> THUMBNAIL:Over
        CAM -> THUMBNAIL:Proc
        CAM -> THUMBNAIL:ROI -> THUMBNAIL
        CAM -> TIFF1
        CAM -> Trans1

    Notes
    -----
    Subclasses should replace :attr:`cam` with that of the respective detector,
    such as `PilatusDetectorCam` for the Pilatus
    detector.
    """

    image1 = Cpt(ImagePlugin, 'IMAGE1:')
    image1_roi = Cpt(ROIPlugin, 'IMAGE1:ROI:')
    image1_cc = Cpt(ColorConvPlugin, 'IMAGE1:CC:')
    image1_proc = Cpt(ProcessPlugin, 'IMAGE1:Proc:')
    image1_over = Cpt(OverlayPlugin, 'IMAGE1:Over:')
    # image2 in parent
    image2_roi = Cpt(ROIPlugin, 'IMAGE2:ROI:')
    image2_cc = Cpt(ColorConvPlugin, 'IMAGE2:CC:')
    image2_proc = Cpt(ProcessPlugin, 'IMAGE2:Proc:')
    image2_over = Cpt(OverlayPlugin, 'IMAGE2:Over:')
    thumbnail = Cpt(ImagePlugin, 'THUMBNAIL:')
    thumbnail_roi = Cpt(ROIPlugin, 'THUMBNAIL:ROI:')
    thumbnail_cc = Cpt(ColorConvPlugin, 'THUMBNAIL:CC:')
    thumbnail_proc = Cpt(ProcessPlugin, 'THUMBNAIL:Proc:')
    thumbnail_over = Cpt(OverlayPlugin, 'THUMBNAIL:Over:')
    cc1 = Cpt(ColorConvPlugin, 'CC1:')
    cc2 = Cpt(ColorConvPlugin, 'CC2:')
    hdf51 = Cpt(HDF5Plugin, 'HDF51:')
    jpeg1 = Cpt(JPEGPlugin, 'JPEG1:')
    netcdf1 = Cpt(NetCDFPlugin, 'NetCDF1:')
    nexus1 = Cpt(NexusPlugin, 'Nexus1:')
    over1 = Cpt(OverlayPlugin, 'Over1:')
    proc1 = Cpt(ProcessPlugin, 'Proc1:')
    roi1 = Cpt(ROIPlugin, 'ROI1:')
    roi2 = Cpt(ROIPlugin, 'ROI2:')
    roi3 = Cpt(ROIPlugin, 'ROI3:')
    roi4 = Cpt(ROIPlugin, 'ROI4:')
    stats1 = Cpt(StatsPlugin, 'Stats1:')
    # stats2 in parent
    stats3 = Cpt(StatsPlugin, 'Stats3:')
    stats4 = Cpt(StatsPlugin, 'Stats4:')
    stats5 = Cpt(StatsPlugin, 'Stats5:')
    tiff1 = Cpt(TIFFPlugin, 'TIFF1:')
    trans1 = Cpt(TransformPlugin, 'Trans1:')
示例#12
0
class DelayNewport(DelayBase):
    __doc__ = DelayBase.__doc__
    motor = Cpt(Newport, '')
示例#13
0
class EpicsMotorInterface(FltMvInterface, EpicsMotor):
    """
    The standard EpicsMotor class, but with our interface attached.

    This includes some PCDS preferences, but not any IOC differences.

    Notes
    -----
    The full list of preferences implemented here are:

        1. Use the FltMvInterface mixin class for some name aliases and
        bells-and-whistles level functions
        2. Instead of using the limit fields on the setpoint PV, the EPICS
        motor class has the ``LLM`` and ``HLM`` soft limit fields for
        convenient usage. Unfortunately, pyepics does not update its internal
        cache of the limits after the first get attempt. We therefore disregard
        the internal limits of the PV and use the soft limit records
        exclusively.
        3. The disable puts field ``.DISP`` is added, along with ``enable`` and
        ``disable`` convenience methods. When ``.DISP`` is 1, puts to the motor
        record will be ignored, effectively disabling the interface.
        4. The description field keeps track of the motors scientific use along
           the beamline.
    """
    # Reimplemented because pyepics does not recognize when the limits have
    # been changed without a re-connection of the PV. Instead we trust the soft
    # limits records
    user_setpoint = Cpt(EpicsSignal, ".VAL", limits=False, kind='normal')
    # Additional soft limit configurations
    low_soft_limit = Cpt(EpicsSignal, ".LLM", kind='omitted')
    high_soft_limit = Cpt(EpicsSignal, ".HLM", kind='omitted')
    # Enable/Disable puts
    disabled = Cpt(EpicsSignal, ".DISP", kind='omitted')
    # Description is valuable
    description = Cpt(EpicsSignal, '.DESC', kind='normal')

    @property
    def low_limit(self):
        """
        The lower soft limit for the motor.
        """
        return self.low_soft_limit.value

    @low_limit.setter
    def low_limit(self, value):
        self.low_soft_limit.put(value)

    @property
    def high_limit(self):
        """
        The higher soft limit for the motor.
        """
        return self.high_soft_limit.value

    @high_limit.setter
    def high_limit(self, value):
        self.high_soft_limit.put(value)

    @property
    def limits(self):
        """
        The soft limits of the motor.
        """
        return (self.low_limit, self.high_limit)

    @limits.setter
    def limits(self, limits):
        self.low_limit = limits[0]
        self.high_limit = limits[1]

    def enable(self):
        """
        Enables the motor.

        When disabled, all EPICS puts to the base record will be dropped.
        """
        return self.disabled.put(value=0)

    def disable(self):
        """
        Disables the motor.

        When disabled, all EPICS puts to the base record will be dropped.
        """
        return self.disabled.put(value=1)

    def check_value(self, value):
        """
        Raise an exception if the motor cannot move to value.

        The full list of checks done in this method:

            - Check if the value is within the soft limits of the motor.
            - Check if the motor is disabled (``.DISP`` field)

        Raises
        ------
        `MotorDisabledError`
            If the motor is passed any motion command when disabled
        ``LimitError(ValueError)``
            When the provided value is outside the range of the low
            and high limits
        """
        # First check that the user has returned a valid EPICS value. It will
        # not consult the limits of the PV itself because limits=False
        super().check_value(value)

        # Find the soft limit values from EPICS records and check that this
        # command will be accepted by the motor
        if any(self.limits):
            if not (self.low_limit <= value <= self.high_limit):
                raise LimitError("Value {} outside of range: [{}, {}]".format(
                    value, self.low_limit, self.high_limit))

        # Find the value for the disabled attribute
        if self.disabled.value == 1:
            raise MotorDisabledError("Motor is not enabled. Motion requests "
                                     "ignored")
示例#14
0
class PCDSMotorBase(EpicsMotorInterface):
    """
    EpicsMotor for PCDS

    This incapsulates all motor record implementations standard for PCDS,
    including but not exclusive to Pico, Piezo, IMS and Newport motors. While
    these types of motors may have additional records and configuration
    requirements, this class is meant to handle the shared records between
    them.

    Notes
    -----
    The purpose of this class is to account for the differences between the
    community Motor Record and the PCDS Motor Record. The points that matter
    for this class are:

        1. The ``TDIR`` field does not exist on the PCDS implementation. This
        indicates what direction the motor has travelled last. To account for
        this difference, we use the `_pos_changed` callback to keep track of
        which direction we believe the motor to be travelling and store this in
        a simple ``ophyd.Signal``
        2. The ``SPG`` field implements the three states used in the LCLS
        motor record.  This is a reduced version of the standard EPICS
        ``SPMG`` field.  Setting to ``STOP``, ``PAUSE`` and ``GO``  will
        respectively stop motor movement, pause a move in progress, or resume
        a paused move.
    """
    # Disable missing field that our EPICS motor record lacks
    # This attribute is tracked by the _pos_changed callback
    direction_of_travel = Cpt(Signal, kind='omitted')
    # This attribute changes if the motor is stopped and unable to move 'Stop',
    # paused and ready to resume on Go 'Paused', and to resume a move 'Go'.
    motor_spg = Cpt(EpicsSignal, ".SPG", kind='omitted')

    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self.stage_sigs[self.motor_spg] = 2

    def spg_stop(self):
        """
        Stops the motor.

        After which the motor must be set back to 'go' via <motor>.spg_go() in
        order to move again.
        """
        return self.motor_spg.put(value='Stop')

    def spg_pause(self):
        """
        Pauses a move.

        Move will resume if <motor>.go() is called.
        """
        return self.motor_spg.put(value='Pause')

    def spg_go(self):
        """
        Resumes paused movement.
        """
        return self.motor_spg.put(value='Go')

    def check_value(self, value):
        """
        Raise an exception if the motor cannot move to value.

        The full list of checks done in this method:

            - Check if the value is within the soft limits of the motor.
            - Check if the motor is disabled (``.DISP`` field)`
            - Check if the spg field is on "pause" or "stop"

        Raises
        ------
        `MotorDisabledError`
            If the motor is passed any motion command when disabled, or if
            the ``.SPG`` field is not set to ``Go``.
        ``LimitError(ValueError)``
            When the provided value is outside the range of the low
            and high limits
        """
        super().check_value(value)

        if self.motor_spg.value in [0, 'Stop']:
            raise MotorDisabledError("Motor is stopped.  Motion requests "
                                     "ignored until motor is set to 'Go'")

        if self.motor_spg.value in [1, 'Pause']:
            raise MotorDisabledError("Motor is paused.  If a move is set, "
                                     "motion will resume when motor is set "
                                     "to 'Go'")

    def _pos_changed(self,
                     timestamp=None,
                     old_value=None,
                     value=None,
                     **kwargs):
        # Store the internal travelling direction of the motor to account for
        # the fact that our EPICS motor does not have TDIR field
        if None not in (value, old_value):
            self.direction_of_travel.put(int(value > old_value))
        # Pass information to PositionerBase
        super()._pos_changed(timestamp=timestamp,
                             old_value=old_value,
                             value=value,
                             **kwargs)
示例#15
0
 class MyDetector(SingleTrigger, SimDetector):
     tiff1 = Cpt(TIFFPlugin, 'TIFF1:')
示例#16
0
 class MMC(EpicsMotorInterface):
     direction_of_travel = Cpt(Signal, kind='omitted')
示例#17
0
class PIM(Device, BaseInterface):
    """
    Profile Intensity Monitor.

    Consists of y-motion and zoom motors, and an area detector.

    Parameters
    ----------
    prefix : str
        The EPICS base of the PIM.

    name : str
        A name to refer to the device.

    prefix_det : str, optional
        The EPICS base PV of the detector. If None, it will be attempted to be
        inferred from `prefix`.

    prefix_zoom : str, optional
        The EPICS base PV of the zoom motor. If None, it will be attempted to
        be inferred from `prefix`.
    """

    _prefix_start = ''

    state = Cpt(PIMY, '', kind='omitted')
    zoom_motor = FCpt(IMS, '{self._prefix_zoom}', kind='normal')
    detector = FCpt(PCDSAreaDetectorEmbedded, '{self._prefix_det}',
                    kind='normal')

    tab_whitelist = ['y_motor', 'remove', 'insert', 'removed', 'inserted']
    tab_component_names = True

    def infer_prefix(self, prefix):
        """Pulls out the first two segments of the prefix PV, if not already
           done"""
        if not self._prefix_start:
            self._prefix_start = '{0}:{1}:'.format(prefix.split(':')[0],
                                                   prefix.split(':')[1])

    @property
    def prefix_start(self):
        """Returns the first two segments of the prefix PV."""
        return str(self._prefix_start)

    @property
    def removed(self):
        """Returns `True` if the yag and diode are removed from the beam."""
        return self.state.removed

    @property
    def inserted(self):
        """Returns `True` if yag or diode are inserted."""
        return self.state.inserted

    def insert(self, moved_cb=None, timeout=None, wait=False):
        """Moves the YAG into the beam."""
        return self.state.insert(moved_cb=moved_cb, timeout=timeout,
                                 wait=wait)

    def remove(self, moved_cb=None, timeout=None, wait=False):
        """Moves the YAG and diode out of the beam."""
        return self.state.remove(moved_cb=moved_cb, timeout=timeout,
                                 wait=wait)

    def __init__(self, prefix, *, name, prefix_det=None, prefix_zoom=None,
                 **kwargs):
        self.infer_prefix(prefix)

        # Infer the detector PV from the base prefix
        if prefix_det:
            self._prefix_det = prefix_det
        else:
            self._prefix_det = self.prefix_start+'CVV:01'

        # Infer the zoom motor PV from the base prefix
        if prefix_zoom:
            self._prefix_zoom = prefix_zoom
        else:
            self._prefix_zoom = self.prefix_start+'CLZ:01'

        super().__init__(prefix, name=name, **kwargs)
        self.y_motor = self.state.motor
示例#18
0
class PCDSMotorBase(EpicsMotorInterface):
    """
    EpicsMotor for PCDS.

    This incapsulates all motor record implementations standard for PCDS,
    including but not exclusive to Pico, Piezo, IMS and Newport motors. While
    these types of motors may have additional records and configuration
    requirements, this class is meant to handle the shared records between
    them.

    Notes
    -----
    The purpose of this class is to account for the differences between the
    community Motor Record and the PCDS Motor Record. The points that matter
    for this class are:

        1. The 'TDIR' field does not exist on the PCDS implementation. This
        indicates what direction the motor has travelled last. To account for
        this difference, we use the :meth:`._pos_changed` callback to keep
        track of which direction we believe the motor to be travelling
        and store this in a simple :class:`~ophyd.signal.Signal`.
        2. The 'SPG' field implements the three states used in the LCLS
        motor record.  This is a reduced version of the standard EPICS
        'SPMG' field.  Setting to 'STOP', 'PAUSE' and 'GO'  will respectively
        stop motor movement, pause a move in progress, or resume a paused move.
    """

    # Disable missing field that our EPICS motor record lacks
    # This attribute is tracked by the _pos_changed callback
    direction_of_travel = Cpt(Signal, kind='omitted')
    # This attribute changes if the motor is stopped and unable to move 'Stop',
    # paused and ready to resume on Go 'Paused', and to resume a move 'Go'.
    motor_spg = Cpt(EpicsSignal, '.SPG', kind='omitted')

    tab_whitelist = ["spg_stop", "spg_pause", "spg_go"]

    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self.stage_sigs[self.motor_spg] = 2

    def spg_stop(self):
        """
        Stops the motor.

        After which the motor must be set back to 'go' via :meth:`.spg_go` in
        order to move again.
        """

        return self.motor_spg.put(value='Stop')

    def spg_pause(self):
        """
        Pauses a move.

        Move will resume if :meth:`.go` is called.
        """

        return self.motor_spg.put(value='Pause')

    def spg_go(self):
        """Resumes paused movement."""
        return self.motor_spg.put(value='Go')

    def check_value(self, value):
        """
        Raise an exception if the motor cannot move to value.

        The full list of checks done in this method:

            - Check if the value is within the soft limits of the motor.
            - Check if the motor is disabled ('.DISP' field).
            - Check if the spg field is on "pause" or "stop".

        Raises
        ------
        MotorDisabledError
            If the motor is passed any motion command when disabled, or if
            the '.SPG' field is not set to 'Go'.

        ~ophyd.utils.errors.LimitError
            When the provided value is outside the range of the low
            and high limits.
        """

        super().check_value(value)

        if self.motor_spg.get() in [0, 'Stop']:
            raise MotorDisabledError("Motor is stopped.  Motion requests "
                                     "ignored until motor is set to 'Go'")

        if self.motor_spg.get() in [1, 'Pause']:
            raise MotorDisabledError("Motor is paused.  If a move is set, "
                                     "motion will resume when motor is set "
                                     "to 'Go'")

    def _pos_changed(self,
                     timestamp=None,
                     old_value=None,
                     value=None,
                     **kwargs):
        # Store the internal travelling direction of the motor to account for
        # the fact that our EPICS motor does not have TDIR field
        try:
            comparison = int(value > old_value)
            self.direction_of_travel.put(comparison)
        except TypeError:
            # We have some sort of null/None/default value
            logger.debug('Could not compare value=%s > old_value=%s', value,
                         old_value)
        # Pass information to PositionerBase
        super()._pos_changed(timestamp=timestamp,
                             old_value=old_value,
                             value=value,
                             **kwargs)

    def screen(self):
        """
        Opens Epics motor expert screen e.g. for reseting motor after stalling.
        """
        executable = 'motor-expert-screen'
        if shutil.which(executable) is None:
            logger.error('%s is not on path, we cannot start the screen',
                         executable)
            return
        arg = self.prefix
        os.system(executable + ' ' + arg)
示例#19
0
 class TestDet(SimDetector):
     p = Cpt(TIFFPlugin, 'TIFF1:')
示例#20
0
class EpicsMotorInterface(FltMvInterface, EpicsMotor):
    """
    The standard EpicsMotor class, but with our interface attached.

    This includes some PCDS preferences, but not any IOC differences.

    Notes
    -----
    The full list of preferences implemented here are:

        1. Use the FltMvInterface mixin class for some name aliases and
           bells-and-whistles level functions.
        2. Instead of using the limit fields on the setpoint PV, the EPICS
           motor class has the 'LLM' and 'HLM' soft limit fields for
           convenient usage. Unfortunately, pyepics does not update its
           internal cache of the limits after the first get attempt. We
           therefore disregard the internal limits of the PV and use the soft
           limit records exclusively.
        3. The disable puts field '.DISP' is added, along with :meth:`enable`
           and :meth:`disable` convenience methods. When '.DISP' is 1, puts to
           the motor record will be ignored, effectively disabling the
           interface.
        4. The description field keeps track of the motors scientific use along
           the beamline.
    """
    # Enable/Disable puts
    disabled = Cpt(EpicsSignal, ".DISP", kind='omitted')
    set_metadata(disabled, dict(variety='command-enum'))
    # Description is valuable
    description = Cpt(EpicsSignal, '.DESC', kind='normal')
    # Current Dial position
    dial_position = Cpt(EpicsSignalRO, '.DRBV', kind='normal')

    tab_whitelist = [
        "set_current_position", "home", "velocity", "enable", "disable",
        "check_limit_switches", "get_low_limit", "set_low_limit",
        "get_high_limit", "set_high_limit"
    ]

    set_metadata(EpicsMotor.home_forward, dict(variety='command-proc',
                                               value=1))
    EpicsMotor.home_forward.kind = Kind.normal
    set_metadata(EpicsMotor.home_reverse, dict(variety='command-proc',
                                               value=1))
    EpicsMotor.home_reverse.kind = Kind.normal
    set_metadata(EpicsMotor.low_limit_switch, dict(variety='bitmask', bits=1))
    EpicsMotor.low_limit_switch.kind = Kind.normal
    set_metadata(EpicsMotor.high_limit_switch, dict(variety='bitmask', bits=1))
    EpicsMotor.high_limit_switch.kind = Kind.normal
    set_metadata(EpicsMotor.motor_done_move, dict(variety='bitmask', bits=1))
    EpicsMotor.motor_done_move.kind = Kind.omitted
    set_metadata(EpicsMotor.motor_is_moving, dict(variety='bitmask', bits=1))
    EpicsMotor.motor_is_moving.kind = Kind.normal
    set_metadata(EpicsMotor.motor_stop, dict(variety='command-proc', value=1))
    EpicsMotor.motor_stop.kind = Kind.normal
    EpicsMotor.high_limit_travel.kind = Kind.config
    EpicsMotor.low_limit_travel.kind = Kind.config
    EpicsMotor.direction_of_travel.kind = Kind.normal

    def format_status_info(self, status_info):
        """
        Override status info handler to render the motor.

        Display motor status info in the ipython terminal.

        Parameters
        ----------
        status_info: dict
            Nested dictionary. Each level has keys name, kind, and is_device.
            If is_device is True, subdevice dictionaries may follow. Otherwise,
            the only other key in the dictionary will be value.

        Returns
        -------
        status: str
            Formatted string with all relevant status information.
        """
        description = get_status_value(status_info, 'description', 'value')
        units = get_status_value(status_info, 'user_setpoint', 'units')
        dial = get_status_value(status_info, 'dial_position', 'value')
        user = get_status_value(status_info, 'position')

        low, high = self.limits
        switch_limits = self.check_limit_switches()

        name = ' '.join(self.prefix.split(':'))
        name = f'{name}: {self.prefix}'
        if description:
            name = f'{description}: {self.prefix}'

        return f"""\
{name}
Current position (user, dial): {user}, {dial} [{units}]
User limits (low, high): {low}, {high} [{units}]
Preset position: {self.presets.state()}
Limit Switch: {switch_limits}
"""

    @property
    def limits(self):
        """Override the limits attribute"""
        return self._get_epics_limits()

    def _get_epics_limits(self):
        limits = self.user_setpoint.limits
        if limits is None or limits == (None, None):
            # Not initialized
            return (0, 0)
        return limits

    def enable(self):
        """
        Enables the motor.

        When disabled, all EPICS puts to the base record will be dropped.
        """

        return self.disabled.put(value=0)

    def disable(self):
        """
        Disables the motor.

        When disabled, all EPICS puts to the base record will be dropped.
        """

        return self.disabled.put(value=1)

    def check_value(self, value):
        """
        Raise an exception if the motor cannot move to value.

        The full list of checks done in this method:

            - Check if the value is within the soft limits of the motor.
            - Check if the motor is disabled ('.DISP' field).

        Raises
        ------
        MotorDisabledError
            If the motor is passed any motion command when disabled.

        ~ophyd.utils.errors.LimitError
            When the provided value is outside the range of the low
            and high limits.
        """

        # First check that the user has returned a valid EPICS value. It will
        # not consult the limits of the PV itself because limits=False
        super().check_value(value)

        # Find the soft limit values from EPICS records and check that this
        # command will be accepted by the motor
        if any(self.limits):
            if not (self.low_limit <= value <= self.high_limit):
                raise LimitError("Value {} outside of range: [{}, {}]".format(
                    value, self.low_limit, self.high_limit))

        # Find the value for the disabled attribute
        if self.disabled.get() == 1:
            raise MotorDisabledError("Motor is not enabled. Motion requests "
                                     "ignored")

    def check_limit_switches(self):
        """
        Check the limits switches.

        Returns
        -------
        limit_switch_indicator : str
            Indicate which limit switch is activated.
        """
        low = self.low_limit_switch.get()
        high = self.high_limit_switch.get()
        if low and high:
            return "Low [x] High [x]"
        elif low:
            return "Low [x] High []"
        elif high:
            return "Low [] High [x]"
        else:
            return "Low [] High []"

    def get_low_limit(self):
        """Get the low limit."""
        return self.low_limit_travel.get()

    def set_low_limit(self, value):
        """
        Set the low limit.

        Parameters
        ----------
        value : float
            Limit of travel in the negative direction.

        Raises
        ------
        ValueError
            When motor in motion or position outside of limit.
        """
        if self.moving:
            raise ValueError('Motor is in motion, cannot set the low limit!')

        if value > self.position:
            raise ValueError(f'Could not set motor low limit to {value} at'
                             f' position {self.position}. Low limit must '
                             'be lower than the current position.')

        _current_high_limit = self.limits[1]
        if value > _current_high_limit:
            raise ValueError(f'Could not set motor low limit to {value}.'
                             'Low limit must be lower than the current'
                             f'high limit: {_current_high_limit}')

        # update EPICS limits
        self.low_limit_travel.put(value)

    def get_high_limit(self):
        """Get high limit."""
        return self.high_limit_travel.get()

    def set_high_limit(self, value):
        """
        Limit of travel in the positive direction.

        Parameters
        ----------
        value : float
            High limit value to be set.

        Raises
        ------
        ValueError
            When motor in motion or position outside of limit.
        """
        if self.moving:
            raise ValueError('Motor is in motion, cannot set the high limit!')

        if value < self.position:
            raise ValueError(f'Could not set motor high limit to {value} '
                             f'at position {self.position}. High limit '
                             'must be higher than the current position.')

        _current_low_limit = self.limits[0]
        if value < _current_low_limit:
            raise ValueError(f'Could not set motor high limit to {value}. '
                             'High limit must be higher than the current low '
                             f'limit: {_current_low_limit}')
        # update EPICS limits
        self.high_limit_travel.put(value)

    def clear_limits(self):
        """Set both low and high limits to 0."""
        self.high_limit_travel.put(0)
        self.low_limit_travel.put(0)
示例#21
0
 class MyDetector(SimDetector):
     tiff1 = Cpt(TIFFPlugin, 'TIFF1:')
示例#22
0
class IMS(PCDSMotorBase):
    """
    PCDS implementation of the Motor Record for IMS motors.

    This is a subclass of :class:`PCDSMotorBase`.
    """

    __doc__ += basic_positioner_init
    # Bit masks to clear errors and flags
    _bit_flags = {
        'powerup': {
            'clear': 36,
            'readback': 24
        },
        'stall': {
            'clear': 40,
            'readback': 22
        },
        'error': {
            'clear': 48,
            'readback': 15,
            'mask': 0x7f
        }
    }
    # Custom IMS bit fields
    reinit_command = Cpt(EpicsSignal, '.RINI', kind='omitted')
    bit_status = Cpt(EpicsSignalRO, '.MSTA', kind='omitted')
    seq_seln = Cpt(EpicsSignal, ':SEQ_SELN', kind='omitted')
    error_severity = Cpt(EpicsSignal, '.SEVR', kind='omitted')
    part_number = Cpt(EpicsSignalRO, '.PN', kind='omitted')

    # IMS velocity has limits
    velocity = Cpt(EpicsSignal, '.VELO', limits=True, kind='config')
    velocity_base = Cpt(EpicsSignal, '.VBAS', kind='omitted')
    velocity_max = Cpt(EpicsSignal, '.VMAX', kind='config')

    tab_whitelist = ['auto_setup', 'reinitialize', 'clear_.*']

    def stage(self):
        """
        Stage the IMS motor.

        This clears all present flags on the motor and reinitializes the motor
        if we don't register a valid part number.
        """

        # Check the part number to avoid crashing the IOC
        if not self.part_number.get() or self.error_severity.get() == 3:
            self.reinitialize(wait=True)
        # Clear any pre-existing flags
        self.clear_all_flags()
        return super().stage()

    def auto_setup(self):
        """
        Automated setup of the IMS motor.

        If necessarry this command will:

            * Reinitialize the motor.
            * Clear powerup, stall and error flags.
        """

        # Reinitialize if necessary
        if not self.part_number.get() or self.error_severity.get() == 3:
            self.reinitialize(wait=True)
        # Clear all flags
        self.clear_all_flags()

    def reinitialize(self, wait=False):
        """
        Reinitialize the IMS motor.

        Parameters
        ----------
        wait : bool
            Wait for the motor to be fully intialized.

        Returns
        -------
        :class:`~ophyd.status.SubscriptionStatus`
            Status object reporting the initialization state of the motor.
        """
        logger.info('Reinitializing motor')
        # Issue command
        self.reinit_command.put(1)

        # Check error
        def initialize_complete(value=None, **kwargs):
            return value != 3

        # Generate a status
        st = SubscriptionStatus(self.error_severity,
                                initialize_complete,
                                settle_time=0.5)
        # Wait on status if requested
        if wait:
            status_wait(st)
        return st

    def clear_all_flags(self):
        """Clear all the flags from the IMS motor."""
        # Clear all flags
        for func in (self.clear_powerup, self.clear_stall, self.clear_error):
            func(wait=True)

    def clear_powerup(self, wait=False, timeout=10):
        """Clear powerup flag."""
        return self._clear_flag('powerup', wait=wait, timeout=timeout)

    def clear_stall(self, wait=False, timeout=5):
        """Clear stall flag."""
        return self._clear_flag('stall', wait=wait, timeout=timeout)

    def clear_error(self, wait=False, timeout=10):
        """Clear error flag."""
        return self._clear_flag('error', wait=wait, timeout=timeout)

    def _clear_flag(self, flag, wait=False, timeout=10):
        """Clear flag whose information is in :attr:`._bit_flags`"""
        # Gather our flag information
        flag_info = self._bit_flags[flag]
        bit = flag_info['readback']
        mask = flag_info.get('mask', 1)

        # Create a callback function to check for bit
        def flag_is_cleared(value=None, **kwargs):
            return not bool((int(value) >> bit) & mask)

        # Check that we need to actually set the flag
        if flag_is_cleared(value=self.bit_status.get()):
            logger.debug("%s flag is not currently active", flag)
            st = DeviceStatus(self)
            st.set_finished()
            return st

        # Issue our command
        logger.info('Clearing %s flag ...', flag)
        self.seq_seln.put(flag_info['clear'])
        # Generate a status
        st = SubscriptionStatus(self.bit_status, flag_is_cleared)
        if wait:
            status_wait(st, timeout=timeout)
        return st
示例#23
0
 class MyDetector(SingleTrigger, SimDetector):
     tiff1 = Cpt(TIFFPlugin, 'TIFF1:')
     stats1 = Cpt(StatsPlugin, 'Stats1:')
     roi1 = Cpt(ROIPlugin, 'ROI1:')
示例#24
0
class Newport(PCDSMotorBase):
    """
    PCDS implementation of the Motor Record for Newport motors.

    This is a subclass of :class:`PCDSMotorBase` that:
    - Overwrites missing signals for Newports
    - Disables the :meth:`home` method broken for Newports
    - Does special metadata handling because this is broken for Newports
    """

    __doc__ += basic_positioner_init
    # Overrides are in roughly the same order as from EpicsMotor

    # Override from EpicsMotor to change class for MD update
    user_readback = Cpt(EpicsSignalROEditMD,
                        '.RBV',
                        kind='hinted',
                        auto_monitor=True)
    user_setpoint = Cpt(EpicsSignalEditMD,
                        '.VAL',
                        limits=True,
                        auto_monitor=True)

    # Override from EpicsMotor to disable
    offset_freeze_switch = Cpt(Signal, kind='omitted')

    # Override from EpicsMotor to add subscription
    motor_egu = Cpt(EpicsSignal, '.EGU', kind='config', auto_monitor=True)

    # Override from EpicsMotor to add subscription
    high_limit_travel = Cpt(EpicsSignal,
                            '.HLM',
                            kind='omitted',
                            auto_monitor=True)
    low_limit_travel = Cpt(EpicsSignal,
                           '.LLM',
                           kind='omitted',
                           auto_monitor=True)

    # Override from EpicsMotor to disable
    home_forward = Cpt(Signal, kind='omitted')
    home_reverse = Cpt(Signal, kind='omitted')

    # Add new signal for subscription
    motor_prec = Cpt(EpicsSignalRO, '.PREC', kind='omitted', auto_monitor=True)

    def home(self, *args, **kwargs):
        # This function should eventually be used. There is a way to home
        # Newport motors to a reference mark
        raise NotImplementedError("Homing is not yet implemented for Newport "
                                  "motors")

    # This needs to be re-done if you override user_readback
    @user_readback.sub_value
    def _pos_changed(self, *args, **kwargs):
        super()._pos_changed(*args, **kwargs)

    @motor_egu.sub_value
    def _update_units(self, value, **kwargs):
        self.user_readback._override_metadata(units=value)
        self.user_setpoint._override_metadata(units=value)

    @high_limit_travel.sub_value
    def _update_hlt(self, value, **kwargs):
        self.user_readback._override_metadata(upper_ctrl_limit=value)
        self.user_setpoint._override_metadata(upper_ctrl_limit=value)

    @low_limit_travel.sub_value
    def _update_llt(self, value, **kwargs):
        self.user_readback._override_metadata(lower_ctrl_limit=value)
        self.user_setpoint._override_metadata(lower_ctrl_limit=value)

    @motor_prec.sub_value
    def _update_prec(self, value, **kwargs):
        self.user_readback._override_metadata(precision=value)
        self.user_setpoint._override_metadata(precision=value)
示例#25
0
 class MyDetector(SingleTrigger, SimDetector):
     plugins = Cpt(PluginGroup, '')
     roi1 = Cpt(ROIPlugin, 'ROI1:')
     stats1 = Cpt(StatsPlugin, 'Stats1:')
示例#26
0
class BeckhoffAxis(EpicsMotorInterface):
    """
    Beckhoff Axis motor record as implemented by ESS and extended by us.

    This class adds a convenience :meth:`.clear_error` method, and makes
    sure to call it on stage.

    It also exposes the PLC debug PVs.
    """

    __doc__ += basic_positioner_init
    tab_whitelist = ['clear_error']

    plc = Cpt(BeckhoffAxisPLC,
              ':PLC:',
              kind='normal',
              doc='PLC error handling.')
    motor_spmg = Cpt(EpicsSignal,
                     '.SPMG',
                     kind='config',
                     doc='Stop, Pause, Move, Go')

    # Clear the normal homing PVs that don't really work here
    home_forward = None
    home_reverse = None

    def clear_error(self):
        """Clear any active motion errors on this axis."""
        self.plc.cmd_err_reset.put(1)

    def stage(self):
        """
        Stage the Beckhoff axis.

        This simply clears any errors. Stage is called at the start of most
        :mod:`bluesky` plans.
        """

        self.clear_error()
        return super().stage()

    @raise_if_disconnected
    def home(self, direction=None, wait=True, **kwargs):
        """
        Perform the configured homing function.

        This is set on the controller. Unlike other kinds of axes, only
        the single pre-programmed homing routine can be used, so the
        ``direction`` argument has no effect.
        """
        self._run_subs(sub_type=self._SUB_REQ_DONE, success=False)
        self._reset_sub(self._SUB_REQ_DONE)

        status = MoveStatus(self,
                            self.plc.home_pos.get(),
                            timeout=None,
                            settle_time=self._settle_time)
        self.plc.cmd_home.put(1)

        self.subscribe(status._finished,
                       event_type=self._SUB_REQ_DONE,
                       run=False)

        try:
            if wait:
                status_wait(status)
        except KeyboardInterrupt:
            self.stop()
            raise

        return status
示例#27
0
 class MyDetector(SimDetector):
     imageplugin = Cpt(ImagePlugin, ImagePlugin._default_suffix)
     statsplugin = Cpt(StatsPlugin, StatsPlugin._default_suffix)
     colorconvplugin = Cpt(ColorConvPlugin, ColorConvPlugin._default_suffix)
     processplugin = Cpt(ProcessPlugin, ProcessPlugin._default_suffix)
     overlayplugin = Cpt(OverlayPlugin, OverlayPlugin._default_suffix)
     roiplugin = Cpt(ROIPlugin, ROIPlugin._default_suffix)
     transformplugin = Cpt(TransformPlugin, TransformPlugin._default_suffix)
     netcdfplugin = Cpt(NetCDFPlugin, NetCDFPlugin._default_suffix)
     tiffplugin = Cpt(TIFFPlugin, TIFFPlugin._default_suffix)
     jpegplugin = Cpt(JPEGPlugin, JPEGPlugin._default_suffix)
     # nexusplugin = Cpt(NexusPlugin, NexusPlugin._default_suffix)
     hdf5plugin = Cpt(HDF5Plugin, HDF5Plugin._default_suffix)
示例#28
0
class AmiDet(Device):
    """
    Detector that gets data from pyami scalars.

    The data will be in the form of an accumulated mean, rms, and number
    of entries used in the calculations. The raw data is not avaiable via
    pyami.

    This only supports scalars. The array features are known to crash both the
    python session and active ami clients, so don't use them.

    Parameters
    ----------
    prefix: ``str``
        The ami name to use to retrieve the data.

    name: ``str``, required keyword
        The shorter name to use to label the data.

    filter_str: ``str``, optional
        If provided, we'll filter the incoming data using this filter string.
        If omitted or None, we'll use the last set_l3t string.
        If False, but not None, we'll do no filtering at all. This includes the
        empty string.

    min_duration: ``float``, optional
        If provided, we'll wait this many seconds before declaring the
        acquisition as complete. Otherwise, we'll stop acquring on read.

    normalize: ``bool`` or ``AmiDet``, optional
        Determines the normalization behavior of this detector. The default is
        ``True``, which means normalize to the current ``monitor_det``. See
        `set_monitor_det`. ``False`` means do not normalize. You can also pass
        in any other detector to normalize against something that is not the
        ``monitor_det``.
    """
    mean = Cpt(Signal, value=0., kind='hinted')
    err = Cpt(Signal, value=0., kind='hinted')
    entries = Cpt(Signal, value=0, kind='hinted')
    mean_raw = Cpt(Signal, value=0., kind='normal')
    err_raw = Cpt(Signal, value=0., kind='normal')
    mean_mon = Cpt(Signal, value=0., kind='normal')
    err_mon = Cpt(Signal, value=0., kind='normal')
    entries_mon = Cpt(Signal, value=0., kind='normal')
    mon_prefix = Cpt(Signal, value='', kind='normal')
    rms = Cpt(Signal, value=0., kind='omitted')

    def __init__(self, prefix, *, name, filter_string=None, min_duration=0,
                 normalize=True):
        auto_setup_pyami()
        self._entry = None
        self._monitor = None
        self.filter_string = filter_string
        self.min_duration = min_duration
        self.normalize = normalize
        super().__init__(prefix, name=name)

    def stage(self):
        """
        Called early in a bluesky scan to initialize the pyami.Entry object.

        Note that pyami.Entry objects begin accumulating data immediately.

        This will be when the filter_string is used to determine how to filter
        the pyami data. Setting the filter_string after stage is called will
        have no effect.

        Internally this creates a new pyami.Entry object. These objects start
        accumulating data immediately.
        """
        if self.filter_string is None and last_filter_string is not None:
            self._entry = pyami.Entry(self.prefix, 'Scalar',
                                      last_filter_string)
        elif self.filter_string:
            self._entry = pyami.Entry(self.prefix, 'Scalar',
                                      self.filter_string)
        else:
            self._entry = pyami.Entry(self.prefix, 'Scalar')
        if self.normalize:
            if isinstance(self.normalize, AmiDet):
                self._monitor = self.normalize
            else:
                self._monitor = monitor_det
            if self._monitor is not None:
                self.mon_prefix.put(self._monitor.prefix)
        return super().stage()

    def unstage(self):
        """
        Called late in a bluesky scan to remove the pyami.Entry object and the
        monitor.
        """
        self._entry = None
        if self._monitor is not None and self._monitor is not self:
            self._monitor.unstage()
            unstaged = super().unstage() + [self._monitor]
        else:
            unstaged = super().unstage()
        self._monitor = None
        self.mon_prefix.put('')
        return unstaged

    def trigger(self):
        """
        Called during a bluesky scan to clear the accumulated pyami data.

        This must be done because the pyami.Entry objects continually
        accumulate data forever. You can stop it by deleting the objects
        as in `unstage`, and you can clear it here to at least start from a
        clean slate.

        If min_duration is zero, this will return a status already marked done
        and successful. Otherwise, this will return a status that will be
        marked done after min_duration seconds.

        If there is a normalization detector in use and it has not been staged,
        it will be staged during the first trigger in a scan.
        """
        if self._entry is None:
            raise RuntimeError('AmiDet %s(%s) was never staged!', self.name,
                               self.prefix)
        if self._monitor is not None and self._monitor is not self:
            if self._monitor._staged != Staged.yes:
                self._monitor.unstage()
                self._monitor.stage()
            monitor_status = self._monitor.trigger()
        else:
            monitor_status = None
        self._entry.clear()
        if self.min_duration:
            def inner(duration, status):
                time.sleep(duration)
                status.set_finished()
            status = Status(obj=self)
            Thread(target=inner, args=(self.min_duration, status)).start()
        else:
            status = Status(obj=self)
            status.set_finished()
        if monitor_status is None:
            return status
        else:
            return status & monitor_status

    def get(self, *args, **kwargs):
        self._get_data()
        return super().get(*args, **kwargs)

    def read(self, *args, **kwargs):
        self._get_data()
        return super().read(*args, **kwargs)

    def _get_data(self):
        """
        Helper function that stuffs ami data into this device's signals.

        Parameters
        ----------
        del_entry: ``bool``
            If ``True``, we'll clear the accumulated data after getting it.
        """
        if self._entry is None:
            raise RuntimeError('Must stage AmiDet to begin accumulating data')

        data = self._entry.get()
        self.mean_raw.put(data['mean'])
        self.rms.put(data['rms'])
        self.entries.put(data['entries'])
        # Calculate the standard error because old python did
        if data['entries']:
            data['err'] = data['rms']/np.sqrt(data['entries'])
        else:
            data['err'] = 0
        self.err_raw.put(data['err'])

        def adj_error(det_mean, det_err, mon_mean, mon_err):
            return det_err/mon_mean + mon_err * (det_mean/mon_mean)**2

        if self._monitor is None:
            self.mean.put(data['mean'])
            self.err.put(data['err'])
            self.mean_mon.put(0)
            self.err_mon.put(0)
            self.entries_mon.put(0)
        elif self._monitor is self:
            self.mean.put(1)
            if data['mean'] == 0:
                self.err.put(np.nan)
            else:
                self.err.put(adj_error(data['mean'], data['err'],
                                       data['mean'], data['err']))
            self.mean_mon.put(data['mean'])
            self.err_mon.put(data['err'])
            self.entries_mon.put(data['entries'])
        else:
            mon_data = self._monitor.get()
            if mon_data.mean_raw == 0:
                self.mean.put(np.nan)
                self.err.put(np.nan)
            else:
                self.mean.put(data['mean']/mon_data.mean_raw)
                self.err.put(adj_error(data['mean'], data['err'],
                                       mon_data.mean_raw,
                                       mon_data.err_raw))
            self.mean_mon.put(mon_data.mean_raw)
            self.err_mon.put(mon_data.err_raw)
            self.entries_mon.put(mon_data.entries)

    def put(self, *args, **kwargs):
        raise ReadOnlyError('AmiDet is read-only')

    def set_det_filter(self, *args, event_codes=None, operator='&'):
        """
        Set the filter on this detector only.

        This lets you override the l3t filter for a single AmiDet. Call with
        no arguments to revert to the last l3t filter. Call with a simple
        ``False`` to disable filtering on this detector. Call as you would to
        set the l3t filter to setup a normal filtering override.

        Parameters
        ----------
        *args: (``AmiDet``, ``float``, ``float``) n times
            A sequence of (detector, low, high), which create filters that make
            sure the detector is between low and high. If instead, the first
            argument is ``False``, we'll disable filtering on this detector.

        event_codes: ``list``, optional
            A list of event codes to include in the filter. l3pass will be when
            the event code is present.

        operator: ``str``, optional
            The operator for combining the detector ranges and event codes.
            This can either be ``|`` to ``or`` the conditions together, so
            l3pass will happen if any filter passes, or it can be left at the
            default ``&`` to ``and`` the conditions together, so l3pass will
            only happen if all filters pass.
        """
        if len(args) == 1 and not args[0]:
            self.filter_string = False
        else:
            self.filter_string = dets_filter(*args, event_codes=event_codes,
                                             operator=operator)
示例#29
0
 class MyDetector(SingleTrigger, SimDetector):
     hdf1 = Cpt(FS_hdf, 'HDF1:',
                write_path_template=wpath,
                read_path_template=rpath,
                root=root, reg=fs)
示例#30
0
class PIM(BaseInterface, Device):
    """
    Profile Intensity Monitor.

    Consists of y-motion and zoom motors, and an area detector.

    Parameters
    ----------
    prefix : str
        The EPICS base of the PIM.

    name : str
        A name to refer to the device.

    prefix_det : str, optional
        The EPICS base PV of the detector. If None, it will be attempted to be
        inferred from `prefix`.

    prefix_zoom : str, optional
        The EPICS base PV of the zoom motor. If None, it will be attempted to
        be inferred from `prefix`.
    """

    _prefix_start = ''

    state = Cpt(PIMY, '', kind='normal')
    zoom_motor = FCpt(IMS, '{self._prefix_zoom}', kind='normal')
    detector = FCpt(PCDSAreaDetectorEmbedded,
                    '{self._prefix_det}',
                    kind='normal')

    tab_whitelist = ['y_motor', 'remove', 'insert', 'removed', 'inserted']
    tab_component_names = True

    def infer_prefix(self, prefix):
        """Pulls out the first two segments of the prefix PV, if not already
           done"""
        if not self._prefix_start:
            self._prefix_start = '{0}:{1}:'.format(
                prefix.split(':')[0],
                prefix.split(':')[1])

    def format_status_info(self, status_info):
        """
        Override status info handler to render the PIM.

        Display pim status info in the ipython terminal.

        Parameters
        ----------
        status_info: dict
            Nested dictionary. Each level has keys name, kind, and is_device.
            If is_device is True, subdevice dictionaries may follow. Otherwise,
            the only other key in the dictionary will be value.
        Returns
        -------
        status: str
            Formatted string with all relevant status information.
        """
        focus = get_status_value(status_info, 'focus_motor', 'position')
        f_units = get_status_value(status_info, 'focus_motor', 'user_setpoint',
                                   'units')
        state_pos = get_status_value(status_info, 'state', 'position')
        y_pos = get_status_float(status_info,
                                 'state',
                                 'motor',
                                 'position',
                                 precision=4)
        y_units = get_status_value(status_info, 'state', 'motor',
                                   'user_setpoint', 'units')
        zoom = get_status_float(status_info,
                                'zoom_motor',
                                'position',
                                precision=4)
        z_units = get_status_value(status_info, 'zoom_motor', 'user_setpoint',
                                   'units')

        name = ' '.join(self.prefix.split(':'))
        if focus != 'N/A':
            focus = f' Focus: {focus} [{f_units}]'
        else:
            focus = ''
        if zoom != 'N/A':
            zoom = f'Navitar Zoom: {zoom} [{z_units}]'
        else:
            zoom = ''

        return f"""\
{name}: {state_pos}
Y Position: {y_pos} [{y_units}]
{zoom}{focus}
"""

    @property
    def prefix_start(self):
        """Returns the first two segments of the prefix PV."""
        return str(self._prefix_start)

    @property
    def removed(self):
        """Returns `True` if the yag and diode are removed from the beam."""
        return self.state.removed

    @property
    def inserted(self):
        """Returns `True` if yag or diode are inserted."""
        return self.state.inserted

    def insert(self, moved_cb=None, timeout=None, wait=False):
        """Moves the YAG into the beam."""
        return self.state.insert(moved_cb=moved_cb, timeout=timeout, wait=wait)

    def remove(self, moved_cb=None, timeout=None, wait=False):
        """Moves the YAG and diode out of the beam."""
        return self.state.remove(moved_cb=moved_cb, timeout=timeout, wait=wait)

    def __init__(self,
                 prefix,
                 *,
                 name,
                 prefix_det=None,
                 prefix_zoom=None,
                 **kwargs):
        self.infer_prefix(prefix)

        # Infer the detector PV from the base prefix
        if prefix_det:
            self._prefix_det = prefix_det
        else:
            self._prefix_det = self.prefix_start + 'CVV:01'

        # Infer the zoom motor PV from the base prefix
        if prefix_zoom:
            self._prefix_zoom = prefix_zoom
        else:
            self._prefix_zoom = self.prefix_start + 'CLZ:01'

        super().__init__(prefix, name=name, **kwargs)
        self.y_motor = self.state.motor