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
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)
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'}
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
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
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
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')
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
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()
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
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')
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))
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
class IntState(StatePositioner): state = Cmp(PrefixSignal, 'int', value=2) states_list = [None, 'UNO', 'OUT'] _states_alias = {'UNO': ['IN', 'in']}
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"] )
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)