Esempio n. 1
0
class SamMotor(SndMotor):
    offset_freeze_switch = Cmp(Signal)
    home_forward = Cmp(Signal)
    home_reverse = Cmp(Signal)

    def check_value(self, value, retries=5):
        """
        Check if the value is within the soft limits of the motor.

        Raises
        ------
        ValueError
        """
        if value is None:
            raise ValueError('Cannot write None to epics PVs')

        for i in range(retries):
            try:
                low_limit, high_limit = self.limits
                if not (low_limit <= value <= high_limit):
                    raise LimitError(
                        "Value {} outside of range: [{}, {}]".format(
                            value, low_limit, high_limit))
                return
            except TypeError:
                logger.warning("Failed to get limits, retrying...")
                if i == retries - 1:
                    raise
Esempio n. 2
0
class SndEpicsMotor(PCDSMotorBase, SndMotor):
    """
    SnD motor that inherits from EpicsMotor, therefore having all the relevant
    signals
    """
    direction_of_travel = Cmp(Signal)
    motor_spg = Cmp(Signal, value=2)
Esempio n. 3
0
class LimCls(PVStatePositioner):
    lowlim = Cmp(PrefixSignal, 'lowlim')
    highlim = Cmp(PrefixSignal, 'highlim')

    _state_logic = {'lowlim': {0: 'in',
                               1: 'defer'},
                    'highlim': {0: 'out',
                                1: 'defer'}}

    _states_alias = {'in': 'IN', 'out': 'OUT'}
Esempio n. 4
0
    class FakeYag(Device):
        xwidth = Cmp(SynSignal,
                     func=lambda: fake_slits.read()[pre + '_xwidth']['value'])
        ywidth = Cmp(SynSignal,
                     func=lambda: fake_slits.read()[pre + '_ywidth']['value'])

        def trigger(self):
            xstat = self.xwidth.trigger()
            ystat = self.ywidth.trigger()
            return xstat & ystat
Esempio n. 5
0
class FakeSlits(Device):
    xwidth = Cmp(SynAxis)
    ywidth = Cmp(SynAxis)

    def set(self, x, y=None, **kwargs):
        if y is None:
            y = x
        x_status = self.xwidth.set(x)
        y_status = self.ywidth.set(y)
        return x_status & y_status
Esempio n. 6
0
class CalibTest(CalibMotor):
    motor = Cmp(SynAxis, name="test_axis")

    def __init__(self, *args, name="calib", m1=None, m2=None, **kwargs):
        super().__init__(*args, name="calib", **kwargs)
        self.calib_detector = SynCamera(m1, m2, self.motor, name="camera")
        self.calib_motors = [
            m1,
            m2,
        ]
        self.motor_fields = [self.motor.name]
        self.detector_fields = [
            'centroid_x',
            'centroid_y',
        ]
        self.set = self.move
        for m in [self.motor] + self.calib_motors:
            m.move = m.set

    @property
    def position(self):
        return self.motor.position

    def move(self, position, *args, **kwargs):
        # Perform the calibration move
        status = self.motor.set(position, *args, *kwargs)
        if self.has_calib and self.use_calib:
            status = status & self._calib_compensate(position)
        return status
Esempio n. 7
0
class Filter(InOutPositioner):
    """
    A single attenuation blade.

    Each of these has it's own in/out state, thickness, and material that are
    used in the attenuator IOC's calculations. It also has the capability to
    mark itself as ``STUCK IN`` or ``STUCK OUT`` so the transmission calculator
    can work around mechanical problems.

    This is not intended to be instantiated by a user, but instead included as
    a ``Component`` in a subclass of `AttBase`. You can instantiate these
    classes via the `Attenuator` factory function.
    """
    state = Cmp(EpicsSignal, ':STATE', write_pv=':GO')
    thickness = Cmp(EpicsSignal, ':THICK')
    material = Cmp(EpicsSignal, ':MATERIAL')
    stuck = Cmp(EpicsSignal, ':STUCK')
Esempio n. 8
0
 def change_all_plugin_types(comp):
     if hasattr(comp, "component_names"):
         if hasattr(comp, "_plugin_type"):
             comp.plugin_type = Cmp(Signal, value=comp._plugin_type)
         else:
             for name in comp.component_names:
                 sub_comp = getattr(comp, name)
                 if type(sub_comp) is Cmp:
                     sub_comp = change_all_plugin_types(sub_comp.cls)
     return comp
Esempio n. 9
0
class SynCamera(Device):
    """
    Simulated camera that has centroids as components. 
    """
    centroid_x = Cmp(SynCentroid, motors=[], weights=[1, 0.25])
    centroid_y = Cmp(SynCentroid, motors=[], weights=[1, -0.25])

    def __init__(self, motor1, motor2, delay, name=None, *args, **kwargs):
        # Create the base class
        super().__init__("SYN:CAMERA", name=name, *args, **kwargs)
        
        # Define the centroid components using the inputted motors
        self.centroid_x.motors = [motor1, delay]
        self.centroid_y.motors = [motor2, delay]
        
        # Add them to _signals
        self._signals['centroid_x'] = self.centroid_x
        self._signals['centroid_y'] = self.centroid_y
        # Add them to the read_attrs
        self.read_attrs = ["centroid_x", "centroid_y"]

    def trigger(self):
        return self.centroid_x.trigger() & self.centroid_y.trigger()
Esempio n. 10
0
def _make_att_classes(max_filters, base, name):
    """
    Generate all possible subclasses.
    """
    att_classes = {}
    for i in range(1, max_filters + 1):
        att_filters = {}
        for n in range(1, i + 1):
            comp = Cmp(Filter, ':{:02}'.format(n))
            att_filters['filter{}'.format(n)] = comp

        name = '{}{}'.format(name, i)
        cls = type(name, (base, ), att_filters)
        # Store the number of filters
        cls.num_att = i
        att_classes[i] = cls
    return att_classes
Esempio n. 11
0
class AttBase3rd(AttBase):
    """
    `Attenuator` class to use the 3rd harmonic values.

    This is exactly the same as the normal `AttBase`, but with the alternative
    transmission PVs. It can be instantiated using the ``use_3rd`` argument in
    the `Attenuator` factory function.
    """
    # Positioner Signals
    setpoint = Cmp(EpicsSignal, ':COM:R3_DES')
    readback = Cmp(EpicsSignalRO, ':COM:R3_CUR')

    # Attenuator Signals
    energy = Cmp(EpicsSignalRO, ':COM:T_CALC.VALH')
    trans_ceil = Cmp(EpicsSignalRO, ':COM:R3_CEIL')
    trans_floor = Cmp(EpicsSignalRO, ':COM:R3_FLOOR')
    user_energy = Cmp(EpicsSignal, ':COM:E3DES')
Esempio n. 12
0
class DelayMacro(CalibMotor, DelayTowerMacro):
    """
    Macro-motor for the delay macro-motor.
    """
    # @property
    # def aligned(self, rtol=0, atol=0.001):
    #     """
    #     Checks to see if the towers are in aligned in energy and delay.

    #     Parameters
    #     ----------
    #     rtol : float, optional
    #         Relative tolerance to use when comparing value differences.

    #     atol : float, optional
    #         Absolute tolerance to use when comparing value differences.

    #     Returns
    #     -------
    #     is_aligned : bool
    #         True if the towers are aligned, False if not.
    #     """
    #     t1_delay = self._length_to_delay(self.parent.t1.length)
    #     t4_delay = self._length_to_delay(self.parent.t4.length)
    #     is_aligned = np.isclose(t1_delay, t4_delay, atol=atol, rtol=rtol)
    #     if not is_aligned:
    #         logger.warning("Delay mismatch between t1 and t4. t1: {0:.3f}ps, "
    #                        "t4: {1:.3f}ps".format(t1_delay, t4_delay))
    #     return is_aligned

    calib_detector = Cmp(PCDSDetector, 'XCS:USR:O1000:01', add_prefix=[])

    def __init__(self, prefix, name=None, *args, **kwargs):
        super().__init__(prefix, name=name, *args, **kwargs)
        if self.parent:
            self.motor_fields = ['readback']
            self.calib_motors = [self.parent.t1.chi1, self.parent.t1.y1]
            self.calib_fields = [
                field_prepend('user_readback', calib_motor)
                for calib_motor in self.calib_motors
            ]
            self.detector_fields = [
                'stats2_centroid_x',
                'stats2_centroid_y',
            ]

    def _length_to_delay(self, L=None, theta1=None, theta2=None):
        """
        Converts the inputted L of the delay stage, theta1 and theta2 to
        the expected delay of the system, or uses the current positions
        as inputs.

        Parameters
        ----------
        L : float or None, optional
            Position of the linear delay stage.
        
        theta1 : float or None, optional
            Bragg angle the delay line is set to maximize.

        theta2 : float or None, optional
            Bragg angle the channel cut line is set to maximize.

        Returns
        -------
        delay : float
            The delay of the system in picoseconds.
        """
        # Check if any other inputs were used
        L = L or self.parent.t1.length
        theta1 = theta1 or self.parent.theta1
        theta2 = theta2 or self.parent.theta2

        # Delay calculation
        delay = (2 * (L * (1 - cosd(2 * theta1)) - self.gap *
                      (1 - cosd(2 * theta2)) / sind(theta2)) / self.c)
        return delay

    def _verify_move(self,
                     delay,
                     string="",
                     use_header=True,
                     confirm_move=True,
                     use_diag=True):
        """
        Prints a summary of the current positions and the proposed positions
        of the delay motors based on the inputs. It then prompts the user to 
        confirm the move.
        
        Parameters
        ----------
        delay : float
            Desired delay of the system.

        string : str, optional
            Message to be printed as a prompt.

        use_header : bool, optional
            Adds a basic header to the message.

        confirm_move : bool, optional
            Prompts the user for confirmation.

        use_diag : bool, optional
            Add the diagnostic motor to the list of motors to verify.
        
        Returns
        -------
        allowed : bool
            True if the move is approved, False if the move is not.
        """
        # Add a header to the output
        if use_header:
            string += self._add_verify_header(string)

        # Convert to length and add the body of the string
        length = self._delay_to_length(delay)
        for tower in self._delay_towers:
            string += "\n{:<15}|{:^15.4f}|{:^15.4f}".format(
                tower.L.desc, tower.length, length)

        # Add the diagnostic move
        if use_diag:
            position_dd = self._get_delay_diagnostic_position(delay=delay)
            string += "\n{:<15}|{:^15.4f}|{:^15.4f}".format(
                self.parent.dd.x.desc, self.parent.dd.x.position, position_dd)

        # Prompt the user for a confirmation or return the string
        if confirm_move is True:
            return self._confirm_move(string)
        else:
            return string

    def _check_towers_and_diagnostics(self, delay, use_diag=True):
        """
        Checks the staus of the delay stages on the delay towers. Raises the 
        basic motor errors if any of the motors are not ready to be moved.
        
        Parameters
        ----------
        delay : float
            The desired delay of the system.

        use_diag : bool, optional
            Check the position of the diagnostic motor.
        
        Raises
        ------
        LimitError
            Error raised when the inputted position is beyond the soft limits.
        
        MotorDisabled
            Error raised if the motor is disabled and move is requested.

        MotorFaulted
            Error raised if the motor is disabled and the move is requested.

        MotorStopped
            Error raised If the motor is stopped and a move is requested.

        BadN2Pressure
            Error raised if the pressure in the tower is bad.

        Returns
        -------
        position_dd : list
            Position to move the delay diagnostic to.
        """
        # Get the desired length for the delay stage
        length = self._delay_to_length(delay)

        # Check each of the delay towers
        for tower in self._delay_towers:
            tower.check_status(length=length)

        if use_diag:
            # Check the delay diagnostic position
            position_dd = self._get_delay_diagnostic_position(delay=delay)
            self.parent.dd.x.check_status(position_dd)
            return position_dd

    def _move_towers_and_diagnostics(self, delay, position_dd, use_diag=True):
        """
        Moves the delay stages and delay diagnostic according to the inputted
        delay and diagnostic position.
        
        Parameters
        ----------
        delay  : float
            Delay to set the system to.

        position_dd : float
            Position to move the delay diagnostic to.

        use_diag : bool, optional
            Move the daignostic motors to align with the beam.
        
        Returns
        -------
        status : list
            Nested list of status objects from each tower.
        """
        # Get the desired length for the delay stage
        length = self._delay_to_length(delay)

        # Move the delay stages
        status = [
            tower.set_length(length, wait=False, check_status=False)
            for tower in self._delay_towers
        ]
        # Log the delay change
        logger.debug("Setting delay to {0}.".format(delay))

        if use_diag:
            # Move the delay diagnostic to the inputted position
            status += [self.parent.dd.x.move(position_dd, wait=False)]

        # Perform the compensation
        if self.has_calib and self.use_calib:
            status.append(self._calib_compensate(delay))

        return status

    @property
    @nan_if_no_parent
    def position(self):
        """
        Returns the current energy of the channel cut line.
        
        Returns
        -------
        energy : float
            Energy the channel cut line is set to in eV.
        """
        return self._length_to_delay()

    def set_position(self,
                     delay=None,
                     print_set=True,
                     use_diag=True,
                     verify_move=True):
        """
        Sets the current positions of the motors in the towers to be the 
        calculated positions based on the inputted energies or delay.
        
        Parameters
        ----------
        delay : float or None, optional
            Delay to set the delay stages to.
        
        print_set : bool, optional
            Print a message to the console that the set has been made.

        verify_move : bool, optional
            Prints the current system state and a proposed system state and
            then prompts the user to accept the proposal before changing the
            system.
        """
        # Prompt the user about the move before making it
        if verify_move and self._verify_move(delay, use_diag=use_diag):
            return

        length = self._delay_to_length(delay)

        # Set the position of each delay stage
        for tower in self._delay_towers:
            tower.L.set_position(length, print_set=False)

        # Diagnostic
        if use_diag:
            position_dd = self._get_delay_diagnostic_position(delay=delay)
            self.parent.dd.x.set_position(position_dd, print_set=False)

        if print_set:
            logger.info("Setting positions for delay to {0}.".format(delay))
Esempio n. 13
0
class MacroBase(SndMotor):
    """
    Base pseudo-motor class for the SnD macro-motions.
    """
    # Constants
    c = 0.299792458  # mm/ps
    gap = 55  # m

    # Set add_prefix to be blank so cmp doesnt append the parent prefix
    readback = Cmp(AttributeSignal, "position", add_prefix='')

    def __init__(self, prefix, name=None, read_attrs=None, *args, **kwargs):
        read_attrs = read_attrs or ["readback"]
        super().__init__(prefix,
                         name=name,
                         read_attrs=read_attrs,
                         *args,
                         **kwargs)

        # Make sure this is used
        if not self.parent:
            logger.warning("Macromotors must be instantiated with a parent "
                           "that has the SnD towers as components to function "
                           "properly.")
        else:
            self._delay_towers = [self.parent.t1, self.parent.t4]
            self._channelcut_towers = [self.parent.t2, self.parent.t3]

    @property
    @nan_if_no_parent
    def position(self):
        """
        Returns the current position
        
        Returns
        -------
        energy : float
            Energy the channel cut line is set to in eV.
        """
        return (self.parent.t1.energy, self.parent.t2.energy,
                self._length_to_delay())

    def _verify_move(self, *args, **kwargs):
        """
        Prints a summary of the current positions and the proposed positions
        of the motors based on the inputs. It then prompts the user to confirm
        the move. To be overrided in subclasses.
        """
        pass

    def _check_towers_and_diagnostics(self, *args, **kwargs):
        """
        Checks the towers in the delay line and the channel cut line to make 
        sure they can be moved. Depending on if E1, E2 or delay are entered, 
        the delay line energy motors, channel cut line energy motors or the 
        delay stages will be checked for faults, if they are enabled, if the
        requested energy requires moving beyond the limits and if the pressure
        in the tower N2 is good. To be overrided in subclasses.
        """
        pass

    def _move_towers_and_diagnostics(self, *args, **kwargs):
        """
        Moves all the tower and diagnostic motors according to the inputted
        energies and delay. If any of the inputted information is set to None
        then that component of the move is ignored. To be overrided in 
        subclasses.
        """
        pass

    def _add_verify_header(self, string=""):
        """
        Adds the header that labels the motor desc, current position and propsed
        position.

        Parameters
        ----------
        string : str
            String to add a header to.

        Returns
        -------
        string : str
            String with the header added to it.
        """
        header = "\n{:^15}|{:^15}|{:^15}".format("Motor", "Current",
                                                 "Proposed")
        header += "\n" + "-" * len(header)
        return string + header

    def wait(self, status=None):
        """
        Waits for the status objects to complete the motions.
        
        Parameters
        ----------
        status : list or None, optional
            List of status objects to wait on. If None, will wait on the
            internal status list.
        """
        status = status or self._status
        logger.info("Waiting for the motors to finish moving...")
        for s in list(status):
            status_wait(s)
        logger.info("Move completed.")

    def set(self,
            position,
            wait=True,
            verify_move=True,
            ret_status=True,
            use_diag=True,
            use_calib=True):
        """
        Moves the macro-motor to the inputted position, optionally waiting for
        the motors to complete their moves.

        Parameters
        ----------
        position : float
            Position to move the macro-motor to.
        
        wait : bool, optional
            Wait for each motor to complete the motion before returning the
            console.
            
        verify_move : bool, optional
            Prints the current system state and a proposed system state and
            then prompts the user to accept the proposal before changing the
            system.

        ret_status : bool, optional
            Return the status object of the move.

        use_diag : bool, optional
            Move the daignostic motors to align with the beam.

        use_calib : bool, optional
            Use the configurated calibration parameters
        
        Returns
        -------
        status : list
            List of status objects for each motor that was involved in the move.
        """
        return self.move(position,
                         wait=wait,
                         verify_move=verify_move,
                         ret_status=ret_status,
                         use_diag=use_diag,
                         use_calib=use_calib)

    def move(self, position, wait=True, verify_move=True, use_diag=True):
        """
        Moves the macro-motor to the inputted position, optionally waiting for
        the motors to complete their moves. Alias for set().

        Parameters
        ----------
        position : float
            Position to move the macro-motor to.
        
        wait : bool, optional
            Wait for each motor to complete the motion before returning the
            console.
            
        verify_move : bool, optional
            Prints the current system state and a proposed system state and
            then prompts the user to accept the proposal before changing the
            system.

        ret_status : bool, optional
            Return the status object of the move.
        
        use_diag : bool, optional
            Move the daignostic motors to align with the beam.

        use_calib : bool, optional
            Use the configurated calibration parameters
        
        Returns
        -------
        status : list
            List of status objects for each motor that was involved in the move.
        """
        # Check the towers and diagnostics
        diag_pos = self._check_towers_and_diagnostics(position,
                                                      use_diag=use_diag)

        # Prompt the user about the move before making it
        if verify_move and self._verify_move(position, use_diag=use_diag):
            return

        # Send the move commands to all the motors
        status_list = flatten(
            self._move_towers_and_diagnostics(position,
                                              diag_pos,
                                              use_diag=use_diag))

        # Aggregate the status objects
        status = reduce(lambda x, y: x & y, status_list)

        # Wait for all the motors to finish moving
        if wait:
            self.wait(status)

        return status

    def mv(self,
           position,
           wait=True,
           verify_move=True,
           use_diag=True,
           *args,
           **kwargs):
        """
        Moves the macro parameters to the inputted positions. For energy, this 
        moves the energies of both lines, energy1 moves just the delay line, 
        energy2 moves just the channel cut line, and delay moves just the delay 
        motors.
        
        mv() is different from move() by catching all the common exceptions that
        this motor can raise and just raises a logger warning. Therefore if 
        building higher level functionality, do not use this method and use 
        move() instead, otherwise none of the exceptions will propagate beyond
        this method.

        Parameters
        ----------
        position : float
            Position to move the motor to.

        wait : bool, optional
            Wait for each motor to complete the motion before returning the
            console.
            
        verify_move : bool, optional
            Prints the current system state and a proposed system state and
            then prompts the user to accept the proposal before changing the
            system.

        ret_status : bool, optional
            Return the status object of the move.

        use_diag : bool, optional
            Move the daignostic motors to align with the beam.
        
        Exceptions Caught
        -----------------
        LimitError
            Error raised when the inputted position is beyond the soft limits.
        
        MotorDisabled
            Error raised if the motor is disabled and move is requested.

        MotorFaulted
            Error raised if the motor is disabled and the move is requested.

        MotorStopped
            Error raised If the motor is stopped and a move is requested.

        BadN2Pressure
            Error raised if the pressure in the tower is bad.

        Returns
        -------
        status : list
            List of status objects for each motor that was involved in the move.
        """
        try:
            return super().mv(position,
                              wait=wait,
                              verify_move=verify_move,
                              use_diag=use_diag,
                              *args,
                              **kwargs)
        # Catch all the common motor exceptions
        except LimitError:
            logger.warning("Requested move is outside the soft limits")
        except MotorDisabled:
            logger.warning(
                "Cannot move '{0}' - a motor is currently disabled. "
                "Try running 'motor.enable()'.".format(self.desc))
        except MotorFaulted:
            logger.warning("Cannot move '{0}' - a motor is currently faulted. "
                           "Try running 'motor.clear()'.".format(self.desc))
        except MotorStopped:
            logger.warning("Cannot move '{0}' - a motor is currently stopped. "
                           "Try running 'motor.state='Go''.".format(self.desc))
        except BadN2Pressure:
            logger.warning("Cannot move '{0}' - pressure in a tower is bad."
                           "".format(self.desc))

    def set_position(self, *args, **kwargs):
        """
        Sets the current positions of the motors in the towers to be the 
        calculated positions based on the inputted energies or delay. To be 
        overrided in subclasses.
        """
        pass

    @property
    def aligned(self, *args, **kwargs):
        """
        Checks to see if the towers are in aligned in energy and delay. To be 
        overrided in subclasses.
        """
        pass

    def _confirm_move(self, string):
        """
        Performs a basic confirmation of the move.

        Parameters
        ----------
        string : str
            Message to be printed to user.
        """
        logger.info(string)
        try:
            response = input("\nConfirm Move [y/n]: ")
        except Exception as e:
            logger.info("Exception raised: {0}".format(e))
            response = "n"

        if response.lower() != "y":
            logger.info("\nMove cancelled.")
            return True
        else:
            logger.debug("\nMove confirmed.")
            return False

    def status(self, status="", offset=0, print_status=True, newline=False):
        """
        Returns the status of the device.
        
        Parameters
        ----------
        status : str, optional
            The string to append the status to.
            
        offset : int, optional
            Amount to offset each line of the status.

        print_status : bool, optional
            Determines whether the string is printed or returned.
        """
        try:
            status += "\n{0}{1:<16} {2:^16}".format(" " * offset,
                                                    self.desc + ":",
                                                    self.position)
        except TypeError:
            status += "\n{0}{1:<16} {2:^}".format(" " * offset,
                                                  self.desc + ":",
                                                  str(self.position))

        if newline:
            status += "\n"
        if print_status:
            logger.info(status)
        else:
            return status
Esempio n. 14
0
class IntState(StatePositioner):
    state = Cmp(PrefixSignal, 'int', value=2)
    states_list = [None, 'UNO', 'OUT']
    _states_alias = {'UNO': ['IN', 'in']}
Esempio n. 15
0
            if hasattr(comp, "_plugin_type"):
                comp.plugin_type = Cmp(Signal, value=comp._plugin_type)
            else:
                for name in comp.component_names:
                    sub_comp = getattr(comp, name)
                    if type(sub_comp) is Cmp:
                        sub_comp = change_all_plugin_types(sub_comp.cls)
        return comp
    detector = change_all_plugin_types(detector)
    detector = make_fake_device(detector)
    return detector(name, name=name)

# Hotfix area detector plugins for tests
for comp in (PCDSDetector.image, PCDSDetector.stats):
    plugin_class = comp.cls
    plugin_class.plugin_type = Cmp(Signal, value=plugin_class._plugin_type)

# Hotfix make_fake_device for ophyd=1.2.0
fake_device_cache[EpicsSignalWithRBV] = FakeEpicsSignal

test_df_scan = pd.DataFrame(
    [[  -1,  0,  0,   -0.25,    0.25],
     [-0.5,  0,  0,  -0.125,   0.125],
     [   0,  0,  0,       0,       0],
     [ 0.5,  0,  0,   0.125,  -0.125],
     [   1,  0,  0,    0.25,   -0.25]],
     index = np.linspace(-1, 1, 5),
     columns = ["delay", "m1_pre", "m2_pre", "camera_centroid_x", 
                "camera_centroid_y"]
    )
Esempio n. 16
0
class AttBase(PVPositioner, FltMvInterface):
    """
    Base class for the attenuators.

    This is a device that puts an array of filters in or out to achieve a
    desired transmission ratio.

    This class does not include filters, because the number of filters can
    vary. You should not instantiate this class directly, but instead use the
    `Attenuator` factory function.
    """
    # Positioner Signals
    setpoint = Cmp(EpicsSignal, ':COM:R_DES')
    readback = Cmp(EpicsSignalRO, ':COM:R_CUR')
    actuate = Cmp(EpicsSignal, ':COM:GO')
    done = Cmp(EpicsSignalRO, ':COM:STATUS')

    # Attenuator Signals
    energy = Cmp(EpicsSignalRO, ':COM:T_CALC.VALE')
    trans_ceil = Cmp(EpicsSignalRO, ':COM:R_CEIL')
    trans_floor = Cmp(EpicsSignalRO, ':COM:R_FLOOR')
    user_energy = Cmp(EpicsSignal, ':COM:EDES')
    eget_cmd = Cmp(EpicsSignal, ':COM:EACT.SCAN')

    # Aux Signals
    calcpend = Cmp(EpicsSignalRO, ':COM:CALCP')

    egu = ''  # Transmission is a unitless ratio
    done_value = 0
    _default_read_attrs = ['readback']

    def __init__(self, prefix, *, name, **kwargs):
        super().__init__(prefix, name=name, limits=(0, 1), **kwargs)
        self.filters = []
        for i in range(1, MAX_FILTERS + 1):
            try:
                self.filters.append(getattr(self, 'filter{}'.format(i)))
            except AttributeError:
                break

    @property
    def actuate_value(self):
        """
        Sets the value we use in the GO command. This command will return 3 if
        the setpoint is closer to the ceiling than the floor, or 2 otherwise.
        In the unlikely event of a tie, we choose the floor.

        This will wait until a pending calculation completes before returning.
        """
        timeout = 1
        start = time.time()
        while self.calcpend.get() != 0:
            if time.time() - start > timeout:
                break
            time.sleep(0.01)

        goal = self.setpoint.get()
        ceil = self.trans_ceil.get()
        floor = self.trans_floor.get()
        if abs(goal - ceil) > abs(goal - floor):
            return 2
        else:
            return 3

    def set_energy(self, energy=None):
        """
        Sets the energy to use for transmission calculations.

        Parameters
        ----------
        energy: ``number``, optional
            If provided, this is the energy we'll use for the transmission
            calcluations. If omitted, we'll clear any set energy and use the
            current beam energy instead.
        """
        if energy is None:
            logger.debug('Setting %s to use live energy', self.name or self)
            self.eget_cmd.put(6)
        else:
            logger.debug('Setting %s to use energy=%s', self.name, energy)
            self.eget_cmd.put(0, use_complete=True)
            self.user_energy.put(energy)

    @property
    def transmission(self):
        """
        Ratio of pass-through beam to incoming beam. This is a value between
        1 (full beam) and 0 (no beam).
        """
        return self.position

    @property
    def inserted(self):
        """
        ``True`` if any blade is inserted
        """
        return self.position < 1

    @property
    def removed(self):
        """
        ``True`` if all blades are removed
        """
        return self.position == 1

    def insert(self, wait=False, timeout=None, moved_cb=None):
        """
        Block the beam by setting transmission to zero
        """
        return self.move(0, wait=wait, timeout=timeout, moved_cb=moved_cb)

    def remove(self, wait=False, timeout=None, moved_cb=None):
        """
        Bring the attenuator fully out of the beam
        """
        return self.move(1, wait=wait, timeout=timeout, moved_cb=moved_cb)

    def stage(self):
        """
        Store the original positions of all filter blades.

        This is a ``bluesky`` method called to set up the device for a scan.
        At the end of the scan, ``unstage`` should be called to restore the
        original positions of the filter blades.

        This is better then storing and restoring the transmission because the
        mechanical state associated with a particular transmission changes with
        the beam energy.
        """
        for filt in self.filters:
            # If state is invalid, try to remove at end
            if filt.position in filt._invalid_states:
                self._original_vals[filt.state] = filt.out_states[0]
            # Otherwise, remember so we can restore
            else:
                self._original_vals[filt.state] = filt.state.value
        return super().stage()

    def _setup_move(self, position):
        """
        If we're at a destination, short-circuit the done.

        This was needed because the status pv in the attenuator IOC does not
        react if we request a move to a transmission we've already reached.
        Therefore, this prevents a pointless timeout.
        """
        old_position = self.position
        super()._setup_move(position)
        ceil = self.trans_ceil.get()
        floor = self.trans_floor.get()
        if any(np.isclose((old_position, old_position), (ceil, floor))):
            moving_val = 1 - self.done_value
            self._move_changed(value=moving_val)
            self._move_changed(value=self.done_value)