class SampleNested(Device): yolk = Cpt(EpicsSignal, ':YOLK', string=True) whites = Cpt(EpicsSignalRO, ':WHITES')
class MyDetector(SingleTrigger, SimDetector): stats1 = Cpt(StatsPlugin, 'Stats1:') proc1 = Cpt(ProcessPlugin, 'Proc1:') roi1 = Cpt(ROIPlugin, 'ROI1:')
class MyDetector(SingleTrigger, SimDetector): tiff1 = Cpt(FS_tiff, 'TIFF1:', write_path_template=wpath, read_path_template=rpath, root=root, reg=fs)
class MyDetector(SingleTrigger, SimDetector): roi1 = Cpt(ROIPlugin, 'ROI1:') over1 = Cpt(OverlayPlugin, 'Over1:')
class PluginGroup(Device): tiff1 = Cpt(TIFFPlugin, 'TIFF1:')
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
class MyDet(SimDetector): p = Cpt(HDF5Plugin, suffix='HDF1:')
class LXE(LaserEnergyPositioner): motor = Cpt(Newport, '')
class USBEncoder(Device): tab_component_names = True set_zero = Cpt(EpicsSignal, ':ZEROCNT') pos = Cpt(EpicsSignal, ':POSITION') scale = Cpt(EpicsSignal, ':SCALE') offset = Cpt(EpicsSignal, ':OFFSET')
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')
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:')
class DelayNewport(DelayBase): __doc__ = DelayBase.__doc__ motor = Cpt(Newport, '')
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")
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)
class MyDetector(SingleTrigger, SimDetector): tiff1 = Cpt(TIFFPlugin, 'TIFF1:')
class MMC(EpicsMotorInterface): direction_of_travel = Cpt(Signal, kind='omitted')
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
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)
class TestDet(SimDetector): p = Cpt(TIFFPlugin, 'TIFF1:')
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)
class MyDetector(SimDetector): tiff1 = Cpt(TIFFPlugin, 'TIFF1:')
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
class MyDetector(SingleTrigger, SimDetector): tiff1 = Cpt(TIFFPlugin, 'TIFF1:') stats1 = Cpt(StatsPlugin, 'Stats1:') roi1 = Cpt(ROIPlugin, 'ROI1:')
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)
class MyDetector(SingleTrigger, SimDetector): plugins = Cpt(PluginGroup, '') roi1 = Cpt(ROIPlugin, 'ROI1:') stats1 = Cpt(StatsPlugin, 'Stats1:')
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
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)
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)
class MyDetector(SingleTrigger, SimDetector): hdf1 = Cpt(FS_hdf, 'HDF1:', write_path_template=wpath, read_path_template=rpath, root=root, reg=fs)
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