class OceanOptics(Base, SpectrometerInterface): """ Hardware module for reading spectra from the Ocean Optics spectrometer software. Example config for copy-paste: myspectrometer: module.Class: 'spectrometer.oceanoptics_spectrometer.OceanOptics' spectrometer_serial: 'QEP01583' #insert here the right serial number. """ _serial = ConfigOption('spectrometer_serial', missing='warn') _integration_time = StatusVar('integration_time', default=10000) def on_activate(self): """ Activate module. """ self.spec = sb.Spectrometer.from_serial_number(self._serial) self.log.info(''.format(self.spec.model, self.spec.serial_number)) self.spec.integration_time_micros(self._integration_time) self.log.info('Exposure set to {} microseconds'.format( self._integration_time)) def on_deactivate(self): """ Deactivate module. """ self.spec.close() def recordSpectrum(self): """ Record spectrum from Ocean Optics spectrometer. @return []: spectrum data """ wavelengths = self.spec.wavelengths() specdata = np.empty((2, len(wavelengths)), dtype=np.double) specdata[0] = wavelengths / 1e9 specdata[1] = self.spec.intensities() return specdata def getExposure(self): """ Get exposure. @return float: exposure Not implemented. """ return self._integration_time def setExposure(self, exposureTime): """ Set exposure. @param float exposureTime: exposure time in microseconds """ self._integration_time = exposureTime self.spec.integration_time_micros(self._integration_time)
class PoiManagerLogic(GenericLogic): """ This is the Logic class for mapping and tracking bright features in the confocal scan. """ _modclass = 'poimanagerlogic' _modtype = 'logic' # declare connectors optimiserlogic = Connector(interface='OptimizerLogic') scannerlogic = Connector(interface='ConfocalLogic') savelogic = Connector(interface='SaveLogic') # status vars _roi = StatusVar(default=dict()) # Notice constructor and representer further below _refocus_period = StatusVar(default=120) _active_poi = StatusVar(default=None) _move_scanner_after_optimization = StatusVar(default=True) # Signals for connecting modules sigRefocusStateUpdated = QtCore.Signal(bool) # is_active sigRefocusTimerUpdated = QtCore.Signal(bool, float, float) # is_active, period, remaining_time sigPoiUpdated = QtCore.Signal(str, str, np.ndarray) # old_name, new_name, current_position sigActivePoiUpdated = QtCore.Signal(str) sigRoiUpdated = QtCore.Signal(dict) # Dict containing ROI parameters to update # Internal signals __sigStartPeriodicRefocus = QtCore.Signal() __sigStopPeriodicRefocus = QtCore.Signal() def __init__(self, config, **kwargs): super().__init__(config=config, **kwargs) # timer for the periodic refocus self.__timer = None self._last_refocus = 0 self._periodic_refocus_poi = None # threading self._threadlock = Mutex() return def on_activate(self): """ Initialisation performed during activation of the module. """ self.__timer = QtCore.QTimer() self.__timer.setSingleShot(False) self._last_refocus = 0 self._periodic_refocus_poi = None # Connect callback for a finished refocus self.optimiserlogic().sigRefocusFinished.connect( self._optimisation_callback, QtCore.Qt.QueuedConnection) # Connect internal start/stop signals to decouple QTimer from other threads self.__sigStartPeriodicRefocus.connect( self.start_periodic_refocus, QtCore.Qt.QueuedConnection) self.__sigStopPeriodicRefocus.connect( self.stop_periodic_refocus, QtCore.Qt.QueuedConnection) # Initialise the ROI scan image (xy confocal image) if not present if self._roi.scan_image is None: self.set_scan_image(False) self.sigRoiUpdated.emit({'name': self.roi_name, 'poi_nametag': self.poi_nametag, 'pois': self.poi_positions, 'history': self.roi_pos_history, 'scan_image': self.roi_scan_image, 'scan_image_extent': self.roi_scan_image_extent}) self.sigActivePoiUpdated.emit('' if self.active_poi is None else self.active_poi) self.update_poi_tag_in_savelogic() return def on_deactivate(self): # Stop active processes/loops self.stop_periodic_refocus() # Disconnect signals self.optimiserlogic().sigRefocusFinished.disconnect() self.__sigStartPeriodicRefocus.disconnect() self.__sigStopPeriodicRefocus.disconnect() return @property def data_directory(self): return self.savelogic().data_dir @property def optimise_xy_size(self): return float(self.optimiserlogic().refocus_XY_size) @property def active_poi(self): return self._active_poi @active_poi.setter def active_poi(self, name): self.set_active_poi(name) return @property def poi_names(self): return self._roi.poi_names @property def poi_positions(self): return self._roi.poi_positions @property def poi_anchors(self): return self._roi.poi_anchors @property def roi_name(self): return self._roi.name @roi_name.setter def roi_name(self, name): self.rename_roi(new_name=name) @property def poi_nametag(self): return self._roi.poi_nametag @poi_nametag.setter def poi_nametag(self, tag): self.set_poi_nametag(tag) return @property def roi_origin(self): return self._roi.origin @property def roi_creation_time(self): return self._roi.creation_time @property def roi_creation_time_as_str(self): return self._roi.creation_time_as_str @property def roi_pos_history(self): return self._roi.pos_history @property def roi_scan_image(self): return self._roi.scan_image @property def roi_scan_image_extent(self): return self._roi.scan_image_extent @property def refocus_period(self): return float(self._refocus_period) @refocus_period.setter def refocus_period(self, period): self.set_refocus_period(period) return @property def time_until_refocus(self): if not self.__timer.isActive(): return -1 return max(0., self._refocus_period - (time.time() - self._last_refocus)) @property def scanner_position(self): return self.scannerlogic().get_position()[:3] @property def move_scanner_after_optimise(self): return bool(self._move_scanner_after_optimization) @move_scanner_after_optimise.setter def move_scanner_after_optimise(self, move): self.set_move_scanner_after_optimise(move) return @QtCore.Slot(int) @QtCore.Slot(bool) def set_move_scanner_after_optimise(self, move): with self._threadlock: self._move_scanner_after_optimization = bool(move) return @QtCore.Slot(str) def set_poi_nametag(self, tag): if tag is None or isinstance(tag, str): if tag == '': tag = None self._roi.poi_nametag = tag self.sigRoiUpdated.emit({'poi_nametag': self.poi_nametag}) else: self.log.error('POI name tag must be str or None.') return @QtCore.Slot() @QtCore.Slot(np.ndarray) def add_poi(self, position=None, name=None, emit_change=True): """ Creates a new POI and adds it to the current ROI. POI can be optionally initialized with position and name. @param str name: Name for the POI (must be unique within ROI). None (default) will create generic name. @param scalar[3] position: Iterable of length 3 representing the (x, y, z) position with respect to the ROI origin. None (default) causes the current scanner crosshair position to be used. @param bool emit_change: Flag indicating if the changed POI set should be signaled. """ # Get current scanner position from scannerlogic if no position is provided. if position is None: position = self.scanner_position current_poi_set = set(self.poi_names) # Add POI to current ROI self._roi.add_poi(position=position, name=name) # Get newly added POI name from comparing POI names before and after addition of new POI poi_name = set(self.poi_names).difference(current_poi_set).pop() # Notify about a changed set of POIs if necessary if emit_change: self.sigPoiUpdated.emit('', poi_name, self.get_poi_position(poi_name)) # Set newly created POI as active poi self.set_active_poi(poi_name) return @QtCore.Slot() def delete_poi(self, name=None): """ Deletes the given poi from the ROI. @param str name: Name of the POI to delete. If None (default) delete active POI. @param bool emit_change: Flag indicating if the changed POI set should be signaled. """ if len(self.poi_names) == 0: self.log.warning('Can not delete POI. No POI present in ROI.') return if name is None: if self.active_poi is None: self.log.error('No POI name to delete and no active POI set.') return else: name = self.active_poi self._roi.delete_poi(name) if self.active_poi == name: if len(self.poi_names) > 0: self.set_active_poi(self.poi_names[0]) else: self.set_active_poi(None) # Notify about a changed set of POIs if necessary self.sigPoiUpdated.emit(name, '', np.zeros(3)) return @QtCore.Slot(str) @QtCore.Slot(str, str) def rename_poi(self, new_name, name=None): """ @param str name: @param str new_name: """ if not isinstance(new_name, str) or not new_name: self.log.error('POI name to set must be str of length > 0.') return if name is None: if self.active_poi is None: self.log.error('Unable to rename POI. No POI name given and no active POI set.') return else: name = self.active_poi self._roi.rename_poi(name=name, new_name=new_name) self.sigPoiUpdated.emit(name, new_name, self.get_poi_position(new_name)) if self.active_poi == name: self.set_active_poi(new_name) return @QtCore.Slot(str) def set_active_poi(self, name=None): """ Set the name of the currently active POI @param name: """ if not isinstance(name, str) and name is not None: self.log.error('POI name must be of type str or None.') elif name is None or name == '': self._active_poi = None elif name in self.poi_names: self._active_poi = str(name) else: self.log.error('No POI with name "{0}" found in POI list.'.format(name)) self.sigActivePoiUpdated.emit('' if self.active_poi is None else self.active_poi) self.update_poi_tag_in_savelogic() return def get_poi_position(self, name=None): """ Returns the POI position of the specified POI or the active POI if none is given. @param str name: Name of the POI to return the position for. If None (default) the active POI position is returned. @return float[3]: Coordinates of the desired POI (x,y,z) """ if name is None: name = self.active_poi return self._roi.get_poi_position(name) def get_poi_anchor(self, name=None): """ Returns the POI anchor position (excluding sample movement) of the specified POI or the active POI if none is given. @param str name: Name of the POI to return the position for. If None (default) the active POI position is returned. @return float[3]: Coordinates of the desired POI anchor (x,y,z) """ if name is None: name = self.active_poi return self._roi.get_poi_anchor(name) @QtCore.Slot() def move_roi_from_poi_position(self, name=None, position=None): if position is None: position = self.scanner_position if name is None: if self.active_poi is None: self.log.error('Unable to set POI position. ' 'No POI name given and no active POI set.') return else: name = self.active_poi if len(position) != 3: self.log.error('POI position must be iterable of length 3.') return if not isinstance(name, str): self.log.error('POI name must be of type str.') shift = position - self.get_poi_position(name) self.add_roi_position(self.roi_origin + shift) return @QtCore.Slot() def set_poi_anchor_from_position(self, name=None, position=None): if position is None: position = self.scanner_position if name is None: if self.active_poi is None: self.log.error('Unable to set POI position. ' 'No POI name given and no active POI set.') return else: name = self.active_poi if len(position) != 3: self.log.error('POI position must be iterable of length 3.') return if not isinstance(name, str): self.log.error('POI name must be of type str.') shift = position - self.get_poi_position(name) self._roi.set_poi_anchor(name, self.get_poi_anchor(name) + shift) self.sigPoiUpdated.emit(name, name, self.get_poi_position(name)) return @QtCore.Slot(str) def rename_roi(self, new_name): if not isinstance(new_name, str) or new_name == '': self.log.error('ROI name to set must be str of length > 0.') return self._roi.name = new_name self.sigRoiUpdated.emit({'name': self.roi_name}) return @QtCore.Slot(np.ndarray) def add_roi_position(self, position): self._roi.add_history_entry(position) self.sigRoiUpdated.emit({'pois': self.poi_positions, 'history': self.roi_pos_history, 'scan_image': self.roi_scan_image, 'scan_image_extent': self.roi_scan_image_extent}) return @QtCore.Slot() @QtCore.Slot(int) def delete_history_entry(self, history_index=-1): """ Delete an entry in the ROI history. Deletes the last position by default. @param int|slice history_index: List index for history entry """ old_roi_origin = self.roi_origin self._roi.delete_history_entry(history_index) if np.any(old_roi_origin != self.roi_origin): self.sigRoiUpdated.emit({'pois': self.poi_positions, 'history': self.roi_pos_history, 'scan_image': self.roi_scan_image, 'scan_image_extent': self.roi_scan_image_extent}) else: self.sigRoiUpdated.emit({'history': self.roi_pos_history}) return @QtCore.Slot() def go_to_poi(self, name=None): """ Move crosshair to the given poi. @param str name: the name of the POI """ if name is None: name = self.active_poi if not isinstance(name, str): self.log.error('POI name to move to must be of type str.') return self.move_scanner(self.get_poi_position(name)) return def move_scanner(self, position): if len(position) != 3: self.log.error('Scanner position to set must be iterable of length 3.') return self.scannerlogic().set_position('poimanager', x=position[0], y=position[1], z=position[2]) return @QtCore.Slot() def set_scan_image(self, emit_change=True): """ Get the current xy scan data and set as scan_image of ROI. """ self._roi.set_scan_image( self.scannerlogic().xy_image[:, :, 3], (tuple(self.scannerlogic().image_x_range), tuple(self.scannerlogic().image_y_range))) if emit_change: self.sigRoiUpdated.emit({'scan_image': self.roi_scan_image, 'scan_image_extent': self.roi_scan_image_extent}) return @QtCore.Slot() def reset_roi(self): self.stop_periodic_refocus() self._roi = RegionOfInterest() self.set_scan_image(False) self.sigRoiUpdated.emit({'name': self.roi_name, 'poi_nametag': self.poi_nametag, 'pois': self.poi_positions, 'history': self.roi_pos_history, 'scan_image': self.roi_scan_image, 'scan_image_extent': self.roi_scan_image_extent}) self.set_active_poi(None) return @QtCore.Slot(int) @QtCore.Slot(float) def set_refocus_period(self, period): """ Change the duration of the periodic optimise timer during active periodic refocusing. @param float period: The time between optimisation procedures. """ if period < 0: self.log.error('Refocus period must be a value > 0. Unable to set period of "{0}".' ''.format(period)) return # Acquire thread lock in order to change the period during a running periodic refocus with self._threadlock: self._refocus_period = float(period) if self.__timer.isActive(): self.sigRefocusTimerUpdated.emit(True, self.refocus_period, self.time_until_refocus) else: self.sigRefocusTimerUpdated.emit(False, self.refocus_period, self.refocus_period) return def start_periodic_refocus(self, name=None): """ Starts periodic refocusing of the POI <name>. @param str name: The name of the POI to be refocused periodically. If None (default) perform periodic refocus on active POI. """ if name is None: if self.active_poi is None: self.log.error('Unable to start periodic refocus. No POI name given and no active ' 'POI set.') return else: name = self.active_poi if name not in self.poi_names: self.log.error('No POI with name "{0}" found in POI list.\n' 'Unable to start periodic refocus.') return with self._threadlock: if self.__timer.isActive(): self.log.error('Periodic refocus already running. Unable to start a new one.') return self.module_state.lock() self._periodic_refocus_poi = name self.optimise_poi_position(name=name) self._last_refocus = time.time() self.__timer.timeout.connect(self._periodic_refocus_loop) self.__timer.start(500) self.sigRefocusTimerUpdated.emit(True, self.refocus_period, self.refocus_period) return def stop_periodic_refocus(self): """ Stops the periodic refocusing of the POI. """ with self._threadlock: if self.__timer.isActive(): self.__timer.stop() self.__timer.timeout.disconnect() self._periodic_refocus_poi = None self.module_state.unlock() self.sigRefocusTimerUpdated.emit(False, self.refocus_period, self.refocus_period) return @QtCore.Slot(bool) def toggle_periodic_refocus(self, switch_on): """ @param switch_on: """ if switch_on: self.__sigStartPeriodicRefocus.emit() else: self.__sigStopPeriodicRefocus.emit() return @QtCore.Slot() def _periodic_refocus_loop(self): """ This is the looped function that does the actual periodic refocus. If the time has run out, it refocuses the current poi. Otherwise it just updates the time that is left. """ with self._threadlock: if self.__timer.isActive(): remaining_time = self.time_until_refocus self.sigRefocusTimerUpdated.emit(True, self.refocus_period, remaining_time) if remaining_time <= 0 and self.optimiserlogic().module_state() == 'idle': self.optimise_poi_position(self._periodic_refocus_poi) self._last_refocus = time.time() return @QtCore.Slot() def optimise_poi_position(self, name=None, update_roi_position=True): """ Triggers the optimisation procedure for the given poi using the optimiserlogic. The difference between old and new position can be used to update the ROI position. This function will return immediately. The function "_optimisation_callback" will handle the aftermath of the optimisation. @param str name: Name of the POI for which to optimise the position. @param bool update_roi_position: Flag indicating if the ROI should be shifted accordingly. """ if name is None: if self.active_poi is None: self.log.error('Unable to optimize POI position. ' 'No POI name given and not active POI set.') return else: name = self.active_poi if update_roi_position: tag = 'poimanagermoveroi_{0}'.format(name) else: tag = 'poimanager_{0}'.format(name) if self.optimiserlogic().module_state() == 'idle': self.optimiserlogic().start_refocus(initial_pos=self.get_poi_position(name), caller_tag=tag) self.sigRefocusStateUpdated.emit(True) else: self.log.warning('Unable to start POI refocus procedure. ' 'OptimizerLogic module is still locked.') return def _optimisation_callback(self, caller_tag, optimal_pos): """ Callback function for a finished position optimisation. If desired the relative shift of the optimised POI can be used to update the ROI position. The scanner is moved to the optimised POI if desired. @param caller_tag: @param optimal_pos: """ # If the refocus was initiated by poimanager, update POI and ROI position if caller_tag.startswith('poimanager_') or caller_tag.startswith('poimanagermoveroi_'): shift_roi = caller_tag.startswith('poimanagermoveroi_') poi_name = caller_tag.split('_', 1)[1] if poi_name in self.poi_names: # We only need x, y, z optimal_pos = np.array(optimal_pos[:3], dtype=float) if shift_roi: self.move_roi_from_poi_position(name=poi_name, position=optimal_pos) else: self.set_poi_anchor_from_position(name=poi_name, position=optimal_pos) if self._move_scanner_after_optimization: self.move_scanner(position=optimal_pos) self.sigRefocusStateUpdated.emit(False) return def update_poi_tag_in_savelogic(self): if not self._active_poi: self.savelogic().remove_additional_parameter('Active POI') else: self.savelogic().update_additional_parameters({'Active POI': self._active_poi}) def save_roi(self): """ Save all current absolute POI coordinates to a file. Save ROI history to a second file. Save ROI scan image (if present) to a third file (binary numpy .npy-format). """ # File path and names filepath = self.savelogic().get_path_for_module(module_name='ROIs') roi_name_no_blanks = self.roi_name.replace(' ', '_') timestamp = datetime.now() pois_filename = '{0}_poi_list'.format(roi_name_no_blanks) roi_history_filename = '{0}_{1}_history.npy'.format( timestamp.strftime('%Y%m%d-%H%M-%S'), roi_name_no_blanks) roi_image_filename = '{0}_{1}_scan_image.npy'.format( timestamp.strftime('%Y%m%d-%H%M-%S'), roi_name_no_blanks) # Metadata to save in both file headers x_extent, y_extent = self.roi_scan_image_extent parameters = OrderedDict() parameters['roi_name'] = self.roi_name parameters['poi_nametag'] = '' if self.poi_nametag is None else self.poi_nametag parameters['roi_creation_time'] = self.roi_creation_time_as_str parameters['scan_image_x_extent'] = '{0:.9e},{1:.9e}'.format(*x_extent) parameters['scan_image_y_extent'] = '{0:.9e},{1:.9e}'.format(*y_extent) ################################## # Save POI positions to first file ################################## poi_dict = self.poi_positions poi_positions = np.array(tuple(poi_dict.values()), dtype=float) data = OrderedDict() # Save POI names in the first column data['name'] = np.array(tuple(poi_dict), dtype=str) # Save x,y,z coordinates in the following 3 columns data['X (m)'] = poi_positions[:, 0] data['Y (m)'] = poi_positions[:, 1] data['Z (m)'] = poi_positions[:, 2] self.savelogic().save_data(data, timestamp=timestamp, filepath=filepath, parameters=parameters, filelabel=pois_filename, fmt=['%s', '%.6e', '%.6e', '%.6e']) ############################################ # Save ROI history to second file (binary) if present ############################################ if len(self.roi_pos_history) > 1: np.save(os.path.join(filepath, roi_history_filename), self.roi_pos_history) ####################################################### # Save ROI scan image to third file (binary) if present ####################################################### if self.roi_scan_image is not None: np.save(os.path.join(filepath, roi_image_filename), self.roi_scan_image) return def load_roi(self, complete_path=None): if complete_path is None: return filepath, filename = os.path.split(complete_path) # Try to detect legacy file format is_legacy_format = False if not complete_path.endswith('_poi_list.dat'): self.log.info('Trying to read ROI from legacy file format...') with open(complete_path, 'r') as file: for line in file.readlines(): if line.strip() == '#POI Name\tPOI Key\tX\tY\tZ': is_legacy_format = True elif not line.startswith('#'): break if not is_legacy_format: self.log.error('Unable to load ROI from file. File format not understood.') return if is_legacy_format: filetag = filename.split('_', 1)[1].rsplit('.dat', 1)[0] else: filetag = filename.rsplit('_poi_list.dat', 1)[0] # Read POI data as well as roi metadata from textfile poi_names = np.loadtxt(complete_path, delimiter='\t', usecols=0, dtype=str) if is_legacy_format: poi_coords = np.loadtxt(complete_path, delimiter='\t', usecols=(2, 3, 4), dtype=float) else: poi_coords = np.loadtxt(complete_path, delimiter='\t', usecols=(1, 2, 3), dtype=float) # Create list of POI instances poi_list = [PointOfInterest(pos, poi_names[i]) for i, pos in enumerate(poi_coords)] roi_name = None poi_nametag = None roi_creation_time = None scan_extent = None if is_legacy_format: roi_name = filetag else: with open(complete_path, 'r') as file: for line in file.readlines(): if not line.startswith('#'): break if line.startswith('#roi_name:'): roi_name = line.split('#roi_name:', 1)[1].strip() elif line.startswith('#poi_nametag:'): poi_nametag = line.split('#poi_nametag:', 1)[1].strip() elif line.startswith('#roi_creation_time:'): roi_creation_time = line.split('#roi_creation_time:', 1)[1].strip() elif line.startswith('#scan_image_x_extent:'): scan_x_extent = line.split('#scan_image_x_extent:', 1)[1].strip().split(',') elif line.startswith('#scan_image_y_extent:'): scan_y_extent = line.split('#scan_image_y_extent:', 1)[1].strip().split(',') scan_extent = ((float(scan_x_extent[0]), float(scan_x_extent[1])), (float(scan_y_extent[0]), float(scan_y_extent[1]))) poi_nametag = None if not poi_nametag else poi_nametag # Read ROI position history from binary file history_filename = os.path.join(filepath, '{0}_history.npy'.format(filetag)) try: roi_history = np.load(history_filename) except FileNotFoundError: roi_history = None # Read ROI scan image from binary file image_filename = os.path.join(filepath, '{0}_scan_image.npy'.format(filetag)) try: roi_scan_image = np.load(image_filename) except FileNotFoundError: roi_scan_image = None # Reset current ROI and initialize new one from loaded data self.reset_roi() self._roi = RegionOfInterest(name=roi_name, creation_time=roi_creation_time, history=roi_history, scan_image=roi_scan_image, scan_image_extent=scan_extent, poi_list=poi_list, poi_nametag=poi_nametag) print(poi_nametag, self.poi_nametag) self.sigRoiUpdated.emit({'name': self.roi_name, 'poi_nametag': self.poi_nametag, 'pois': self.poi_positions, 'history': self.roi_pos_history, 'scan_image': self.roi_scan_image, 'scan_image_extent': self.roi_scan_image_extent}) self.set_active_poi(None if len(poi_names) == 0 else poi_names[0]) return @_roi.constructor def dict_to_roi(self, roi_dict): return RegionOfInterest.from_dict(roi_dict) @_roi.representer def roi_to_dict(self, roi): return roi.to_dict() def transform_roi(self, transform_matrix): # TODO: Implement this if transform_matrix.shape != (3, 3): self.log.error('Tranformation matrix must be numpy array of shape (3, 3).') return self.log.error('Tranformation of all POI positions not implemented yet.') return
class MagnetControlLogic(GenericLogic): """This is the Logic class for ODMR.""" _modclass = 'magnetcontrollogic' _modtype = 'logic' # declare connectors fitlogic = Connector(interface='FitLogic') savelogic = Connector(interface='SaveLogic') magnetstage = Connector(interface='magnet_control_interface') counter = Connector(interface='CounterLogic') curr_x_pos = StatusVar('curr_x_pos', 0.0000) curr_y_pos = StatusVar('curr_y_pos', 0.0000) curr_z_pos = StatusVar('curr_z_pos', 0.0000) set_x_pos = StatusVar('set_x_pos', 0.0000) set_y_pos = StatusVar('set_y_pos', 0.0000) set_z_pos = StatusVar('set_z_pos', 0.0000) N_AF_points = StatusVar('N_AF_points', 10) x_start = StatusVar('x_start', 9.4600) x_end = StatusVar('x_end', 9.9600) step_x = StatusVar('step_x', 0.0300) n_x_points = StatusVar('n_x_points', 0.0) y_start = StatusVar('y_start', 9.4600) y_end = StatusVar('y_end', 9.9600) step_y = StatusVar('step_y', 0.0300) n_y_points = StatusVar('n_y_points', 0.0) Xmax = StatusVar('Xmax', 0.0000) Ymax = StatusVar('Ymax', 0.0000) x_scan_fit_x = StatusVar('x_scan_fit_x', 0.0000) x_scan_fit_y = StatusVar('x_scan_fit_x', 0.0000) y_scan_fit_x = StatusVar('x_scan_fit_x', 0.0000) y_scan_fit_y = StatusVar('x_scan_fit_x', 0.0000) fc = StatusVar('fits', None) i = StatusVar('i', 0) motion_time = StatusVar('motion_time', 0.0000) fluorescence_integration_time = StatusVar('fluorescence_integration_time', 0.5) # Update signals, e.g. for GUI module sigPlotXUpdated = QtCore.Signal(np.ndarray, np.ndarray) sigPlotYUpdated = QtCore.Signal(np.ndarray, np.ndarray) sigFitXUpdated = QtCore.Signal(np.ndarray, np.ndarray, dict, str) sigFitYUpdated = QtCore.Signal(np.ndarray, np.ndarray, dict, str) sigPositionUpdated = QtCore.Signal() sigNextXPoint = QtCore.Signal() sigNextYPoint = QtCore.Signal() signal_stop_scanning = QtCore.Signal() def __init__(self, config, **kwargs): super().__init__(config=config, **kwargs) self.threadlock = Mutex() def on_activate(self): """ Initialisation performed during activation of the module. """ # Get connectors self._magnetstage = self.get_connector('magnetstage') self._fit_logic = self.get_connector('fitlogic') self._counter = self.get_connector('counter') self._save_logic = self.get_connector('savelogic') # Set flags # for stopping a measurement self.stopRequested = False # Initalize the ODMR data arrays (mean signal and sweep matrix) self._initialize_plots() # Connect signals self.sigNextXPoint.connect(self._next_x_point, QtCore.Qt.QueuedConnection) self.sigNextYPoint.connect(self._next_y_point, QtCore.Qt.QueuedConnection) return def on_deactivate(self): """ Deinitialisation performed during deactivation of the module. """ return @fc.constructor def sv_set_fits(self, val): # Setup fit container fc = self.fitlogic().make_fit_container('length', '1d') fc.set_units(['mm', 'c/s']) if isinstance(val, dict) and len(val) > 0: fc.load_from_dict(val) else: d1 = OrderedDict() d1['Gaussian peak'] = { 'fit_function': 'gaussian', 'estimator': 'peak' } d1['Lorentzian peak'] = { 'fit_function': 'lorentzian', 'estimator': 'peak' } d1['Two Lorentzian dips'] = { 'fit_function': 'lorentziandouble', 'estimator': 'dip' } d1['N14'] = { 'fit_function': 'lorentziantriple', 'estimator': 'N14' } d1['N15'] = { 'fit_function': 'lorentziandouble', 'estimator': 'N15' } default_fits = OrderedDict() default_fits['1d'] = d1['Gaussian peak'] fc.load_from_dict(default_fits) return fc @fc.representer def sv_get_fits(self, val): """ save configured fits """ if len(val.fit_list) > 0: return val.save_to_dict() else: return None def _initialize_plots(self): """ Initializing the ODMR plots (line and matrix). """ self.fluor_plot_x = np.arange(self.x_start, self.x_end, self.step_x) self.fluor_plot_y = np.zeros(self.fluor_plot_x.size) self.x_scan_fit_x = np.arange(self.x_start, self.x_end, self.step_x) self.x_scan_fit_y = np.zeros(self.fluor_plot_x.size) self.yfluor_plot_x = np.arange(self.y_start, self.y_end, self.step_y) self.yfluor_plot_y = np.zeros(self.yfluor_plot_x.size) self.y_scan_fit_x = np.arange(self.y_start, self.y_end, self.step_y) self.y_scan_fit_y = np.zeros(self.yfluor_plot_x.size) self.sigPlotXUpdated.emit(self.fluor_plot_x, self.fluor_plot_y) self.sigPlotYUpdated.emit(self.yfluor_plot_x, self.yfluor_plot_y) current_x_fit = self.fc.current_fit self.sigFitXUpdated.emit(self.x_scan_fit_x, self.x_scan_fit_y, {}, current_x_fit) current_y_fit = self.fc.current_fit self.sigFitXUpdated.emit(self.y_scan_fit_x, self.y_scan_fit_y, {}, current_y_fit) return def get_current_position(self): try: self.curr_x_pos = float( self._magnetstage.get_current_position(1)[3:-2]) except pyvisa.errors.VisaIOError: print('visa error') time.sleep(0.05) try: self.curr_x_pos = float( self._magnetstage.get_current_position(1)[3:-2]) except pyvisa.errors.VisaIOError: print('visa error') time.sleep(0.05) self.curr_x_pos = float( self._magnetstage.get_current_position(1)[3:-2]) time.sleep(0.05) try: self.curr_y_pos = float( self._magnetstage.get_current_position(2)[3:-2]) except pyvisa.errors.VisaIOError: print('visa error') time.sleep(0.05) self.curr_y_pos = float( self._magnetstage.get_current_position(2)[3:-2]) self.curr_z_pos = float( self._magnetstage.get_current_position(3)[3:-2]) return def set_position(self): self._magnetstage.move_absolute(1, self.set_x_pos) self._magnetstage.move_absolute(2, self.set_y_pos) self._magnetstage.move_absolute(3, self.set_z_pos) return def start_x_scanning(self, tag='logic'): """Starts scanning """ with self.threadlock: if self.module_state() == 'locked': self.log.error( 'Can not start fluorescence scan. Logic is already locked.' ) return -1 self.module_state.lock() self.stopRequested = False self.step_x = int(self.step_x / 2e-4) * 2e-4 self.fluor_plot_x = np.arange(self.x_start, self.x_end, self.step_x) self.fluor_plot_y = np.zeros((len(self.fluor_plot_x))) self.curr_x_pos = float( self._magnetstage.get_current_position(1)[3:-2]) time.sleep(0.1) self.motion_time = float( self._magnetstage.get_motiontime_relativemove( 1, self.step_x)[3:-2]) + 0.05 if self.x_start != self.curr_x_pos: t = float( self._magnetstage.get_motiontime_relativemove( 1, np.abs(self.x_start - self.curr_x_pos))[3:-2]) self._magnetstage.move_absolute(1, self.x_start) time.sleep(t + 1) self.get_current_position() self.i = 0 self.sigNextXPoint.emit() return 0 def _next_x_point(self): with self.threadlock: # If the odmr measurement is not running do nothing if self.module_state() != 'locked': return # Stop measurement if stop has been requested if self.stopRequested: self.stopRequested = False self._magnetstage.stop_motion(1) self.signal_stop_scanning.emit() self.module_state.unlock() return # Move the magnet self._magnetstage.move_relative(1, self.step_x) time.sleep(self.motion_time) self.curr_x_pos = float( self._magnetstage.get_current_position(1)[3:-2]) time.sleep(0.1) if self.curr_x_pos > (self.x_end + self.step_x): self.stopRequested = False self._magnetstage.stop_motion(1) self.module_state.unlock() self.signal_stop_scanning.emit() return # Acquire count data if self.i <= (self.n_x_points - 1): self.fluor_plot_y[ self.i] = self._perform_fluorescence_measure()[0] self.i = self.i + 1 else: self.module_state.unlock() self.signal_stop_scanning.emit() return # Fire update signals self.sigPlotXUpdated.emit(self.fluor_plot_x, self.fluor_plot_y) self.sigPositionUpdated.emit() self.sigNextXPoint.emit() return def start_y_scanning(self, tag='logic'): """Starts scanning """ with self.threadlock: if self.module_state() == 'locked': self.log.error( 'Can not start ODMR scan. Logic is already locked.') return -1 self.module_state.lock() self.stopRequested = False self.step_y = int(self.step_y / 2e-4) * 2e-4 self.yfluor_plot_x = np.arange(self.y_start, self.y_end, self.step_y) self.yfluor_plot_y = np.zeros((len(self.yfluor_plot_x))) self.curr_y_pos = float( self._magnetstage.get_current_position(1)[3:-2]) time.sleep(0.1) self.motion_time = float( self._magnetstage.get_motiontime_relativemove( 2, self.step_y)[3:-2]) + 0.05 if self.y_start != self.curr_y_pos: t = float( self._magnetstage.get_motiontime_relativemove( 2, np.abs(self.y_start - self.curr_y_pos))[3:-2]) self._magnetstage.move_absolute(2, self.y_start) time.sleep(t + 1) self.get_current_position() self.i = 0 self.sigNextYPoint.emit() return 0 def _next_y_point(self): with self.threadlock: # If the odmr measurement is not running do nothing if self.module_state() != 'locked': return # Stop measurement if stop has been requested if self.stopRequested: self.stopRequested = False self._magnetstage.stop_motion(2) self.signal_stop_scanning.emit() self.module_state.unlock() return # Move the magnet self._magnetstage.move_relative(2, self.step_y) time.sleep(self.motion_time + 0.1) self.curr_y_pos = float( self._magnetstage.get_current_position(2)[3:-2]) time.sleep(0.1) if self.curr_y_pos > (self.y_end + self.step_y): self.stopRequested = False self._magnetstage.stop_motion(2) self.module_state.unlock() self.signal_stop_scanning.emit() return # Acquire count data if self.i <= (self.n_y_points - 1): self.yfluor_plot_y[ self.i] = self._perform_fluorescence_measure()[0] self.i = self.i + 1 else: self.module_state.unlock() self.signal_stop_scanning.emit() return # Fire update signals self.sigPlotYUpdated.emit(self.yfluor_plot_x, self.yfluor_plot_y) self.sigPositionUpdated.emit() self.sigNextYPoint.emit() return def stop_scanning(self): """Stops the scan @return int: error code (0:OK, -1:error) """ with self.threadlock: if self.module_state() == 'locked': self.stopRequested = True self.signal_stop_scanning.emit() return 0 def _perform_fluorescence_measure(self): #FIXME: that should be run through the TaskRunner! Implement the call # by not using this connection! if self._counter.get_counting_mode() != 0: self._counter.set_counting_mode(mode='CONTINUOUS') self._counter.start_saving() time.sleep(self.fluorescence_integration_time) self._counter.stopCount() data_array, parameters = self._counter.save_data(to_file=False) data_array = np.array(data_array)[:, 1] return data_array.mean(), parameters def get_fit_x_functions(self): """ Return the hardware constraints/limits @return list(str): list of fit function names """ return list(self.fc.fit_list) def get_fit_y_functions(self): """ Return the hardware constraints/limits @return list(str): list of fit function names """ return list(self.fc.fit_list) def do_x_fit(self, fit_function=None, x_data=None, y_data=None): """ Execute the currently configured fit on the measurement data. Optionally on passed data """ if (x_data is None) or (y_data is None): x_data = self.fluor_plot_x y_data = self.fluor_plot_y if fit_function is not None and isinstance(fit_function, str): if fit_function in self.get_fit_x_functions(): self.fc.set_current_fit(fit_function) else: self.fc.set_current_fit('No Fit') if fit_function != 'No Fit': self.log.warning( 'Fit function "{0}" not available in ODMRLogic fit container.' ''.format(fit_function)) self.x_scan_fit_x, self.x_scan_fit_y, result = self.fc.do_fit( x_data, y_data) if result is None: result_str_dict = {} else: result_str_dict = result.result_str_dict # print(result.result_str_dict) self.sigFitXUpdated.emit(self.x_scan_fit_x, self.x_scan_fit_y, result_str_dict, self.fc.current_fit) return def do_y_fit(self, fit_function=None, x_data=None, y_data=None): """ Execute the currently configured fit on the measurement data. Optionally on passed data """ if (x_data is None) or (y_data is None): x_data = self.yfluor_plot_x y_data = self.yfluor_plot_y if fit_function is not None and isinstance(fit_function, str): if fit_function in self.get_fit_y_functions(): self.fc.set_current_fit(fit_function) else: self.fc.set_current_fit('No Fit') if fit_function != 'No Fit': self.log.warning( 'Fit function "{0}" not available in ODMRLogic fit container.' ''.format(fit_function)) self.y_scan_fit_x, self.y_scan_fit_y, result = self.fc.do_fit( x_data, y_data) if result is None: result_str_dict = {} else: result_str_dict = result.result_str_dict self.sigFitYUpdated.emit(self.y_scan_fit_x, self.y_scan_fit_y, result_str_dict, self.fc.current_fit) return def save_data(self, tag=None, colorscale_range=None, percentile_range=None): """ Saves the current data to a file.""" if tag is None: tag = '' # two paths to save the raw data and the odmr scan data. filepath = self._save_logic.get_path_for_module(module_name='MAGNET') filepath2 = self._save_logic.get_path_for_module(module_name='MAGNET') timestamp = datetime.datetime.now() if len(tag) > 0: filelabel = tag + '_ODMR_data' filelabel2 = tag + '_ODMR_data_raw' else: filelabel = 'ODMR_data' filelabel2 = 'ODMR_data_raw' # prepare the data in a dict or in an OrderedDict: data = OrderedDict() data2 = OrderedDict() data['frequency (Hz)'] = self.odmr_plot_x data['count data (counts/s)'] = self.odmr_plot_y data2['count data (counts/s)'] = self.odmr_raw_data[:self. elapsed_sweeps, :] parameters = OrderedDict() parameters['Microwave CW Power (dBm)'] = self.cw_mw_power parameters['Microwave Sweep Power (dBm)'] = self.sweep_mw_power parameters['Run Time (s)'] = self.run_time parameters['Number of frequency sweeps (#)'] = self.elapsed_sweeps parameters['Start Frequency (Hz)'] = self.mw_start parameters['Stop Frequency (Hz)'] = self.mw_stop parameters['Step size (Hz)'] = self.mw_step parameters['Clock Frequency (Hz)'] = self.clock_frequency if self.fc.current_fit != 'No Fit': parameters['Fit function'] = self.fc.current_fit # add all fit parameter to the saved data: for name, param in self.fc.current_fit_param.items(): parameters[name] = str(param) fig = self.draw_figure(cbar_range=colorscale_range, percentile_range=percentile_range) self._save_logic.save_data(data, filepath=filepath, parameters=parameters, filelabel=filelabel, fmt='%.6e', delimiter='\t', timestamp=timestamp, plotfig=fig) self._save_logic.save_data(data2, filepath=filepath2, parameters=parameters, filelabel=filelabel2, fmt='%.6e', delimiter='\t', timestamp=timestamp) self.log.info('ODMR data saved to:\n{0}'.format(filepath)) return def draw_figure(self, cbar_range=None, percentile_range=None): """ Draw the summary figure to save with the data. @param: list cbar_range: (optional) [color_scale_min, color_scale_max]. If not supplied then a default of data_min to data_max will be used. @param: list percentile_range: (optional) Percentile range of the chosen cbar_range. @return: fig fig: a matplotlib figure object to be saved to file. """ freq_data = self.odmr_plot_x count_data = self.odmr_plot_y fit_freq_vals = self.odmr_fit_x fit_count_vals = self.odmr_fit_y matrix_data = self.odmr_plot_xy # If no colorbar range was given, take full range of data if cbar_range is None: cbar_range = np.array([np.min(matrix_data), np.max(matrix_data)]) else: cbar_range = np.array(cbar_range) prefix = ['', 'k', 'M', 'G', 'T'] prefix_index = 0 # Rescale counts data with SI prefix while np.max(count_data) > 1000: count_data = count_data / 1000 fit_count_vals = fit_count_vals / 1000 prefix_index = prefix_index + 1 counts_prefix = prefix[prefix_index] # Rescale frequency data with SI prefix prefix_index = 0 while np.max(freq_data) > 1000: freq_data = freq_data / 1000 fit_freq_vals = fit_freq_vals / 1000 prefix_index = prefix_index + 1 mw_prefix = prefix[prefix_index] # Rescale matrix counts data with SI prefix prefix_index = 0 while np.max(matrix_data) > 1000: matrix_data = matrix_data / 1000 cbar_range = cbar_range / 1000 prefix_index = prefix_index + 1 cbar_prefix = prefix[prefix_index] # Use qudi style plt.style.use(self._save_logic.mpl_qd_style) # Create figure fig, (ax_mean, ax_matrix) = plt.subplots(nrows=2, ncols=1) ax_mean.plot(freq_data, count_data, linestyle=':', linewidth=0.5) # Do not include fit curve if there is no fit calculated. if max(fit_count_vals) > 0: ax_mean.plot(fit_freq_vals, fit_count_vals, marker='None') ax_mean.set_ylabel('Fluorescence (' + counts_prefix + 'c/s)') ax_mean.set_xlim(np.min(freq_data), np.max(freq_data)) matrixplot = ax_matrix.imshow( matrix_data, cmap=plt.get_cmap('inferno'), # reference the right place in qd origin='lower', vmin=cbar_range[0], vmax=cbar_range[1], extent=[ np.min(freq_data), np.max(freq_data), 0, self.number_of_lines ], aspect='auto', interpolation='nearest') ax_matrix.set_xlabel('Frequency (' + mw_prefix + 'Hz)') ax_matrix.set_ylabel('Scan #') # Adjust subplots to make room for colorbar fig.subplots_adjust(right=0.8) # Add colorbar axis to figure cbar_ax = fig.add_axes([0.85, 0.15, 0.02, 0.7]) # Draw colorbar cbar = fig.colorbar(matrixplot, cax=cbar_ax) cbar.set_label('Fluorescence (' + cbar_prefix + 'c/s)') # remove ticks from colorbar for cleaner image cbar.ax.tick_params(which=u'both', length=0) # If we have percentile information, draw that to the figure if percentile_range is not None: cbar.ax.annotate(str(percentile_range[0]), xy=(-0.3, 0.0), xycoords='axes fraction', horizontalalignment='right', verticalalignment='center', rotation=90) cbar.ax.annotate(str(percentile_range[1]), xy=(-0.3, 1.0), xycoords='axes fraction', horizontalalignment='right', verticalalignment='center', rotation=90) cbar.ax.annotate('(percentile)', xy=(-0.3, 0.5), xycoords='axes fraction', horizontalalignment='right', verticalalignment='center', rotation=90) return fig
class OptimizerLogic(GenericLogic): """This is the Logic class for optimizing scanner position on bright features. """ _modclass = 'optimizerlogic' _modtype = 'logic' # declare connectors confocalscanner1 = Connector(interface='ConfocalScannerInterface') fitlogic = Connector(interface='FitLogic') # declare status vars _clock_frequency = StatusVar('clock_frequency', 50) return_slowness = StatusVar(default=20) refocus_XY_size = StatusVar('xy_size', 0.6e-6) optimizer_XY_res = StatusVar('xy_resolution', 10) refocus_Z_size = StatusVar('z_size', 2e-6) optimizer_Z_res = StatusVar('z_resolution', 30) hw_settle_time = StatusVar('settle_time', 0.1) optimization_sequence = StatusVar(default=['XY', 'Z']) do_surface_subtraction = StatusVar('surface_subtraction', False) surface_subtr_scan_offset = StatusVar('surface_subtraction_offset', 1e-6) opt_channel = StatusVar('optimization_channel', 0) # "private" signals to keep track of activities here in the optimizer logic _sigScanNextXyLine = QtCore.Signal() _sigScanZLine = QtCore.Signal() _sigCompletedXyOptimizerScan = QtCore.Signal() _sigDoNextOptimizationStep = QtCore.Signal() _sigFinishedAllOptimizationSteps = QtCore.Signal() # public signals sigImageUpdated = QtCore.Signal() sigRefocusStarted = QtCore.Signal(str) sigRefocusXySizeChanged = QtCore.Signal() sigRefocusZSizeChanged = QtCore.Signal() sigRefocusFinished = QtCore.Signal(str, list) sigClockFrequencyChanged = QtCore.Signal(int) sigPositionChanged = QtCore.Signal(float, float, float) def __init__(self, config, **kwargs): super().__init__(config=config, **kwargs) # locking for thread safety self.threadlock = Mutex() self.stopRequested = False self.is_crosshair = True # Keep track of who called the refocus self._caller_tag = '' def on_activate(self): """ Initialisation performed during activation of the module. @return int: error code (0:OK, -1:error) """ self._scanning_device = self.get_connector('confocalscanner1') self._fit_logic = self.get_connector('fitlogic') # Reads in the maximal scanning range. The unit of that scan range is micrometer! self.x_range = self._scanning_device.get_position_range()[0] self.y_range = self._scanning_device.get_position_range()[1] self.z_range = self._scanning_device.get_position_range()[2] self._initial_pos_x = 0. self._initial_pos_y = 0. self._initial_pos_z = 0. self.optim_pos_x = self._initial_pos_x self.optim_pos_y = self._initial_pos_y self.optim_pos_z = self._initial_pos_z self.optim_sigma_x = 0. self.optim_sigma_y = 0. self.optim_sigma_z = 0. self._max_offset = 3. # Sets the current position to the center of the maximal scanning range self._current_x = (self.x_range[0] + self.x_range[1]) / 2 self._current_y = (self.y_range[0] + self.y_range[1]) / 2 self._current_z = (self.z_range[0] + self.z_range[1]) / 2 self._current_a = 0.0 ########################### # Fit Params and Settings # model, params = self._fit_logic.make_gaussianlinearoffset_model() self.z_params = params self.use_custom_params = {name: False for name, param in params.items()} # Initialization of internal counter for scanning self._xy_scan_line_count = 0 # Initialization of optimization sequence step counter self._optimization_step = 0 # Sets connections between signals and functions self._sigScanNextXyLine.connect(self._refocus_xy_line, QtCore.Qt.QueuedConnection) self._sigScanZLine.connect(self.do_z_optimization, QtCore.Qt.QueuedConnection) self._sigCompletedXyOptimizerScan.connect(self._set_optimized_xy_from_fit, QtCore.Qt.QueuedConnection) self._sigDoNextOptimizationStep.connect(self._do_next_optimization_step, QtCore.Qt.QueuedConnection) self._sigFinishedAllOptimizationSteps.connect(self.finish_refocus) self._initialize_xy_refocus_image() self._initialize_z_refocus_image() return 0 def on_deactivate(self): """ Reverse steps of activation @return int: error code (0:OK, -1:error) """ return 0 def check_optimization_sequence(self): """ Check the sequence of scan events for the optimization. """ # Check the supplied optimization sequence only contains 'XY' and 'Z' if len(set(self.optimization_sequence).difference({'XY', 'Z'})) > 0: self.log.error('Requested optimization sequence contains unknown steps. Please provide ' 'a sequence containing only \'XY\' and \'Z\' strings. ' 'The default [\'XY\', \'Z\'] will be used.') self.optimization_sequence = ['XY', 'Z'] def get_scanner_count_channels(self): """ Get lis of counting channels from scanning device. @return list(str): names of counter channels """ return self._scanning_device.get_scanner_count_channels() def set_clock_frequency(self, clock_frequency): """Sets the frequency of the clock @param int clock_frequency: desired frequency of the clock @return int: error code (0:OK, -1:error) """ # checks if scanner is still running if self.getState() == 'locked': return -1 else: self._clock_frequency = int(clock_frequency) self.sigClockFrequencyChanged.emit(self._clock_frequency) return 0 def set_refocus_XY_size(self, size): """ Set the number of pixels in the refocus image for X and Y directions @param int size: XY image size in pixels """ self.refocus_XY_size = size self.sigRefocusXySizeChanged.emit() def set_refocus_Z_size(self, size): """ Set the number of values for Z refocus @param int size: number of values for Z refocus """ self.refocus_Z_size = size self.sigRefocusZSizeChanged.emit() def start_refocus(self, initial_pos=None, caller_tag='unknown', tag='logic'): """ Starts the optimization scan around initial_pos @param list initial_pos: with the structure [float, float, float] @param str caller_tag: @param str tag: """ # checking if refocus corresponding to crosshair or corresponding to initial_pos if isinstance(initial_pos, (np.ndarray,)) and initial_pos.size >= 3: self._initial_pos_x, self._initial_pos_y, self._initial_pos_z = initial_pos[0:3] elif isinstance(initial_pos, (list, tuple)) and len(initial_pos) >= 3: self._initial_pos_x, self._initial_pos_y, self._initial_pos_z = initial_pos[0:3] elif initial_pos is None: scpos = self._scanning_device.get_scanner_position()[0:3] self._initial_pos_x, self._initial_pos_y, self._initial_pos_z = scpos else: pass # TODO: throw error # Keep track of where the start_refocus was initiated self._caller_tag = caller_tag # Set the optim_pos values to match the initial_pos values. # This means we can use optim_pos in subsequent steps and ensure # that we benefit from any completed optimization step. self.optim_pos_x = self._initial_pos_x self.optim_pos_y = self._initial_pos_y self.optim_pos_z = self._initial_pos_z self.optim_sigma_x = 0. self.optim_sigma_y = 0. self.optim_sigma_z = 0. # self._xy_scan_line_count = 0 self._optimization_step = 0 self.check_optimization_sequence() scanner_status = self.start_scanner() if scanner_status < 0: self.sigRefocusFinished.emit( self._caller_tag, [self.optim_pos_x, self.optim_pos_y, self.optim_pos_z, 0]) return self.sigRefocusStarted.emit(tag) self._sigDoNextOptimizationStep.emit() def stop_refocus(self): """Stops refocus.""" with self.threadlock: self.stopRequested = True def _initialize_xy_refocus_image(self): """Initialisation of the xy refocus image.""" self._xy_scan_line_count = 0 # Take optim pos as center of refocus image, to benefit from any previous # optimization steps that have occurred. x0 = self.optim_pos_x y0 = self.optim_pos_y # defining position intervals for refocushttp://www.spiegel.de/ xmin = np.clip(x0 - 0.5 * self.refocus_XY_size, self.x_range[0], self.x_range[1]) xmax = np.clip(x0 + 0.5 * self.refocus_XY_size, self.x_range[0], self.x_range[1]) ymin = np.clip(y0 - 0.5 * self.refocus_XY_size, self.y_range[0], self.y_range[1]) ymax = np.clip(y0 + 0.5 * self.refocus_XY_size, self.y_range[0], self.y_range[1]) self._X_values = np.linspace(xmin, xmax, num=self.optimizer_XY_res) self._Y_values = np.linspace(ymin, ymax, num=self.optimizer_XY_res) self._Z_values = self.optim_pos_z * np.ones(self._X_values.shape) self._A_values = np.zeros(self._X_values.shape) self._return_X_values = np.linspace(xmax, xmin, num=self.optimizer_XY_res) self._return_A_values = np.zeros(self._return_X_values.shape) self.xy_refocus_image = np.zeros(( len(self._Y_values), len(self._X_values), 3 + len(self.get_scanner_count_channels()))) self.xy_refocus_image[:, :, 0] = np.full((len(self._Y_values), len(self._X_values)), self._X_values) y_value_matrix = np.full((len(self._X_values), len(self._Y_values)), self._Y_values) self.xy_refocus_image[:, :, 1] = y_value_matrix.transpose() self.xy_refocus_image[:, :, 2] = self.optim_pos_z * np.ones((len(self._Y_values), len(self._X_values))) def _initialize_z_refocus_image(self): """Initialisation of the z refocus image.""" self._xy_scan_line_count = 0 # Take optim pos as center of refocus image, to benefit from any previous # optimization steps that have occurred. z0 = self.optim_pos_z zmin = np.clip(z0 - 0.5 * self.refocus_Z_size, self.z_range[0], self.z_range[1]) zmax = np.clip(z0 + 0.5 * self.refocus_Z_size, self.z_range[0], self.z_range[1]) self._zimage_Z_values = np.linspace(zmin, zmax, num=self.optimizer_Z_res) self._fit_zimage_Z_values = np.linspace(zmin, zmax, num=self.optimizer_Z_res) self._zimage_A_values = np.zeros(self._zimage_Z_values.shape) self.z_refocus_line = np.zeros(( len(self._zimage_Z_values), len(self.get_scanner_count_channels()))) self.z_fit_data = np.zeros(len(self._fit_zimage_Z_values)) def _move_to_start_pos(self, start_pos): """Moves the scanner from its current position to the start position of the optimizer scan. @param start_pos float[]: 3-point vector giving x, y, z position to go to. """ n_ch = len(self._scanning_device.get_scanner_axes()) scanner_pos = self._scanning_device.get_scanner_position() lsx = np.linspace(scanner_pos[0], start_pos[0], self.return_slowness) lsy = np.linspace(scanner_pos[1], start_pos[1], self.return_slowness) lsz = np.linspace(scanner_pos[2], start_pos[2], self.return_slowness) if n_ch <= 3: move_to_start_line = np.vstack((lsx, lsy, lsz)[0:n_ch]) else: move_to_start_line = np.vstack((lsx, lsy, lsz, np.ones(lsx.shape) * scanner_pos[3])) counts = self._scanning_device.scan_line(move_to_start_line) if np.any(counts == -1): return -1 time.sleep(self.hw_settle_time) return 0 def _refocus_xy_line(self): """Scanning a line of the xy optimization image. This method repeats itself using the _sigScanNextXyLine until the xy optimization image is complete. """ n_ch = len(self._scanning_device.get_scanner_axes()) # stop scanning if instructed if self.stopRequested: with self.threadlock: self.stopRequested = False self.finish_refocus() self.sigImageUpdated.emit() self.sigRefocusFinished.emit( self._caller_tag, [self.optim_pos_x, self.optim_pos_y, self.optim_pos_z, 0][0:n_ch]) return # move to the start of the first line if self._xy_scan_line_count == 0: status = self._move_to_start_pos([self.xy_refocus_image[0, 0, 0], self.xy_refocus_image[0, 0, 1], self.xy_refocus_image[0, 0, 2]]) if status < 0: self.log.error('Error during move to starting point.') self.stop_refocus() self._sigScanNextXyLine.emit() return lsx = self.xy_refocus_image[self._xy_scan_line_count, :, 0] lsy = self.xy_refocus_image[self._xy_scan_line_count, :, 1] lsz = self.xy_refocus_image[self._xy_scan_line_count, :, 2] # scan a line of the xy optimization image if n_ch <= 3: line = np.vstack((lsx, lsy, lsz)[0:n_ch]) else: line = np.vstack((lsx, lsy, lsz, np.zeros(lsx.shape))) line_counts = self._scanning_device.scan_line(line) if np.any(line_counts == -1): self.log.error('The scan went wrong, killing the scanner.') self.stop_refocus() self._sigScanNextXyLine.emit() return lsx = self._return_X_values lsy = self.xy_refocus_image[self._xy_scan_line_count, 0, 1] * np.ones(lsx.shape) lsz = self.xy_refocus_image[self._xy_scan_line_count, 0, 2] * np.ones(lsx.shape) if n_ch <= 3: return_line = np.vstack((lsx, lsy, lsz)) else: return_line = np.vstack((lsx, lsy, lsz, np.zeros(lsx.shape))) return_line_counts = self._scanning_device.scan_line(return_line) if np.any(return_line_counts == -1): self.log.error('The scan went wrong, killing the scanner.') self.stop_refocus() self._sigScanNextXyLine.emit() return s_ch = len(self.get_scanner_count_channels()) self.xy_refocus_image[self._xy_scan_line_count, :, 3:3 + s_ch] = line_counts self.sigImageUpdated.emit() self._xy_scan_line_count += 1 if self._xy_scan_line_count < np.size(self._Y_values): self._sigScanNextXyLine.emit() else: self._sigCompletedXyOptimizerScan.emit() def _set_optimized_xy_from_fit(self): """Fit the completed xy optimizer scan and set the optimized xy position.""" fit_x, fit_y = np.meshgrid(self._X_values, self._Y_values) xy_fit_data = self.xy_refocus_image[:, :, 3].ravel() axes = np.empty((len(self._X_values) * len(self._Y_values), 2)) axes = (fit_x.flatten(), fit_y.flatten()) result_2D_gaus = self._fit_logic.make_twoDgaussian_fit( xy_axes=axes, data=xy_fit_data, estimator=self._fit_logic.estimate_twoDgaussian_MLE ) # print(result_2D_gaus.fit_report()) if result_2D_gaus.success is False: self.log.error('Error: 2D Gaussian Fit was not successfull!.') print('2D gaussian fit not successfull') self.optim_pos_x = self._initial_pos_x self.optim_pos_y = self._initial_pos_y self.optim_sigma_x = 0. self.optim_sigma_y = 0. # hier abbrechen else: # @reviewer: Do we need this. With constraints not one of these cases will be possible.... if abs(self._initial_pos_x - result_2D_gaus.best_values['center_x']) < self._max_offset and abs(self._initial_pos_x - result_2D_gaus.best_values['center_x']) < self._max_offset: if result_2D_gaus.best_values['center_x'] >= self.x_range[0] and result_2D_gaus.best_values['center_x'] <= self.x_range[1]: if result_2D_gaus.best_values['center_y'] >= self.y_range[0] and result_2D_gaus.best_values['center_y'] <= self.y_range[1]: self.optim_pos_x = result_2D_gaus.best_values['center_x'] self.optim_pos_y = result_2D_gaus.best_values['center_y'] self.optim_sigma_x = result_2D_gaus.best_values['sigma_x'] self.optim_sigma_y = result_2D_gaus.best_values['sigma_y'] else: self.optim_pos_x = self._initial_pos_x self.optim_pos_y = self._initial_pos_y self.optim_sigma_x = 0. self.optim_sigma_y = 0. # emit image updated signal so crosshair can be updated from this fit self.sigImageUpdated.emit() self._sigDoNextOptimizationStep.emit() def do_z_optimization(self): """ Do the z axis optimization.""" # z scaning self._scan_z_line() # z-fit # If subtracting surface, then data can go negative and the gaussian fit offset constraints need to be adjusted if self.do_surface_subtraction: adjusted_param = {} adjusted_param['offset'] = { 'value': 1e-12, 'min': -self.z_refocus_line[:, self.opt_channel].max(), 'max': self.z_refocus_line[:, self.opt_channel].max() } result = self._fit_logic.make_gausspeaklinearoffset_fit( x_axis=self._zimage_Z_values, data=self.z_refocus_line[:, self.opt_channel], add_params=adjusted_param) else: if any(self.use_custom_params.values()): result = self._fit_logic.make_gausspeaklinearoffset_fit( x_axis=self._zimage_Z_values, data=self.z_refocus_line[:, self.opt_channel], # Todo: It is required that the changed parameters are given as a dictionary or parameter object add_params=None) else: result = self._fit_logic.make_gaussianlinearoffset_fit( x_axis=self._zimage_Z_values, data=self.z_refocus_line[:, self.opt_channel], units='m', estimator=self._fit_logic.estimate_gaussianlinearoffset_peak ) self.z_params = result.params if result.success is False: self.log.error('error in 1D Gaussian Fit.') self.optim_pos_z = self._initial_pos_z self.optim_sigma_z = 0. # interrupt here? else: # move to new position # @reviewer: Do we need this. With constraints not one of these cases will be possible.... # checks if new pos is too far away if abs(self._initial_pos_z - result.best_values['center']) < self._max_offset: # checks if new pos is within the scanner range if result.best_values['center'] >= self.z_range[0] and result.best_values['center'] <= self.z_range[1]: self.optim_pos_z = result.best_values['center'] self.optim_sigma_z = result.best_values['sigma'] gauss, params = self._fit_logic.make_gaussianlinearoffset_model() self.z_fit_data = gauss.eval( x=self._fit_zimage_Z_values, params=result.params) else: # new pos is too far away # checks if new pos is too high self.optim_sigma_z = 0. if result.best_values['center'] > self._initial_pos_z: if self._initial_pos_z + 0.5 * self.refocus_Z_size <= self.z_range[1]: # moves to higher edge of scan range self.optim_pos_z = self._initial_pos_z + 0.5 * self.refocus_Z_size else: self.optim_pos_z = self.z_range[1] # moves to highest possible value else: if self._initial_pos_z + 0.5 * self.refocus_Z_size >= self.z_range[0]: # moves to lower edge of scan range self.optim_pos_z = self._initial_pos_z + 0.5 * self.refocus_Z_size else: self.optim_pos_z = self.z_range[0] # moves to lowest possible value self.sigImageUpdated.emit() self._sigDoNextOptimizationStep.emit() def finish_refocus(self): """ Finishes up and releases hardware after the optimizer scans.""" self.kill_scanner() self.log.info( 'Optimised from ({0:.3e},{1:.3e},{2:.3e}) to local ' 'maximum at ({3:.3e},{4:.3e},{5:.3e}).'.format( self._initial_pos_x, self._initial_pos_y, self._initial_pos_z, self.optim_pos_x, self.optim_pos_y, self.optim_pos_z)) # Signal that the optimization has finished, and "return" the optimal position along with # caller_tag self.sigRefocusFinished.emit( self._caller_tag, [self.optim_pos_x, self.optim_pos_y, self.optim_pos_z, 0]) def _scan_z_line(self): """Scans the z line for refocus.""" # Moves to the start value of the z-scan status = self._move_to_start_pos( [self.optim_pos_x, self.optim_pos_y, self._zimage_Z_values[0]]) if status < 0: self.log.error('Error during move to starting point.') self.stop_refocus() return n_ch = len(self._scanning_device.get_scanner_axes()) # defining trace of positions for z-refocus scan_z_line = self._zimage_Z_values scan_x_line = self.optim_pos_x * np.ones(self._zimage_Z_values.shape) scan_y_line = self.optim_pos_y * np.ones(self._zimage_Z_values.shape) if n_ch <= 3: line = np.vstack((scan_x_line, scan_y_line, scan_z_line)[0:n_ch]) else: line = np.vstack((scan_x_line, scan_y_line, scan_z_line, np.zeros(scan_x_line.shape))) # Perform scan line_counts = self._scanning_device.scan_line(line) if np.any(line_counts == -1): self.log.error('Z scan went wrong, killing the scanner.') self.stop_refocus() return # Set the data self.z_refocus_line = line_counts # If subtracting surface, perform a displaced depth line scan if self.do_surface_subtraction: # Move to start of z-scan status = self._move_to_start_pos([ self.optim_pos_x + self.surface_subtr_scan_offset, self.optim_pos_y, self._zimage_Z_values[0]]) if status < 0: self.log.error('Error during move to starting point.') self.stop_refocus() return # define an offset line to measure "background" if n_ch <= 3: line_bg = np.vstack( (scan_x_line + self.surface_subtr_scan_offset, scan_y_line, scan_z_line)[0:n_ch]) else: line_bg = np.vstack( (scan_x_line + self.surface_subtr_scan_offset, scan_y_line, scan_z_line, np.zeros(scan_x_line.shape))) line_bg_counts = self._scanning_device.scan_line(line_bg) if np.any(line_bg_counts[0] == -1): self.log.error('The scan went wrong, killing the scanner.') self.stop_refocus() return # surface-subtracted line scan data is the difference self.z_refocus_line = line_counts - line_bg_counts def start_scanner(self): """Setting up the scanner device. @return int: error code (0:OK, -1:error) """ self.lock() clock_status = self._scanning_device.set_up_scanner_clock( clock_frequency=self._clock_frequency) if clock_status < 0: self.unlock() return -1 scanner_status = self._scanning_device.set_up_scanner() if scanner_status < 0: self._scanning_device.close_scanner_clock() self.unlock() return -1 return 0 def kill_scanner(self): """Closing the scanner device. @return int: error code (0:OK, -1:error) """ try: rv = self._scanning_device.close_scanner() except: self.log.exception('Closing refocus scanner failed.') return -1 try: rv2 = self._scanning_device.close_scanner_clock() except: self.log.exception('Closing refocus scanner clock failed.') return -1 self.unlock() return rv + rv2 def _do_next_optimization_step(self): """Handle the steps through the specified optimization sequence """ # At the end fo the sequence, finish the optimization if self._optimization_step == len(self.optimization_sequence): self._sigFinishedAllOptimizationSteps.emit() return # Read the next step in the optimization sequence this_step = self.optimization_sequence[self._optimization_step] # Increment the step counter self._optimization_step += 1 # Launch the next step if this_step == 'XY': self._initialize_xy_refocus_image() self._sigScanNextXyLine.emit() elif this_step == 'Z': self._initialize_z_refocus_image() self._sigScanZLine.emit() def set_position(self, tag, x=None, y=None, z=None, a=None): """ Set focus position. @param str tag: sting indicating who caused position change @param float x: x axis position in m @param float y: y axis position in m @param float z: z axis position in m @param float a: a axis position in m """ if x is not None: self._current_x = x if y is not None: self._current_y = y if z is not None: self._current_z = z self.sigPositionChanged.emit(self._current_x, self._current_y, self._current_z)
class CounterLogic(GenericLogic): """ This logic module gathers data from a hardware counting device. @signal sigCounterUpdate: there is new counting data available @signal sigCountContinuousNext: used to simulate a loop in which the data acquisition runs. @sigmal sigCountGatedNext: ??? @return error: 0 is OK, -1 is error """ sigCounterUpdated = QtCore.Signal() sigCountDataNext = QtCore.Signal() sigGatedCounterFinished = QtCore.Signal() sigGatedCounterContinue = QtCore.Signal(bool) sigCountingSamplesChanged = QtCore.Signal(int) sigCountLengthChanged = QtCore.Signal(int) sigCountFrequencyChanged = QtCore.Signal(float) sigSavingStatusChanged = QtCore.Signal(bool) sigCountStatusChanged = QtCore.Signal(bool) sigCountingModeChanged = QtCore.Signal(CountingMode) _modclass = 'CounterLogic' _modtype = 'logic' ## declare connectors counter1 = Connector(interface='SlowCounterInterface') savelogic = Connector(interface='SaveLogic') # status vars _count_length = StatusVar('count_length', 300) _smooth_window_length = StatusVar('smooth_window_length', 10) _counting_samples = StatusVar('counting_samples', 1) _count_frequency = StatusVar('count_frequency', 50) _saving = StatusVar('saving', False) def __init__(self, config, **kwargs): """ Create CounterLogic object with connectors. @param dict config: module configuration @param dict kwargs: optional parameters """ super().__init__(config=config, **kwargs) #locking for thread safety self.threadlock = Mutex() self.log.debug('The following configuration was found.') # checking for the right configuration for key in config.keys(): self.log.debug('{0}: {1}'.format(key, config[key])) # in bins self._count_length = 300 self._smooth_window_length = 10 self._counting_samples = 1 # oversampling # in hertz self._count_frequency = 50 # self._binned_counting = True # UNUSED? self._counting_mode = CountingMode['CONTINUOUS'] self._saving = False return def on_activate(self): """ Initialisation performed during activation of the module. """ # Connect to hardware and save logic self._counting_device = self.counter1() self._save_logic = self.savelogic() # Recall saved app-parameters if 'counting_mode' in self._statusVariables: self._counting_mode = CountingMode[ self._statusVariables['counting_mode']] constraints = self.get_hardware_constraints() number_of_detectors = constraints.max_detectors # initialize data arrays self.countdata = np.zeros( [len(self.get_channels()), self._count_length]) self.countdata_smoothed = np.zeros( [len(self.get_channels()), self._count_length]) self.rawdata = np.zeros( [len(self.get_channels()), self._counting_samples]) self._already_counted_samples = 0 # For gated counting self._data_to_save = [] # Flag to stop the loop self.stopRequested = False self._saving_start_time = time.time() # connect signals self.sigCountDataNext.connect(self.count_loop_body, QtCore.Qt.QueuedConnection) return def on_deactivate(self): """ Deinitialisation performed during deactivation of the module. """ # Save parameters to disk self._statusVariables['counting_mode'] = self._counting_mode.name # Stop measurement if self.module_state() == 'locked': self._stopCount_wait() self.sigCountDataNext.disconnect() return def get_hardware_constraints(self): """ Retrieve the hardware constrains from the counter device. @return SlowCounterConstraints: object with constraints for the counter """ return self._counting_device.get_constraints() def set_counting_samples(self, samples=1): """ Sets the length of the counted bins. The counter is stopped first and restarted afterwards. @param int samples: oversampling in units of bins (positive int ). @return int: oversampling in units of bins. """ # Determine if the counter has to be restarted after setting the parameter if self.module_state() == 'locked': restart = True else: restart = False if samples > 0: self._stopCount_wait() self._counting_samples = int(samples) # if the counter was running, restart it if restart: self.startCount() else: self.log.warning( 'counting_samples has to be larger than 0! Command ignored!') self.sigCountingSamplesChanged.emit(self._counting_samples) return self._counting_samples def set_count_length(self, length=300): """ Sets the time trace in units of bins. @param int length: time trace in units of bins (positive int). @return int: length of time trace in units of bins This makes sure, the counter is stopped first and restarted afterwards. """ if self.module_state() == 'locked': restart = True else: restart = False if length > 0: self._stopCount_wait() self._count_length = int(length) # if the counter was running, restart it if restart: self.startCount() else: self.log.warning( 'count_length has to be larger than 0! Command ignored!') self.sigCountLengthChanged.emit(self._count_length) return self._count_length def set_count_frequency(self, frequency=50): """ Sets the frequency with which the data is acquired. @param float frequency: the desired frequency of counting in Hz @return float: the actual frequency of counting in Hz This makes sure, the counter is stopped first and restarted afterwards. """ constraints = self.get_hardware_constraints() if self.module_state() == 'locked': restart = True else: restart = False if constraints.min_count_frequency <= frequency <= constraints.max_count_frequency: self._stopCount_wait() self._count_frequency = frequency # if the counter was running, restart it if restart: self.startCount() else: self.log.warning('count_frequency not in range! Command ignored!') self.sigCountFrequencyChanged.emit(self._count_frequency) return self._count_frequency def get_count_length(self): """ Returns the currently set length of the counting array. @return int: count_length """ return self._count_length #FIXME: get from hardware def get_count_frequency(self): """ Returns the currently set frequency of counting (resolution). @return float: count_frequency """ return self._count_frequency def get_counting_samples(self): """ Returns the currently set number of samples counted per readout. @return int: counting_samples """ return self._counting_samples def get_saving_state(self): """ Returns if the data is saved in the moment. @return bool: saving state """ return self._saving def start_saving(self, resume=False): """ Sets up start-time and initializes data array, if not resuming, and changes saving state. If the counter is not running it will be started in order to have data to save. @return bool: saving state """ if not resume: self._data_to_save = [] self._saving_start_time = time.time() self._saving = True # If the counter is not running, then it should start running so there is data to save if self.module_state() != 'locked': self.startCount() self.sigSavingStatusChanged.emit(self._saving) return self._saving def save_data(self, to_file=True, postfix=''): """ Save the counter trace data and writes it to a file. @param bool to_file: indicate, whether data have to be saved to file @param str postfix: an additional tag, which will be added to the filename upon save @return dict parameters: Dictionary which contains the saving parameters """ # stop saving thus saving state has to be set to False self._saving = False self._saving_stop_time = time.time() # write the parameters: parameters = OrderedDict() parameters['Start counting time'] = time.strftime( '%d.%m.%Y %Hh:%Mmin:%Ss', time.localtime(self._saving_start_time)) parameters['Stop counting time'] = time.strftime( '%d.%m.%Y %Hh:%Mmin:%Ss', time.localtime(self._saving_stop_time)) parameters['Count frequency (Hz)'] = self._count_frequency parameters['Oversampling (Samples)'] = self._counting_samples parameters[ 'Smooth Window Length (# of events)'] = self._smooth_window_length if to_file: # If there is a postfix then add separating underscore if postfix == '': filelabel = 'count_trace' else: filelabel = 'count_trace_' + postfix # prepare the data in a dict or in an OrderedDict: header = 'Time (s)' for i, detector in enumerate(self.get_channels()): header = header + ',Signal{0} (counts/s)'.format(i) data = {header: self._data_to_save} filepath = self._save_logic.get_path_for_module( module_name='Counter') fig = self.draw_figure(data=np.array(self._data_to_save)) self._save_logic.save_data(data, filepath=filepath, parameters=parameters, filelabel=filelabel, plotfig=fig, delimiter='\t') self.log.info('Counter Trace saved to:\n{0}'.format(filepath)) self.sigSavingStatusChanged.emit(self._saving) return self._data_to_save, parameters def draw_figure(self, data): """ Draw figure to save with data file. @param: nparray data: a numpy array containing counts vs time for all detectors @return: fig fig: a matplotlib figure object to be saved to file. """ count_data = data[:, 1:len(self.get_channels()) + 1] time_data = data[:, 0] # Scale count values using SI prefix prefix = ['', 'k', 'M', 'G'] prefix_index = 0 while np.max(count_data) > 1000: count_data = count_data / 1000 prefix_index = prefix_index + 1 counts_prefix = prefix[prefix_index] # Use qudi style plt.style.use(self._save_logic.mpl_qd_style) # Create figure fig, ax = plt.subplots() ax.plot(time_data, count_data, linestyle=':', linewidth=0.5) ax.set_xlabel('Time (s)') ax.set_ylabel('Fluorescence (' + counts_prefix + 'c/s)') return fig def set_counting_mode(self, mode='CONTINUOUS'): """Set the counting mode, to change between continuous and gated counting. Possible options are: 'CONTINUOUS' = counts continuously 'GATED' = bins the counts according to a gate signal 'FINITE_GATED' = finite measurement with predefined number of samples @return str: counting mode """ constraints = self.get_hardware_constraints() if self.module_state() != 'locked': if CountingMode[mode] in constraints.counting_mode: self._counting_mode = CountingMode[mode] self.log.debug('New counting mode: {}'.format( self._counting_mode)) else: self.log.warning( 'Counting mode not supported from hardware. Command ignored!' ) self.sigCountingModeChanged.emit(self._counting_mode) else: self.log.error( 'Cannot change counting mode while counter is still running.') return self._counting_mode def get_counting_mode(self): """ Retrieve the current counting mode. @return str: one of the possible counting options: 'CONTINUOUS' = counts continuously 'GATED' = bins the counts according to a gate signal 'FINITE_GATED' = finite measurement with predefined number of samples """ return self._counting_mode # FIXME: Not implemented for self._counting_mode == 'gated' def startCount(self): """ This is called externally, and is basically a wrapper that redirects to the chosen counting mode start function. @return error: 0 is OK, -1 is error """ # Sanity checks constraints = self.get_hardware_constraints() # TODO: BUG FIXED HERE: introduce corresponding changes to GitHub files if self._counting_mode.value not in [ constraints.counting_mode[j].value for j in range(len(constraints.counting_mode)) ]: self.log.error( 'Unknown counting mode "{0}". Cannot start the counter.' ''.format(self._counting_mode)) self.sigCountStatusChanged.emit(False) return -1 # ORIGINAL VERSION # constraints = self.get_hardware_constraints() # if self._counting_mode not in constraints.counting_mode: # self.log.error('Unknown counting mode "{0}". Cannot start the counter.' # ''.format(self._counting_mode)) # self.sigCountStatusChanged.emit(False) # return -1 with self.threadlock: # Lock module if self.module_state() != 'locked': self.module_state.lock() else: self.log.warning( 'Counter already running. Method call ignored.') return 0 # Set up clock clock_status = self._counting_device.set_up_clock( clock_frequency=self._count_frequency) if clock_status < 0: self.module_state.unlock() self.sigCountStatusChanged.emit(False) return -1 # Set up counter if self._counting_mode == CountingMode['FINITE_GATED']: counter_status = self._counting_device.set_up_counter( counter_buffer=self._count_length) # elif self._counting_mode == CountingMode['GATED']: # else: counter_status = self._counting_device.set_up_counter() if counter_status < 0: self._counting_device.close_clock() self.module_state.unlock() self.sigCountStatusChanged.emit(False) return -1 # initialising the data arrays self.rawdata = np.zeros( [len(self.get_channels()), self._counting_samples]) self.countdata = np.zeros( [len(self.get_channels()), self._count_length]) self.countdata_smoothed = np.zeros( [len(self.get_channels()), self._count_length]) self._sampling_data = np.empty( [len(self.get_channels()), self._counting_samples]) # the sample index for gated counting self._already_counted_samples = 0 # Start data reader loop self.sigCountStatusChanged.emit(True) self.sigCountDataNext.emit() return def stopCount(self): """ Set a flag to request stopping counting. """ if self.module_state() == 'locked': with self.threadlock: self.stopRequested = True return def count_loop_body(self): """ This method gets the count data from the hardware for the continuous counting mode (default). It runs repeatedly in the logic module event loop by being connected to sigCountContinuousNext and emitting sigCountContinuousNext through a queued connection. """ if self.module_state() == 'locked': with self.threadlock: # check for aborts of the thread in break if necessary if self.stopRequested: # close off the actual counter cnt_err = self._counting_device.close_counter() clk_err = self._counting_device.close_clock() if cnt_err < 0 or clk_err < 0: self.log.error( 'Could not even close the hardware, giving up.') # switch the state variable off again self.stopRequested = False self.module_state.unlock() self.sigCounterUpdated.emit() return # read the current counter value self.rawdata = self._counting_device.get_counter( samples=self._counting_samples) if self.rawdata[0, 0] < 0: self.log.error( 'The counting went wrong, killing the counter.') self.stopRequested = True else: if self._counting_mode == CountingMode['CONTINUOUS']: self._process_data_continous() elif self._counting_mode == CountingMode['GATED']: self._process_data_gated() elif self._counting_mode == CountingMode['FINITE_GATED']: self._process_data_finite_gated() else: self.log.error( 'No valid counting mode set! Can not process counter data.' ) # call this again from event loop self.sigCounterUpdated.emit() self.sigCountDataNext.emit() return def save_current_count_trace(self, name_tag=''): """ The currently displayed counttrace will be saved. @param str name_tag: optional, personal description that will be appended to the file name @return: dict data: Data which was saved str filepath: Filepath dict parameters: Experiment parameters str filelabel: Filelabel This method saves the already displayed counts to file and does not accumulate them. The counttrace variable will be saved to file with the provided name! """ # If there is a postfix then add separating underscore if name_tag == '': filelabel = 'snapshot_count_trace' else: filelabel = 'snapshot_count_trace_' + name_tag stop_time = self._count_length / self._count_frequency time_step_size = stop_time / len(self.countdata) x_axis = np.arange(0, stop_time, time_step_size) # prepare the data in a dict or in an OrderedDict: data = OrderedDict() chans = self.get_channels() savearr = np.empty((len(chans) + 1, len(x_axis))) savearr[0] = x_axis datastr = 'Time (s)' for i, ch in enumerate(chans): savearr[i + 1] = self.countdata[i] datastr += ',Signal {0} (counts/s)'.format(i) data[datastr] = savearr.transpose() # write the parameters: parameters = OrderedDict() timestr = time.strftime('%d.%m.%Y %Hh:%Mmin:%Ss', time.localtime(time.time())) parameters['Saved at time'] = timestr parameters['Count frequency (Hz)'] = self._count_frequency parameters['Oversampling (Samples)'] = self._counting_samples parameters[ 'Smooth Window Length (# of events)'] = self._smooth_window_length filepath = self._save_logic.get_path_for_module(module_name='Counter') self._save_logic.save_data(data, filepath=filepath, parameters=parameters, filelabel=filelabel, delimiter='\t') self.log.debug('Current Counter Trace saved to: {0}'.format(filepath)) return data, filepath, parameters, filelabel def get_channels(self): """ Shortcut for hardware get_counter_channels. @return list(str): return list of active counter channel names """ return self._counting_device.get_counter_channels() def _process_data_continous(self): """ Processes the raw data from the counting device @return: """ for i, ch in enumerate(self.get_channels()): # remember the new count data in circular array self.countdata[i, 0] = np.average(self.rawdata[i]) # move the array to the left to make space for the new data self.countdata = np.roll(self.countdata, -1, axis=1) # also move the smoothing array self.countdata_smoothed = np.roll(self.countdata_smoothed, -1, axis=1) # calculate the median and save it window = -int(self._smooth_window_length / 2) - 1 for i, ch in enumerate(self.get_channels()): self.countdata_smoothed[i, window:] = np.median( self.countdata[i, -self._smooth_window_length:]) # save the data if necessary if self._saving: # if oversampling is necessary if self._counting_samples > 1: chans = self.get_channels() self._sampling_data = np.empty( [len(chans) + 1, self._counting_samples]) self._sampling_data[ 0, :] = time.time() - self._saving_start_time for i, ch in enumerate(chans): self._sampling_data[i + 1, 0] = self.rawdata[i] self._data_to_save.extend(list(self._sampling_data)) # if we don't want to use oversampling else: # append tuple to data stream (timestamp, average counts) chans = self.get_channels() newdata = np.empty((len(chans) + 1, )) newdata[0] = time.time() - self._saving_start_time for i, ch in enumerate(chans): newdata[i + 1] = self.countdata[i, -1] self._data_to_save.append(newdata) return def _process_data_gated(self): """ Processes the raw data from the counting device @return: """ # remember the new count data in circular array self.countdata[0] = np.average(self.rawdata[0]) # move the array to the left to make space for the new data self.countdata = np.roll(self.countdata, -1) # also move the smoothing array self.countdata_smoothed = np.roll(self.countdata_smoothed, -1) # calculate the median and save it self.countdata_smoothed[-int(self._smooth_window_length / 2) - 1:] = np.median( self. countdata[-self._smooth_window_length:]) # save the data if necessary if self._saving: # if oversampling is necessary if self._counting_samples > 1: self._sampling_data = np.empty((self._counting_samples, 2)) self._sampling_data[:, 0] = time.time() - self._saving_start_time self._sampling_data[:, 1] = self.rawdata[0] self._data_to_save.extend(list(self._sampling_data)) # if we don't want to use oversampling else: # append tuple to data stream (timestamp, average counts) self._data_to_save.append( np.array((time.time() - self._saving_start_time, self.countdata[-1]))) return def _process_data_finite_gated(self): """ Processes the raw data from the counting device @return: """ if self._already_counted_samples + len(self.rawdata[0]) >= len( self.countdata): needed_counts = len(self.countdata) - self._already_counted_samples self.countdata[0:needed_counts] = self.rawdata[0][0:needed_counts] self.countdata = np.roll(self.countdata, -needed_counts) self._already_counted_samples = 0 self.stopRequested = True else: # replace the first part of the array with the new data: self.countdata[0:len(self.rawdata[0])] = self.rawdata[0] # roll the array by the amount of data it had been inserted: self.countdata = np.roll(self.countdata, -len(self.rawdata[0])) # increment the index counter: self._already_counted_samples += len(self.rawdata[0]) return def _stopCount_wait(self, timeout=5.0): """ Stops the counter and waits until it actually has stopped. @param timeout: float, the max. time in seconds how long the method should wait for the process to stop. @return: error code """ self.stopCount() start_time = time.time() while self.module_state() == 'locked': time.sleep(0.1) if time.time() - start_time >= timeout: self.log.error( 'Stopping the counter timed out after {0}s'.format( timeout)) return -1 return 0
class PulserDummy(Base, PulserInterface): """ Dummy class for PulseInterface Be careful in adjusting the method names in that class, since some of them are also connected to the mwsourceinterface (to give the AWG the possibility to act like a microwave source). Example config for copy-paste: pulser_dummy: module.Class: 'pulser_dummy.PulserDummy' """ _modclass = 'PulserDummy' _modtype = 'hardware' activation_config = StatusVar(default=None) def __init__(self, config, **kwargs): super().__init__(config=config, **kwargs) self.log.info('Dummy Pulser: I will simulate an AWG :) !') self.connected = False self.sample_rate = 25e9 # Deactivate all channels at first: self.channel_states = { 'a_ch1': False, 'a_ch2': False, 'a_ch3': False, 'd_ch1': False, 'd_ch2': False, 'd_ch3': False, 'd_ch4': False, 'd_ch5': False, 'd_ch6': False, 'd_ch7': False, 'd_ch8': False } # for each analog channel one value self.amplitude_dict = {'a_ch1': 1.0, 'a_ch2': 1.0, 'a_ch3': 1.0} self.offset_dict = {'a_ch1': 0.0, 'a_ch2': 0.0, 'a_ch3': 0.0} # for each digital channel one value self.digital_high_dict = { 'd_ch1': 5.0, 'd_ch2': 5.0, 'd_ch3': 5.0, 'd_ch4': 5.0, 'd_ch5': 5.0, 'd_ch6': 5.0, 'd_ch7': 5.0, 'd_ch8': 5.0 } self.digital_low_dict = { 'd_ch1': 0.0, 'd_ch2': 0.0, 'd_ch3': 0.0, 'd_ch4': 0.0, 'd_ch5': 0.0, 'd_ch6': 0.0, 'd_ch7': 0.0, 'd_ch8': 0.0 } self.waveform_set = set() self.sequence_dict = dict() self.current_loaded_assets = dict() self.use_sequencer = True self.interleave = False self.current_status = 0 # that means off, not running. def on_activate(self): """ Initialisation performed during activation of the module. """ self.connected = True self.channel_states = { 'a_ch1': False, 'a_ch2': False, 'a_ch3': False, 'd_ch1': False, 'd_ch2': False, 'd_ch3': False, 'd_ch4': False, 'd_ch5': False, 'd_ch6': False, 'd_ch7': False, 'd_ch8': False } if self.activation_config is None: self.activation_config = self.get_constraints( ).activation_config['config0'] elif self.activation_config not in self.get_constraints( ).activation_config.values(): self.activation_config = self.get_constraints( ).activation_config['config0'] for chnl in self.activation_config: self.channel_states[chnl] = True def on_deactivate(self): """ Deinitialisation performed during deactivation of the module. """ self.connected = False def get_constraints(self): """ Retrieve the hardware constrains from the Pulsing device. @return constraints object: object with pulser constraints as attributes. Provides all the constraints (e.g. sample_rate, amplitude, total_length_bins, channel_config, ...) related to the pulse generator hardware to the caller. SEE PulserConstraints CLASS IN pulser_interface.py FOR AVAILABLE CONSTRAINTS!!! If you are not sure about the meaning, look in other hardware files to get an impression. If still additional constraints are needed, then they have to be added to the PulserConstraints class. Each scalar parameter is an ScalarConstraints object defined in cor.util.interfaces. Essentially it contains min/max values as well as min step size, default value and unit of the parameter. PulserConstraints.activation_config differs, since it contain the channel configuration/activation information of the form: {<descriptor_str>: <channel_set>, <descriptor_str>: <channel_set>, ...} If the constraints cannot be set in the pulsing hardware (e.g. because it might have no sequence mode) just leave it out so that the default is used (only zeros). """ constraints = PulserConstraints() if self.interleave: constraints.sample_rate.min = 12.0e9 constraints.sample_rate.max = 24.0e9 constraints.sample_rate.step = 4.0e8 constraints.sample_rate.default = 24.0e9 else: constraints.sample_rate.min = 10.0e6 constraints.sample_rate.max = 12.0e9 constraints.sample_rate.step = 10.0e6 constraints.sample_rate.default = 12.0e9 constraints.a_ch_amplitude.min = 0.02 constraints.a_ch_amplitude.max = 2.0 constraints.a_ch_amplitude.step = 0.001 constraints.a_ch_amplitude.default = 2.0 constraints.a_ch_offset.min = -1.0 constraints.a_ch_offset.max = 1.0 constraints.a_ch_offset.step = 0.001 constraints.a_ch_offset.default = 0.0 constraints.d_ch_low.min = -1.0 constraints.d_ch_low.max = 4.0 constraints.d_ch_low.step = 0.01 constraints.d_ch_low.default = 0.0 constraints.d_ch_high.min = 0.0 constraints.d_ch_high.max = 5.0 constraints.d_ch_high.step = 0.01 constraints.d_ch_high.default = 5.0 constraints.waveform_length.min = 80 constraints.waveform_length.max = 64800000 constraints.waveform_length.step = 1 constraints.waveform_length.default = 80 constraints.waveform_num.min = 1 constraints.waveform_num.max = 32000 constraints.waveform_num.step = 1 constraints.waveform_num.default = 1 constraints.sequence_num.min = 1 constraints.sequence_num.max = 8000 constraints.sequence_num.step = 1 constraints.sequence_num.default = 1 constraints.subsequence_num.min = 1 constraints.subsequence_num.max = 4000 constraints.subsequence_num.step = 1 constraints.subsequence_num.default = 1 # If sequencer mode is available then these should be specified constraints.repetitions.min = 0 constraints.repetitions.max = 65539 constraints.repetitions.step = 1 constraints.repetitions.default = 0 constraints.event_triggers = ['A', 'B'] constraints.flags = ['A', 'B', 'C', 'D'] constraints.sequence_steps.min = 0 constraints.sequence_steps.max = 8000 constraints.sequence_steps.step = 1 constraints.sequence_steps.default = 0 # the name a_ch<num> and d_ch<num> are generic names, which describe UNAMBIGUOUSLY the # channels. Here all possible channel configurations are stated, where only the generic # names should be used. The names for the different configurations can be customary chosen. activation_config = OrderedDict() activation_config['config0'] = frozenset( {'a_ch1', 'd_ch1', 'd_ch2', 'a_ch2', 'd_ch3', 'd_ch4'}) activation_config['config1'] = frozenset( {'a_ch2', 'd_ch1', 'd_ch2', 'a_ch3', 'd_ch3', 'd_ch4'}) # Usage of channel 1 only: activation_config['config2'] = frozenset({'a_ch2', 'd_ch1', 'd_ch2'}) # Usage of channel 2 only: activation_config['config3'] = frozenset({'a_ch3', 'd_ch3', 'd_ch4'}) # Usage of Interleave mode: activation_config['config4'] = frozenset({'a_ch1', 'd_ch1', 'd_ch2'}) # Usage of only digital channels: activation_config['config5'] = frozenset({ 'd_ch1', 'd_ch2', 'd_ch3', 'd_ch4', 'd_ch5', 'd_ch6', 'd_ch7', 'd_ch8' }) # Usage of only one analog channel: activation_config['config6'] = frozenset({'a_ch1'}) activation_config['config7'] = frozenset({'a_ch2'}) activation_config['config8'] = frozenset({'a_ch3'}) # Usage of only the analog channels: activation_config['config9'] = frozenset({'a_ch2', 'a_ch3'}) constraints.activation_config = activation_config constraints.sequence_option = SequenceOption.OPTIONAL return constraints def pulser_on(self): """ Switches the pulsing device on. @return int: error code (0:stopped, -1:error, 1:running) """ if self.current_status == 0: self.current_status = 1 self.log.info('PulserDummy: Switch on the Output.') time.sleep(1) return 0 else: return -1 def pulser_off(self): """ Switches the pulsing device off. @return int: error code (0:stopped, -1:error, 1:running) """ if self.current_status == 1: self.current_status = 0 self.log.info('PulserDummy: Switch off the Output.') return 0 def write_waveform(self, name, analog_samples, digital_samples, is_first_chunk, is_last_chunk, total_number_of_samples): """ Write a new waveform or append samples to an already existing waveform on the device memory. The flags is_first_chunk and is_last_chunk can be used as indicator if a new waveform should be created or if the write process to a waveform should be terminated. NOTE: All sample arrays in analog_samples and digital_samples must be of equal length! @param str name: the name of the waveform to be created/append to @param dict analog_samples: keys are the generic analog channel names (i.e. 'a_ch1') and values are 1D numpy arrays of type float32 containing the voltage samples. @param dict digital_samples: keys are the generic digital channel names (i.e. 'd_ch1') and values are 1D numpy arrays of type bool containing the marker states. @param bool is_first_chunk: Flag indicating if it is the first chunk to write. If True this method will create a new empty wavveform. If False the samples are appended to the existing waveform. @param bool is_last_chunk: Flag indicating if it is the last chunk to write. Some devices may need to know when to close the appending wfm. @param int total_number_of_samples: The number of sample points for the entire waveform (not only the currently written chunk) @return (int, list): Number of samples written (-1 indicates failed process) and list of created waveform names """ waveforms = list() # Sanity checks if len(analog_samples) > 0: number_of_samples = len(analog_samples[list(analog_samples)[0]]) elif len(digital_samples) > 0: number_of_samples = len(digital_samples[list(digital_samples)[0]]) else: self.log.error( 'No analog or digital samples passed to write_waveform method in dummy ' 'pulser.') return -1, waveforms for chnl, samples in analog_samples.items(): if len(samples) != number_of_samples: self.log.error( 'Unequal length of sample arrays for different channels in dummy ' 'pulser.') return -1, waveforms for chnl, samples in digital_samples.items(): if len(samples) != number_of_samples: self.log.error( 'Unequal length of sample arrays for different channels in dummy ' 'pulser.') return -1, waveforms # Determine if only digital samples are active. In that case each channel will get a # waveform. Otherwise only the analog channels will have a waveform with digital channel # samples included (as it is the case in Tektronix and Keysight AWGs). # Simulate a 1Gbit/s transfer speed. Assume each analog waveform sample is 5 bytes large # (4 byte float and 1 byte marker bitmask). Assume each digital waveform sample is 1 byte. if len(analog_samples) > 0: for chnl in analog_samples: waveforms.append(name + chnl[1:]) time.sleep(number_of_samples * 5 * 8 / 1024**3) else: for chnl in digital_samples: waveforms.append(name + chnl[1:]) time.sleep(number_of_samples * 8 / 1024**3) self.waveform_set.update(waveforms) self.log.info( 'Waveforms with nametag "{0}" directly written on dummy pulser.'. format(name)) return number_of_samples, waveforms def write_sequence(self, name, sequence_parameter_list): """ Write a new sequence on the device memory. @param name: str, the name of the waveform to be created/append to @param sequence_parameter_list: list, contains the parameters for each sequence step and the according waveform names. @return: int, number of sequence steps written (-1 indicates failed process) """ # Check if all waveforms are present on virtual device memory for waveform_tuple, param_dict in sequence_parameter_list: for waveform in waveform_tuple: if waveform not in self.waveform_set: self.log.error( 'Failed to create sequence "{0}" due to waveform "{1}" not ' 'present in device memory.'.format(name, waveform)) return -1 if name in self.sequence_dict: del self.sequence_dict[name] # Fill in sequence information for step, (wfm_tuple, seq_step) in enumerate(sequence_parameter_list, 1): self.log.debug('flag_trigger: {}'.format(seq_step.flag_trigger)) self.log.debug('flag_high: {}'.format(seq_step.flag_high)) self.sequence_dict[name] = len(sequence_parameter_list[0][0]) time.sleep(1) self.log.info( 'Sequence with name "{0}" directly written on dummy pulser.'. format(name)) return len(sequence_parameter_list) def get_waveform_names(self): """ Retrieve the names of all uploaded waveforms on the device. @return list: List of all uploaded waveform name strings in the device workspace. """ return list(self.waveform_set) def get_sequence_names(self): """ Retrieve the names of all uploaded sequence on the device. @return list: List of all uploaded sequence name strings in the device workspace. """ return list(self.sequence_dict) def delete_waveform(self, waveform_name): """ Delete the waveform with name "waveform_name" from the device memory. @param str waveform_name: The name of the waveform to be deleted Optionally a list of waveform names can be passed. @return list: a list of deleted waveform names. """ if isinstance(waveform_name, str): waveform_name = [waveform_name] deleted_waveforms = list() for waveform in waveform_name: if waveform in self.waveform_set: self.waveform_set.remove(waveform) deleted_waveforms.append(waveform) return deleted_waveforms def delete_sequence(self, sequence_name): """ Delete the sequence with name "sequence_name" from the device memory. @param str sequence_name: The name of the sequence to be deleted Optionally a list of sequence names can be passed. @return list: a list of deleted sequence names. """ if isinstance(sequence_name, str): sequence_name = [sequence_name] deleted_sequences = list() for sequence in sequence_name: if sequence in self.sequence_dict: del self.sequence_dict[sequence] deleted_sequences.append(sequence) return deleted_sequences def load_waveform(self, load_dict): """ Loads a waveform to the specified channel of the pulsing device. For devices that have a workspace (i.e. AWG) this will load the waveform from the device workspace into the channel. For a device without mass memory this will make the waveform/pattern that has been previously written with self.write_waveform ready to play. @param load_dict: dict|list, a dictionary with keys being one of the available channel index and values being the name of the already written waveform to load into the channel. Examples: {1: rabi_ch1, 2: rabi_ch2} or {1: rabi_ch2, 2: rabi_ch1} If just a list of waveform names if given, the channel association will be invoked from the channel suffix '_ch1', '_ch2' etc. @return (dict, str): Dictionary with keys being the channel number and values being the respective asset loaded into the channel, string describing the asset type ('waveform' or 'sequence') """ if isinstance(load_dict, list): new_dict = dict() for waveform in load_dict: channel = int(waveform.rsplit('_ch', 1)[1]) new_dict[channel] = waveform load_dict = new_dict # Determine if the device is purely digital and get all active channels analog_channels = [ chnl for chnl in self.activation_config if chnl.startswith('a') ] digital_channels = [ chnl for chnl in self.activation_config if chnl.startswith('d') ] pure_digital = len(analog_channels) == 0 # Check if waveforms are present in virtual dummy device memory and specified channels are # active. Create new load dict. new_loaded_assets = dict() for channel, waveform in load_dict.items(): if waveform not in self.waveform_set: self.log.error( 'Loading failed. Waveform "{0}" not found on device memory.' ''.format(waveform)) return self.current_loaded_assets if pure_digital: if 'd_ch{0:d}'.format(channel) not in digital_channels: self.log.error( 'Loading failed. Digital channel {0:d} not active.' ''.format(channel)) return self.current_loaded_assets else: if 'a_ch{0:d}'.format(channel) not in analog_channels: self.log.error( 'Loading failed. Analog channel {0:d} not active.' ''.format(channel)) return self.current_loaded_assets new_loaded_assets[channel] = waveform self.current_loaded_assets = new_loaded_assets return self.get_loaded_assets() def load_sequence(self, sequence_name): """ Loads a sequence to the channels of the device in order to be ready for playback. For devices that have a workspace (i.e. AWG) this will load the sequence from the device workspace into the channels. @param sequence_name: str, name of the sequence to load @return (dict, str): Dictionary with keys being the channel number and values being the respective asset loaded into the channel, string describing the asset type ('waveform' or 'sequence') """ if sequence_name not in self.sequence_dict: self.log.error( 'Sequence loading failed. No sequence with name "{0}" found on device ' 'memory.'.format(sequence_name)) return self.get_loaded_assets() # Determine if the device is purely digital and get all active channels analog_channels = natural_sort(chnl for chnl in self.activation_config if chnl.startswith('a')) digital_channels = natural_sort(chnl for chnl in self.activation_config if chnl.startswith('d')) pure_digital = len(analog_channels) == 0 if pure_digital and len( digital_channels) != self.sequence_dict[sequence_name]: self.log.error( 'Sequence loading failed. Number of active digital channels ({0:d}) does' ' not match the number of tracks in the sequence ({1:d}).' ''.format(len(digital_channels), self.sequence_dict[sequence_name])) return self.get_loaded_assets() if not pure_digital and len( analog_channels) != self.sequence_dict[sequence_name]: self.log.error( 'Sequence loading failed. Number of active analog channels ({0:d}) does' ' not match the number of tracks in the sequence ({1:d}).' ''.format(len(analog_channels), self.sequence_dict[sequence_name])) return self.get_loaded_assets() new_loaded_assets = dict() if pure_digital: for track_index, chnl in enumerate(digital_channels): chnl_num = int(chnl.split('ch')[1]) new_loaded_assets[chnl_num] = '{0}_{1:d}'.format( sequence_name, track_index) else: for track_index, chnl in enumerate(analog_channels): chnl_num = int(chnl.split('ch')[1]) new_loaded_assets[chnl_num] = '{0}_{1:d}'.format( sequence_name, track_index) self.current_loaded_assets = new_loaded_assets return self.get_loaded_assets() def get_loaded_assets(self): """ Retrieve the currently loaded asset names for each active channel of the device. The returned dictionary will have the channel numbers as keys. In case of loaded waveforms the dictionary values will be the waveform names. In case of a loaded sequence the values will be the sequence name appended by a suffix representing the track loaded to the respective channel (i.e. '<sequence_name>_1'). @return (dict, str): Dictionary with keys being the channel number and values being the respective asset loaded into the channel, string describing the asset type ('waveform' or 'sequence') """ # Determine if it's a waveform or a sequence asset_type = None for asset_name in self.current_loaded_assets.values(): if 'ch' in asset_name.rsplit('_', 1)[1]: current_type = 'waveform' else: current_type = 'sequence' if asset_type is None or asset_type == current_type: asset_type = current_type else: self.log.error( 'Unable to determine loaded asset type. Mixed naming convention ' 'assets loaded (waveform and sequence tracks).') return dict(), '' return self.current_loaded_assets, asset_type def clear_all(self): """ Clears all loaded waveform from the pulse generators RAM. @return int: error code (0:OK, -1:error) Unused for digital pulse generators without storage capability (PulseBlaster, FPGA). """ self.current_loaded_assets = dict() self.waveform_set = set() self.sequence_dict = dict() return 0 def get_status(self): """ Retrieves the status of the pulsing hardware @return (int, dict): inter value of the current status with the corresponding dictionary containing status description for all the possible status variables of the pulse generator hardware """ status_dic = { -1: 'Failed Request or Communication', 0: 'Device has stopped, but can receive commands.', 1: 'Device is active and running.' } # All the other status messages should have higher integer values # then 1. return self.current_status, status_dic def get_sample_rate(self): """ Get the sample rate of the pulse generator hardware @return float: The current sample rate of the device (in Hz) Do not return a saved sample rate in a class variable, but instead retrieve the current sample rate directly from the device. """ return self.sample_rate def set_sample_rate(self, sample_rate): """ Set the sample rate of the pulse generator hardware @param float sample_rate: The sampling rate to be set (in Hz) @return float: the sample rate returned from the device. Note: After setting the sampling rate of the device, retrieve it again for obtaining the actual set value and use that information for further processing. """ constraint = self.get_constraints().sample_rate if sample_rate > constraint.max: self.sample_rate = constraint.max elif sample_rate < constraint.min: self.sample_rate = constraint.min else: self.sample_rate = sample_rate return self.sample_rate def get_analog_level(self, amplitude=None, offset=None): """ Retrieve the analog amplitude and offset of the provided channels. @param list amplitude: optional, if a specific amplitude value (in Volt peak to peak, i.e. the full amplitude) of a channel is desired. @param list offset: optional, if a specific high value (in Volt) of a channel is desired. @return dict: with keys being the generic string channel names and items being the values for those channels. Amplitude is always denoted in Volt-peak-to-peak and Offset in (absolute) Voltage. Note: Do not return a saved amplitude and/or offset value but instead retrieve the current amplitude and/or offset directly from the device. If no entries provided then the levels of all channels where simply returned. If no analog channels provided, return just an empty dict. Example of a possible input: amplitude = ['a_ch1','a_ch4'], offset =[1,3] to obtain the amplitude of channel 1 and 4 and the offset {'a_ch1': -0.5, 'a_ch4': 2.0} {'a_ch1': 0.0, 'a_ch3':-0.75} since no high request was performed. The major difference to digital signals is that analog signals are always oscillating or changing signals, otherwise you can use just digital output. In contrast to digital output levels, analog output levels are defined by an amplitude (here total signal span, denoted in Voltage peak to peak) and an offset (a value around which the signal oscillates, denoted by an (absolute) voltage). In general there is no bijective correspondence between (amplitude, offset) and (value high, value low)! """ if amplitude is None: amplitude = [] if offset is None: offset = [] ampl = dict() off = dict() if not amplitude and not offset: for a_ch, pp_amp in self.amplitude_dict.items(): ampl[a_ch] = pp_amp for a_ch, offset in self.offset_dict.items(): off[a_ch] = offset else: for a_ch in amplitude: ampl[a_ch] = self.amplitude_dict[a_ch] for a_ch in offset: off[a_ch] = self.offset_dict[a_ch] return ampl, off def set_analog_level(self, amplitude=None, offset=None): """ Set amplitude and/or offset value of the provided analog channel. @param dict amplitude: dictionary, with key being the channel and items being the amplitude values (in Volt peak to peak, i.e. the full amplitude) for the desired channel. @param dict offset: dictionary, with key being the channel and items being the offset values (in absolute volt) for the desired channel. @return (dict, dict): tuple of two dicts with the actual set values for amplitude and offset. If nothing is passed then the command will return two empty dicts. Note: After setting the analog and/or offset of the device, retrieve them again for obtaining the actual set value(s) and use that information for further processing. The major difference to digital signals is that analog signals are always oscillating or changing signals, otherwise you can use just digital output. In contrast to digital output levels, analog output levels are defined by an amplitude (here total signal span, denoted in Voltage peak to peak) and an offset (a value around which the signal oscillates, denoted by an (absolute) voltage). In general there is no bijective correspondence between (amplitude, offset) and (value high, value low)! """ if amplitude is None: amplitude = dict() if offset is None: offset = dict() for a_ch, amp in amplitude.items(): self.amplitude_dict[a_ch] = amp for a_ch, off in offset.items(): self.offset_dict[a_ch] = off return self.get_analog_level(amplitude=list(amplitude), offset=list(offset)) def get_digital_level(self, low=None, high=None): """ Retrieve the digital low and high level of the provided channels. @param list low: optional, if a specific low value (in Volt) of a channel is desired. @param list high: optional, if a specific high value (in Volt) of a channel is desired. @return: (dict, dict): tuple of two dicts, with keys being the channel number and items being the values for those channels. Both low and high value of a channel is denoted in (absolute) Voltage. Note: Do not return a saved low and/or high value but instead retrieve the current low and/or high value directly from the device. If no entries provided then the levels of all channels where simply returned. If no digital channels provided, return just an empty dict. Example of a possible input: low = ['d_ch1', 'd_ch4'] to obtain the low voltage values of digital channel 1 an 4. A possible answer might be {'d_ch1': -0.5, 'd_ch4': 2.0} {} since no high request was performed. The major difference to analog signals is that digital signals are either ON or OFF, whereas analog channels have a varying amplitude range. In contrast to analog output levels, digital output levels are defined by a voltage, which corresponds to the ON status and a voltage which corresponds to the OFF status (both denoted in (absolute) voltage) In general there is no bijective correspondence between (amplitude, offset) and (value high, value low)! """ if low is None: low = [] if high is None: high = [] if not low and not high: low_val = self.digital_low_dict high_val = self.digital_high_dict else: low_val = dict() high_val = dict() for d_ch in low: low_val[d_ch] = self.digital_low_dict[d_ch] for d_ch in high: high_val[d_ch] = self.digital_high_dict[d_ch] return low_val, high_val def set_digital_level(self, low=None, high=None): """ Set low and/or high value of the provided digital channel. @param dict low: dictionary, with key being the channel and items being the low values (in volt) for the desired channel. @param dict high: dictionary, with key being the channel and items being the high values (in volt) for the desired channel. @return (dict, dict): tuple of two dicts where first dict denotes the current low value and the second dict the high value. If nothing is passed then the command will return two empty dicts. Note: After setting the high and/or low values of the device, retrieve them again for obtaining the actual set value(s) and use that information for further processing. The major difference to analog signals is that digital signals are either ON or OFF, whereas analog channels have a varying amplitude range. In contrast to analog output levels, digital output levels are defined by a voltage, which corresponds to the ON status and a voltage which corresponds to the OFF status (both denoted in (absolute) voltage) In general there is no bijective correspondence between (amplitude, offset) and (value high, value low)! """ if low is None: low = dict() if high is None: high = dict() for d_ch, low_voltage in low.items(): self.digital_low_dict[d_ch] = low_voltage for d_ch, high_voltage in high.items(): self.digital_high_dict[d_ch] = high_voltage return self.get_digital_level(low=list(low), high=list(high)) def get_active_channels(self, ch=None): """ Get the active channels of the pulse generator hardware. @param list ch: optional, if specific analog or digital channels are needed to be asked without obtaining all the channels. @return dict: where keys denoting the channel number and items boolean expressions whether channel are active or not. Example for an possible input (order is not important): ch = ['a_ch2', 'd_ch2', 'a_ch1', 'd_ch5', 'd_ch1'] then the output might look like {'a_ch2': True, 'd_ch2': False, 'a_ch1': False, 'd_ch5': True, 'd_ch1': False} If no parameters are passed to this method all channels will be asked for their setting. """ if ch is None: ch = [] active_ch = {} if not ch: active_ch = self.channel_states else: for channel in ch: active_ch[channel] = self.channel_states[channel] return active_ch def set_active_channels(self, ch=None): """ Set the active/inactive channels for the pulse generator hardware. The state of ALL available analog and digital channels will be returned (True: active, False: inactive). The actually set and returned channel activation must be part of the available activation_configs in the constraints. You can also activate/deactivate subsets of available channels but the resulting activation_config must still be valid according to the constraints. If the resulting set of active channels can not be found in the available activation_configs, the channel states must remain unchanged. @param dict ch: dictionary with keys being the analog or digital string generic names for the channels (i.e. 'd_ch1', 'a_ch2') with items being a boolean value. True: Activate channel, False: Deactivate channel @return dict: with the actual set values for ALL active analog and digital channels If nothing is passed then the command will simply return the unchanged current state. Note: After setting the active channels of the device, use the returned dict for further processing. Example for possible input: ch={'a_ch2': True, 'd_ch1': False, 'd_ch3': True, 'd_ch4': True} to activate analog channel 2 digital channel 3 and 4 and to deactivate digital channel 1. All other available channels will remain unchanged. """ if ch is None: ch = {} old_activation = self.channel_states.copy() for channel in ch: self.channel_states[channel] = ch[channel] active_channel_set = { chnl for chnl, is_active in self.channel_states.items() if is_active } if active_channel_set not in self.get_constraints( ).activation_config.values(): self.log.error( 'Channel activation to be set not found in constraints.\n' 'Channel activation unchanged.') self.channel_states = old_activation else: self.activation_config = active_channel_set return self.get_active_channels(ch=list(ch)) def get_interleave(self): """ Check whether Interleave is ON or OFF in AWG. @return bool: True: ON, False: OFF Unused for pulse generator hardware other than an AWG. """ return self.interleave def set_interleave(self, state=False): """ Turns the interleave of an AWG on or off. @param bool state: The state the interleave should be set to (True: ON, False: OFF) @return bool: actual interleave status (True: ON, False: OFF) Note: After setting the interleave of the device, retrieve the interleave again and use that information for further processing. Unused for pulse generator hardware other than an AWG. """ self.interleave = state return self.get_interleave() def write(self, command): """ Sends a command string to the device. @param string command: string containing the command @return int: error code (0:OK, -1:error) """ self.log.info( 'It is so nice that you talk to me and told me "{0}"; ' 'as a dummy it is very dull out here! :) '.format(command)) return 0 def query(self, question): """ Asks the device a 'question' and receive and return an answer from it. @param string question: string containing the command @return string: the answer of the device to the 'question' in a string """ self.log.info('Dude, I\'m a dummy! Your question \'{0}\' is way too ' 'complicated for me :D !'.format(question)) return 'I am a dummy!' def reset(self): """ Reset the device. @return int: error code (0:OK, -1:error) """ self.__init__() self.connected = True self.log.info('Dummy reset!') return 0
class PulseAnalysisLogic(GenericLogic): """unstable: Nikolas Tomek """ _modclass = 'PulseAnalysisLogic' _modtype = 'logic' # status vars current_method = StatusVar('current_method', 'mean_norm') signal_start_bin = StatusVar('signal_start_bin', 0) signal_end_bin = StatusVar('signal_end_bin', 200) norm_start_bin = StatusVar('norm_start_bin', 300) norm_end_bin = StatusVar('norm_end_bin', 400) def __init__(self, config, **kwargs): super().__init__(config=config, **kwargs) def on_activate(self): """ Initialisation performed during activation of the module. """ self.analysis_methods = OrderedDict() filename_list = [] # The assumption is that in the directory pulsed_analysis_methods, there are # *.py files, which contain only methods! path = os.path.join(self.get_main_dir(), 'logic', 'pulsed_analysis_methods') for entry in os.listdir(path): if os.path.isfile(os.path.join(path, entry)) and entry.endswith('.py'): filename_list.append(entry[:-3]) for filename in filename_list: mod = importlib.import_module( 'logic.pulsed_analysis_methods.{0}'.format(filename)) for method in dir(mod): try: # Check for callable function or method: ref = getattr(mod, method) if callable(ref) and (inspect.ismethod(ref) or inspect.isfunction(ref)): # Bind the method as an attribute to the Class setattr(PulseAnalysisLogic, method, getattr(mod, method)) # Add method to dictionary if it is a generator method if method.startswith('analyse_'): self.analysis_methods[method[8:]] = eval('self.' + method) except: self.log.error( 'It was not possible to import element {0} from {1} into ' 'PulseAnalysisLogic.'.format(method, filename)) return def on_deactivate(self): """ Deinitialisation performed during deactivation of the module. """ return def analyze_data(self, laser_data): """ Analysis the laser pulses and computes the measuring error given by photon shot noise @param numpy.ndarray (int) laser_data: 2D array containing the extracted laser countdata @return: float array signal_data: Array with the computed signal @return: float array measuring_error: Array with the computed signal error """ signal_data, measuring_error = self.analysis_methods[ self.current_method](laser_data) return signal_data, measuring_error
class SoftPIDController(GenericLogic, PIDControllerInterface): """ Control a process via software PID. """ _modclass = 'pidlogic' _modtype = 'logic' ## declare connectors process = Connector(interface='ProcessInterface') control = Connector(interface='ProcessControlInterface') # config opt timestep = ConfigOption(default=100) # status vars kP = StatusVar(default=1) kI = StatusVar(default=1) kD = StatusVar(default=1) setpoint = StatusVar(default=273.15) manualvalue = StatusVar(default=0) sigNewValue = QtCore.Signal(float) def __init__(self, config, **kwargs): super().__init__(config=config, **kwargs) self.log.debug('The following configuration was found.') # checking for the right configuration for key in config.keys(): self.log.debug('{0}: {1}'.format(key, config[key])) #number of lines in the matrix plot self.NumberOfSecondsLog = 100 self.threadlock = Mutex() def on_activate(self): """ Initialisation performed during activation of the module. """ self._process = self.get_connector('process') self._control = self.get_connector('control') self.previousdelta = 0 self.cv = self._control.getControlValue() self.timer = QtCore.QTimer() self.timer.setSingleShot(True) self.timer.setInterval(self.timestep) self.timer.timeout.connect(self._calcNextStep, QtCore.Qt.QueuedConnection) self.sigNewValue.connect(self._control.setControlValue) self.history = np.zeros([3, 5]) self.savingState = False self.enable = False self.integrated = 0 self.countdown = 2 self.timer.start(self.timestep) def on_deactivate(self): """ Perform required deactivation. """ pass def _calcNextStep(self): """ This function implements the Takahashi Type C PID controller: the P and D term are no longer dependent on the set-point, only on PV (which is Thlt). The D term is NOT low-pass filtered. This function should be called once every TS seconds. """ self.pv = self._process.getProcessValue() if self.countdown > 0: self.countdown -= 1 self.previousdelta = self.setpoint - self.pv print('Countdown: ', self.countdown) elif self.countdown == 0: self.countdown = -1 self.integrated = 0 self.enable = True if (self.enable): delta = self.setpoint - self.pv self.integrated += delta ## Calculate PID controller: self.P = self.kP * delta self.I = self.kI * self.timestep * self.integrated self.D = self.kD / self.timestep * (delta - self.previousdelta) self.cv += self.P + self.I + self.D self.previousdelta = delta ## limit contol output to maximum permissible limits limits = self._control.getControlLimits() if (self.cv > limits[1]): self.cv = limits[1] if (self.cv < limits[0]): self.cv = limits[0] self.history = np.roll(self.history, -1, axis=1) self.history[0, -1] = self.pv self.history[1, -1] = self.cv self.history[2, -1] = self.setpoint self.sigNewValue.emit(self.cv) else: self.cv = self.manualvalue limits = self._control.getControlLimits() if (self.cv > limits[1]): self.cv = limits[1] if (self.cv < limits[0]): self.cv = limits[0] self.sigNewValue.emit(self.cv) self.timer.start(self.timestep) def startLoop(self): """ Start the control loop. """ self.countdown = 2 def stopLoop(self): """ Stop the control loop. """ self.countdown = -1 self.enable = False def getSavingState(self): """ Find out if we are keeping data for saving later. @return bool: whether module is saving process and control data """ return self.savingState def startSaving(self): """ Start saving process and control data. Does not do anything right now. """ pass def saveData(self): """ Write process and control data to file. Does not do anything right now. """ pass def get_kp(self): """ Return the proportional constant. @return float: proportional constant of PID controller """ return self.kP def set_kp(self, kp): """ Set the proportional constant of the PID controller. @prarm float kp: proportional constant of PID controller """ self.kP = kp def get_ki(self): """ Get the integration constant of the PID controller @return float: integration constant of the PID controller """ return self.kI def set_ki(self, ki): """ Set the integration constant of the PID controller. @param float ki: integration constant of the PID controller """ self.kI = ki def get_kd(self): """ Get the derivative constant of the PID controller @return float: the derivative constant of the PID controller """ return self.kD def set_kd(self, kd): """ Set the derivative constant of the PID controller @param float kd: the derivative constant of the PID controller """ self.kD = kd def get_setpoint(self): """ Get the current setpoint of the PID controller. @return float: current set point of the PID controller """ return self.setpoint def set_setpoint(self, setpoint): """ Set the current setpoint of the PID controller. @param float setpoint: new set point of the PID controller """ self.setpoint = setpoint def get_manual_value(self): """ Return the control value for manual mode. @return float: control value for manual mode """ return self.manualvalue def set_manual_value(self, manualvalue): """ Set the control value for manual mode. @param float manualvalue: control value for manual mode of controller """ self.manualvalue = manualvalue limits = self._control.getControlLimits() if (self.manualvalue > limits[1]): self.manualvalue = limits[1] if (self.manualvalue < limits[0]): self.manualvalue = limits[0] def get_enabled(self): """ See if the PID controller is controlling a process. @return bool: whether the PID controller is preparing to or conreolling a process """ return self.enable or self.countdown >= 0 def set_enabled(self, enabled): """ Set the state of the PID controller. @param bool enabled: desired state of PID controller """ if enabled and not self.enable and self.countdown == -1: self.startLoop() if not enabled and self.enable: self.stopLoop() def get_control_limits(self): """ Get the minimum and maximum value of the control actuator. @return list(float): (minimum, maximum) values of the control actuator """ return self._control.getControlLimits() def set_control_limits(self, limits): """ Set the minimum and maximum value of the control actuator. @param list(float) limits: (minimum, maximum) values of the control actuator This function does nothing, control limits are handled by the control module """ pass def get_control_value(self): """ Get current control output value. @return float: control output value """ return self.cv def get_process_value(self): """ Get current process input value. @return float: current process input value """ return self.pv def get_extra(self): """ Extra information about the controller state. @return dict: extra informatin about internal controller state Do not depend on the output of this function, not every field exists for every PID controller. """ return {'P': self.P, 'I': self.I, 'D': self.D}
class PulseExtractionLogic(GenericLogic): """ """ _modclass = 'PulseExtractionLogic' _modtype = 'logic' extraction_settings = StatusVar('extraction_settings', default={'conv_std_dev': 10.0, 'count_threshold': 10, 'threshold_tolerance': 20e-9, 'min_laser_length': 200e-9, 'current_method': 'conv_deriv'}) def __init__(self, config, **kwargs): super().__init__(config=config, **kwargs) self.log.debug('The following configuration was found.') # checking for the right configuration for key in config.keys(): self.log.debug('{0}: {1}'.format(key, config[key])) self.number_of_lasers = 50 def on_activate(self): """ Initialisation performed during activation of the module. """ self.gated_extraction_methods = OrderedDict() self.ungated_extraction_methods = OrderedDict() self.extraction_methods = OrderedDict() filename_list = [] # The assumption is that in the directory pulse_extraction_methods, there are # *.py files, which contain only methods! path = os.path.join(get_main_dir(), 'logic', 'pulse_extraction_methods') for entry in os.listdir(path): if os.path.isfile(os.path.join(path, entry)) and entry.endswith('.py'): filename_list.append(entry[:-3]) for filename in filename_list: mod = importlib.import_module('logic.pulse_extraction_methods.{0}'.format(filename)) for method in dir(mod): try: # Check for callable function or method: ref = getattr(mod, method) if callable(ref) and (inspect.ismethod(ref) or inspect.isfunction(ref)): # Bind the method as an attribute to the Class setattr(PulseExtractionLogic, method, getattr(mod, method)) # Add method to dictionary if it is an extraction method if method.startswith('gated_'): self.gated_extraction_methods[method[6:]] = eval('self.' + method) self.extraction_methods[method[6:]] = eval('self.' + method) elif method.startswith('ungated_'): self.ungated_extraction_methods[method[8:]] = eval('self.' + method) self.extraction_methods[method[8:]] = eval('self.' + method) except: self.log.error('It was not possible to import element {0} from {1} into ' 'PulseExtractionLogic.'.format(method, filename)) return def on_deactivate(self): """ Deinitialisation performed during deactivation of the module. """ return def extract_laser_pulses(self, count_data, is_gated=False): """ @param count_data: @param is_gated: @return: """ # convert time to bin self.threshold_tolerance_bin = int(self.extraction_settings['threshold_tolerance']/self.fast_counter_binwidth+1) self.min_laser_length_bin = int(self.extraction_settings['min_laser_length'] / self.fast_counter_binwidth + 1) self.log.debug('Minimum laser length in bins: {0}'.format(self.min_laser_length_bin)) self.log.debug('Threshold tolerance in bins: {0}'.format(self.threshold_tolerance_bin)) if is_gated: return_dict = self.gated_extraction_methods[self.extraction_settings['current_method']](count_data) else: return_dict = self.ungated_extraction_methods[self.extraction_settings['current_method']](count_data) return return_dict
class NuclearOperationsLogic(GenericLogic): """ A higher order logic, which combines several lower class logic modules in order to perform measurements and manipulations of nuclear spins. DISCLAIMER: =========== This module has two major issues: - a lack of proper documentation of all the methods - usage of tasks is not implemented and therefore direct connection to all the modules is used (I tried to compress as good as possible all the part, where access to other modules occurs so that a later replacement would be easier and one does not have to search throughout the whole file.) The state of this module is considered to be UNSTABLE. I am currently working on that and will from time to time improve the status of this module. So if you want to use it, be aware that there might appear drastic changes. --- Alexander Stark """ _modclass = 'NuclearOperationsLogic' _modtype = 'logic' # declare connectors #TODO: Use rather the task runner instead directly the module! sequencegenerationlogic = Connector(interface='SequenceGenerationLogic') traceanalysislogic = Connector(interface='TraceAnalysisLogic') gatedcounterlogic = Connector(interface='CounterLogic') odmrlogic = Connector(interface='ODMRLogic') optimizerlogic = Connector(interface='OptimizerLogic') scannerlogic = Connector(interface='ScannerLogic') savelogic = Connector(interface='SaveLogic') # status vars electron_rabi_periode = StatusVar('electron_rabi_periode', 1800e-9) # in s # pulser microwave: pulser_mw_freq = StatusVar('pulser_mw_freq', 200e6) # in Hz pulser_mw_amp = StatusVar('pulser_mw_amp', 2.25) # in V pulser_mw_ch = StatusVar('pulser_mw_ch', -1) # pulser rf: nuclear_rabi_period0 = StatusVar('nuclear_rabi_period0', 30e-6) # in s pulser_rf_freq0 = StatusVar('pulser_rf_freq0', 6.32e6) # in Hz pulser_rf_amp0 = StatusVar('pulser_rf_amp0', 0.1) nuclear_rabi_period1 = StatusVar('nuclear_rabi_period1', 30e-6) # in s pulser_rf_freq1 = StatusVar('pulser_rf_freq1', 3.24e6) # in Hz pulser_rf_amp1 = StatusVar('pulser_rf_amp1', 0.1) pulser_rf_ch = StatusVar('pulser_rf_ch', -2) # laser options: pulser_laser_length = StatusVar('pulser_laser_length', 3e-6) # in s pulser_laser_amp = StatusVar('pulser_laser_amp', 1) # in V pulser_laser_ch = StatusVar('pulser_laser_ch', 1) num_singleshot_readout = StatusVar('num_singleshot_readout', 3000) pulser_idle_time = StatusVar('pulser_idle_time', 1.5e-6) # in s # detection gated counter: pulser_detect_ch = StatusVar('pulser_detect_ch', 1) # measurement parameters: current_meas_asset_name = StatusVar('current_meas_asset_name', '') x_axis_start = StatusVar('x_axis_start', 1e-3) # in s x_axis_step = StatusVar('x_axis_step', 10e-3) # in s x_axis_num_points = StatusVar('x_axis_num_points', 50) # How often the measurement should be repeated. num_of_meas_runs = StatusVar('num_of_meas_runs', 1) # parameters for confocal and odmr optimization: optimize_period_odmr = StatusVar('optimize_period_odmr', 200) optimize_period_confocal = StatusVar('optimize_period_confocal', 300) # in s odmr_meas_freq0 = StatusVar('odmr_meas_freq0', 10000e6) # in Hz odmr_meas_freq1 = StatusVar('odmr_meas_freq1', 10002.1e6) # in Hz odmr_meas_freq2 = StatusVar('odmr_meas_freq2', 10004.2e6) # in Hz odmr_meas_runtime = StatusVar('odmr_meas_runtime', 30) # in s odmr_meas_freq_range = StatusVar('odmr_meas_freq_range', 30e6) # in Hz odmr_meas_step = StatusVar('odmr_meas_step', 0.15e6) # in Hz odmr_meas_power = StatusVar('odmr_meas_power', -30) # in dBm # Microwave measurment parameters: mw_cw_freq = StatusVar('mw_cw_freq', 10e9) # in Hz mw_cw_power = StatusVar('mw_cw_power', -30) # in dBm # on which odmr peak the manipulation is going to be applied: mw_on_odmr_peak = StatusVar('mw_on_odmr_peak', 1) # Gated counter: gc_number_of_samples = StatusVar('gc_number_of_samples', 3000) # in counts gc_samples_per_readout = StatusVar('gc_samples_per_readout', 10) # in counts # signals sigNextMeasPoint = QtCore.Signal() sigCurrMeasPointUpdated = QtCore.Signal() sigMeasurementStopped = QtCore.Signal() sigMeasStarted = QtCore.Signal() def __init__(self, config, **kwargs): super().__init__(config=config, **kwargs) self.log.debug('The following configuration was found.') # checking for the right configuration for key in config.keys(): self.log.info('{0}: {1}'.format(key, config[key])) self.threadlock = Mutex() def on_activate(self): """ Initialisation performed during activation of the module. """ # establish the access to all connectors: self._save_logic = self.get_connector('savelogic') #FIXME: THAT IS JUST A TEMPORARY SOLUTION! Implement the access on the # needed methods via the TaskRunner! self._seq_gen_logic = self.get_connector('sequencegenerationlogic') self._trace_ana_logic = self.get_connector('traceanalysislogic') self._gc_logic = self.get_connector('gatedcounterlogic') self._odmr_logic = self.get_connector('odmrlogic') self._optimizer_logic = self.get_connector('optimizerlogic') self._confocal_logic = self.get_connector('scannerlogic') # current measurement information: self.current_meas_point = self.x_axis_start self.current_meas_index = 0 self.num_of_current_meas_runs = 0 self.elapsed_time = 0 self.start_time = datetime.datetime.now() self.next_optimize_time = self.start_time # store here all the measured odmr peaks self.measured_odmr_list = [] self._optimize_now = False self._stop_requested = False # store here all the measured odmr peaks self.measured_odmr_list = [] # Perform initialization routines: self.initialize_x_axis() self.initialize_y_axis() self.initialize_meas_param() # connect signals: self.sigNextMeasPoint.connect(self._meas_point_loop, QtCore.Qt.QueuedConnection) def on_deactivate(self): """ Deactivate the module properly. """ return def initialize_x_axis(self): """ Initialize the x axis. """ stop = self.x_axis_start + self.x_axis_step * self.x_axis_num_points self.x_axis_list = np.arange(self.x_axis_start, stop + (self.x_axis_step / 2), self.x_axis_step) self.current_meas_point = self.x_axis_start def initialize_y_axis(self): """ Initialize the y axis. """ self.y_axis_list = np.zeros( self.x_axis_list.shape) # y axis where current data are stored self.y_axis_fit_list = np.zeros( self.x_axis_list.shape) # y axis where fit is stored. # here all consequutive measurements are saved, where the # self.num_of_meas_runs determines the measurement index for the row. self.y_axis_matrix = np.zeros((1, len(self.x_axis_list))) # here all the measurement parameters per measurement point are stored: self.parameter_matrix = np.zeros((1, len(self.x_axis_list)), dtype=object) def initialize_meas_param(self): """ Initialize the measurement param containter. """ # here all measurement parameters will be included for any kind of # nuclear measurement. self._meas_param = OrderedDict() def start_nuclear_meas(self, continue_meas=False): """ Start the nuclear operation measurement. """ self._stop_requested = False if not continue_meas: # prepare here everything for a measurement and go to the measurement # loop. self.prepare_measurement_protocols(self.current_meas_asset_name) self.initialize_x_axis() self.initialize_y_axis() self.current_meas_index = 0 self.sigCurrMeasPointUpdated.emit() self.num_of_current_meas_runs = 0 self.measured_odmr_list = [] self.elapsed_time = 0 self.start_time = datetime.datetime.now() self.next_optimize_time = 0 # load the measurement sequence: self._load_measurement_seq(self.current_meas_asset_name) self._pulser_on() self.set_mw_on_odmr_freq(self.mw_cw_freq, self.mw_cw_power) self.mw_on() self.lock() self.sigMeasStarted.emit() self.sigNextMeasPoint.emit() def _meas_point_loop(self): """ Run this loop continuously until the an abort criterium is reached. """ if self._stop_requested: with self.threadlock: # end measurement and switch all devices off self.stopRequested = False self.unlock() self.mw_off() self._pulser_off() # emit all needed signals for the update: self.sigCurrMeasPointUpdated.emit() self.sigMeasurementStopped.emit() return # if self._optimize_now: self.elapsed_time = (datetime.datetime.now() - self.start_time).total_seconds() if self.next_optimize_time < self.elapsed_time: current_meas_asset = self.current_meas_asset_name self.mw_off() # perform optimize position: self._load_laser_on() self._pulser_on() self.do_optimize_pos() # perform odmr measurement: self._load_pulsed_odmr() self._pulser_on() self.do_optimize_odmr_freq() # use the new measured frequencies for the microwave: if self.mw_on_odmr_peak == 1: self.mw_cw_freq = self.odmr_meas_freq0 elif self.mw_on_odmr_peak == 2: self.mw_cw_freq = self.odmr_meas_freq1 elif self.mw_on_odmr_peak == 3: self.mw_cw_freq = self.odmr_meas_freq2 else: self.log.error( 'The maximum number of odmr can only be 3, ' 'therfore only the peaks with number 0, 1 or 2 can ' 'be selected but an number of "{0}" was set. ' 'Measurement stopped!'.format(self.mw_on_odmr_peak)) self.stop_nuclear_meas() self.sigNextMeasPoint.emit() return self.set_mw_on_odmr_freq(self.mw_cw_freq, self.mw_cw_power) # establish the previous measurement conditions self.mw_on() self._load_measurement_seq(current_meas_asset) self._pulser_on() self.elapsed_time = (datetime.datetime.now() - self.start_time).total_seconds() self.next_optimize_time = self.elapsed_time + self.optimize_period_odmr # if stop request was done already here, do not perform the current # measurement but jump to the switch off procedure at the top of this # method. if self._stop_requested: self.sigNextMeasPoint.emit() return # this routine will return a desired measurement value and the # measurement parameters, which belong to it. curr_meas_points, meas_param = self._get_meas_point( self.current_meas_asset_name) # this routine will handle the saving and storing of the measurement # results: self._set_meas_point(num_of_meas_runs=self.num_of_current_meas_runs, meas_index=self.current_meas_index, meas_points=curr_meas_points, meas_param=meas_param) if self._stop_requested: self.sigNextMeasPoint.emit() return # increment the measurement index or set it back to zero if it exceed # the maximal number of x axis measurement points. The measurement index # will be used for the next measurement if self.current_meas_index + 1 >= len(self.x_axis_list): self.current_meas_index = 0 # If the next measurement run begins, add a new matrix line to the # self.y_axis_matrix self.num_of_current_meas_runs += 1 new_row = np.zeros(len(self.x_axis_list)) # that vertical stack command behaves similar to the append method # in python lists, where the new_row will be appended to the matrix: self.y_axis_matrix = np.vstack((self.y_axis_matrix, new_row)) self.parameter_matrix = np.vstack((self.parameter_matrix, new_row)) else: self.current_meas_index += 1 # check if measurement is at the end, and if not, adjust the measurement # sequence to the next measurement point. if self.num_of_current_meas_runs < self.num_of_meas_runs: # take the next measurement index from the x axis as the current # measurement point: self.current_meas_point = self.x_axis_list[self.current_meas_index] # adjust the measurement protocol with the new current_meas_point self.adjust_measurement(self.current_meas_asset_name) self._load_measurement_seq(self.current_meas_asset_name) else: self.stop_nuclear_meas() self.sigNextMeasPoint.emit() def _set_meas_point(self, num_of_meas_runs, meas_index, meas_points, meas_param): """ Handle the proper setting of the current meas_point and store all the additional measurement parameter. @param int meas_index: @param int num_of_meas_runs @param float meas_points: @param meas_param: @return: """ # one matrix contains all the measured values, the other one contains # all the parameters for the specified measurement point: self.y_axis_matrix[num_of_meas_runs, meas_index] = meas_points self.parameter_matrix[num_of_meas_runs, meas_index] = meas_param # the y_axis_list contains the summed and averaged values for each # measurement index: self.y_axis_list[meas_index] = self.y_axis_matrix[:, meas_index].mean() self.sigCurrMeasPointUpdated.emit() def _get_meas_point(self, meas_type): """ Start the actual measurement (most probably with the gated counter) And perform the measurement with that routine. @return tuple (float, dict): """ # save also the count trace of the gated counter after the measurement. # here the actual measurement is going to be started and stoped and # then analyzed and outputted in a proper format. # Check whether proper mode is active and if not activated that: if self._gc_logic.get_counting_mode() != 'finite-gated': self._gc_logic.set_counting_mode(mode='finite-gated') self._gc_logic.set_count_length(self.gc_number_of_samples) self._gc_logic.set_counting_samples(self.gc_samples_per_readout) self._gc_logic.startCount() time.sleep(2) # wait until the gated counter is done or available to start: while self._gc_logic.getState() != 'idle' and not self._stop_requested: # print('in SSR measure') time.sleep(1) # for safety reasons, stop also the counter if it is still running: # self._gc_logic.stopCount() name_tag = '{0}_{1}'.format(self.current_meas_asset_name, self.current_meas_point) self._gc_logic.save_current_count_trace(name_tag=name_tag) if meas_type in ['Nuclear_Rabi', 'Nuclear_Frequency_Scan']: entry_indices = np.where(self._gc_logic.countdata > 50) trunc_countdata = self._gc_logic.countdata[entry_indices] flip_prop, param = self._trace_ana_logic.analyze_flip_prob( trunc_countdata) elif meas_type in [ 'QSD_-_Artificial_Drive', 'QSD_-_SWAP_FID', 'QSD_-_Entanglement_FID' ]: # do something measurement specific pass return flip_prop, param def stop_nuclear_meas(self): """ Stop the Nuclear Operation Measurement. @return int: error code (0:OK, -1:error) """ with self.threadlock: if self.getState() == 'locked': self._stop_requested = True return 0 def get_fit_functions(self): """ Returns all fit methods, which are currently implemented for that module. @return list: with string entries denoting the names of the fit. """ return [ 'No Fit', 'pos. Lorentzian', 'neg. Lorentzian', 'pos. Gaussian' ] def do_fit(self, fit_function=None): """ Performs the chosen fit on the measured data. @param string fit_function: name of the chosen fit function @return dict: a dictionary with the relevant fit parameters, i.e. the result of the fit """ #TODO: implement the fit. pass def get_meas_type_list(self): return [ 'Nuclear_Rabi', 'Nuclear_Frequency_Scan', 'QSD_-_Artificial_Drive', 'QSD_-_SWAP_FID', 'QSD_-_Entanglement_FID' ] def get_available_odmr_peaks(self): """ Retrieve the information on which odmr peak the microwave can be applied. @return list: with string entries denoting the peak number """ return [1, 2, 3] def prepare_measurement_protocols(self, meas_type): """ Prepare and create all measurement protocols for the specified measurement type @param str meas_type: a measurement type from the list get_meas_type_list """ self._create_laser_on() self._create_pulsed_odmr() #FIXME: Move this creation routine to the tasks! if meas_type == 'Nuclear_Rabi': # generate: self._seq_gen_logic.generate_nuclear_meas_seq( name=meas_type, rf_length_ns=self.current_meas_point * 1e9, rf_freq_MHz=self.pulser_rf_freq0 / 1e6, rf_amp_V=self.pulser_rf_amp0, rf_channel=self.pulser_rf_ch, mw_freq_MHz=self.pulser_mw_freq / 1e6, mw_amp_V=self.pulser_mw_amp, mw_rabi_period_ns=self.electron_rabi_periode * 1e9, mw_channel=self.pulser_mw_ch, laser_time_ns=self.pulser_laser_length * 1e9, laser_channel=self.pulser_laser_ch, laser_amp_V=self.pulser_laser_amp, detect_channel=self.pulser_detect_ch, wait_time_ns=self.pulser_idle_time * 1e9, num_singleshot_readout=self.num_singleshot_readout) # sample: self._seq_gen_logic.sample_pulse_sequence(sequence_name=meas_type, write_to_file=True, chunkwise=False) # upload: self._seq_gen_logic.upload_sequence(seq_name=meas_type) elif meas_type == 'Nuclear_Frequency_Scan': # generate: self._seq_gen_logic.generate_nuclear_meas_seq( name=meas_type, rf_length_ns=(self.nuclear_rabi_period0 * 1e9) / 2, rf_freq_MHz=self.current_meas_point / 1e6, rf_amp_V=self.pulser_rf_amp0, rf_channel=self.pulser_rf_ch, mw_freq_MHz=self.pulser_mw_freq / 1e6, mw_amp_V=self.pulser_mw_amp, mw_rabi_period_ns=self.electron_rabi_periode * 1e9, mw_channel=self.pulser_mw_ch, laser_time_ns=self.pulser_laser_length * 1e9, laser_channel=self.pulser_laser_ch, laser_amp_V=self.pulser_laser_amp, detect_channel=self.pulser_detect_ch, wait_time_ns=self.pulser_idle_time * 1e9, num_singleshot_readout=self.num_singleshot_readout) # sample: self._seq_gen_logic.sample_pulse_sequence(sequence_name=meas_type, write_to_file=True, chunkwise=False) # upload: self._seq_gen_logic.upload_sequence(seq_name=meas_type) elif meas_type == 'QSD_-_Artificial_Drive': pass elif meas_type == 'QSD_-_SWAP_FID': pass elif meas_type == 'QSD_-_Entanglement_FID': pass def adjust_measurement(self, meas_type): """ Adjust the measurement sequence for the next measurement point. @param meas_type: @return: """ if meas_type == 'Nuclear_Rabi': # only the rf asset has to be regenerated since that is the only # thing what has changed. # You just have to ensure that the RF pulse in the sequence # Nuclear_Rabi is called exactly like this RF pulse: # generate the new pulse (which will overwrite the Ensemble) self._seq_gen_logic.generate_rf_pulse_ens( name='RF_pulse', rf_length_ns=(self.current_meas_point * 1e9) / 2, rf_freq_MHz=self.pulser_rf_freq0 / 1e6, rf_amp_V=self.pulser_rf_amp0, rf_channel=self.pulser_rf_ch) # sample the ensemble (and maybe save it to file, which will # overwrite the old one): self._seq_gen_logic.sample_pulse_block_ensemble( ensemble_name='RF_pulse', write_to_file=True, chunkwise=False) # upload the new sampled file to the device: self._seq_gen_logic.upload_asset(asset_name='RF_pulse') elif meas_type == 'Nuclear_Frequency_Scan': # generate the new pulse (which will overwrite the Ensemble) self._seq_gen_logic.generate_rf_pulse_ens( name='RF_pulse', rf_length_ns=(self.nuclear_rabi_period0 * 1e9) / 2, rf_freq_MHz=self.current_meas_point / 1e6, rf_amp_V=self.pulser_rf_amp0, rf_channel=self.pulser_rf_ch) # sample the ensemble (and maybe save it to file, which will # overwrite the old one): self._seq_gen_logic.sample_pulse_block_ensemble( ensemble_name='RF_pulse', write_to_file=True, chunkwise=False) # upload the new sampled file to the device: self._seq_gen_logic.upload_asset(asset_name='RF_pulse') elif meas_type == 'QSD_-_Artificial Drive': pass elif meas_type == 'QSD_-_SWAP_FID': pass elif meas_type == 'QSD_-_Entanglement_FID': pass def _load_measurement_seq(self, meas_seq): """ Load the current measurement sequence in the pulser @param str meas_seq: the measurement sequence which should be loaded into the device. @return: """ # now load the measurement sequence again on the device, which will # load the uploaded pulse instead of the old one: self._seq_gen_logic.load_asset(asset_name=meas_seq) def _create_laser_on(self): """ Create the laser asset. @return: """ #FIXME: Move this creation routine to the tasks! # generate: self._seq_gen_logic.generate_laser_on( name='Laser_On', laser_time_bins=3000, laser_channel=self.pulser_laser_ch) # sample: self._seq_gen_logic.sample_pulse_block_ensemble( ensemble_name='Laser_On', write_to_file=True, chunkwise=False) # upload: self._seq_gen_logic.upload_asset(asset_name='Laser_On') def _load_laser_on(self): """ Load the laser on asset into the pulser. @return: """ #FIXME: Move this creation routine to the tasks! self._seq_gen_logic.load_asset(asset_name='Laser_On') def _pulser_on(self): """ Switch on the pulser output. """ self._set_channel_activation(active=True, apply_to_device=True) self._seq_gen_logic.pulser_on() def _pulser_off(self): """ Switch off the pulser output. """ self._set_channel_activation(active=False, apply_to_device=False) self._seq_gen_logic.pulser_off() def _set_channel_activation(self, active=True, apply_to_device=False): """ Set the channels according to the current activation config to be either active or not. @param bool active: the activation according to the current activation config will be checked and if channel is not active and active=True, then channel will be activated. Otherwise if channel is active and active=False channel will be deactivated. All other channels, which are not in activation config will be deactivated if they are not already deactivated. @param bool apply_to_device: Apply the activation or deactivation of the current activation_config either to the device and the viewboxes, or just to the viewboxes. """ pulser_const = self._seq_gen_logic.get_hardware_constraints() curr_config_name = self._seq_gen_logic.current_activation_config_name activation_config = pulser_const['activation_config'][curr_config_name] # here is the current activation pattern of the pulse device: active_ch = self._seq_gen_logic.get_active_channels() ch_to_change = { } # create something like a_ch = {1:True, 2:True} to switch # check whether the correct channels are already active, and if not # correct for that and activate and deactivate the appropriate ones: available_ch = self._get_available_ch() for ch_name in available_ch: # if the channel is in the activation, check whether it is active: if ch_name in activation_config: if apply_to_device: # if channel is not active but activation is needed (active=True), # then add that to ch_to_change to change the state of the channels: if not active_ch[ch_name] and active: ch_to_change[ch_name] = active # if channel is active but deactivation is needed (active=False), # then add that to ch_to_change to change the state of the channels: if active_ch[ch_name] and not active: ch_to_change[ch_name] = active else: # all other channel which are active should be deactivated: if active_ch[ch_name]: ch_to_change[ch_name] = False self._seq_gen_logic.set_active_channels(ch_to_change) def _get_available_ch(self): """ Helper method to get a list of all available channels. @return list: entries are the generic string names of the channels. """ config = self._seq_gen_logic.get_hardware_constraints( )['activation_config'] available_ch = [] all_a_ch = [] all_d_ch = [] for conf in config: # extract all analog channels from the config curr_a_ch = [entry for entry in config[conf] if 'a_ch' in entry] curr_d_ch = [entry for entry in config[conf] if 'd_ch' in entry] # append all new analog channels to a temporary array for a_ch in curr_a_ch: if a_ch not in all_a_ch: all_a_ch.append(a_ch) # append all new digital channels to a temporary array for d_ch in curr_d_ch: if d_ch not in all_d_ch: all_d_ch.append(d_ch) all_a_ch.sort() all_d_ch.sort() available_ch.extend(all_a_ch) available_ch.extend(all_d_ch) return available_ch def do_optimize_pos(self): """ Perform an optimize position. """ #FIXME: Move this optimization routine to the tasks! curr_pos = self._confocal_logic.get_position() self._optimizer_logic.start_refocus( curr_pos, caller_tag='nuclear_operations_logic') # check just the state of the optimizer while self._optimizer_logic.getState( ) != 'idle' and not self._stop_requested: time.sleep(0.5) # use the position to move the scanner self._confocal_logic.set_position('nuclear_operations_logic', self._optimizer_logic.optim_pos_x, self._optimizer_logic.optim_pos_y, self._optimizer_logic.optim_pos_z) def _create_pulsed_odmr(self): """ Create the pulsed ODMR asset. """ #FIXME: Move this creation routine to the tasks! # generate: self._seq_gen_logic.generate_pulsedodmr( name='PulsedODMR', mw_time_ns=(self.electron_rabi_periode * 1e9) / 2, mw_freq_MHz=self.pulser_mw_freq / 1e6, mw_amp_V=self.pulser_mw_amp, mw_channel=self.pulser_mw_ch, laser_time_ns=self.pulser_laser_length * 1e9, laser_channel=self.pulser_laser_ch, laser_amp_V=self.pulser_laser_amp, wait_time_ns=self.pulser_idle_time * 1e9) # sample: self._seq_gen_logic.sample_pulse_block_ensemble( ensemble_name='PulsedODMR', write_to_file=True, chunkwise=False) # upload: self._seq_gen_logic.upload_asset(asset_name='PulsedODMR') def _load_pulsed_odmr(self): """ Load a pulsed ODMR asset. """ #FIXME: Move this creation routine to the tasks! self._seq_gen_logic.load_asset(asset_name='PulsedODMR') def do_optimize_odmr_freq(self): """ Perform an ODMR measurement. """ #FIXME: Move this creation routine to the tasks! # make the odmr around the peak which is used for the mw drive: if self.mw_on_odmr_peak == 0: center_freq = self.odmr_meas_freq0 if self.mw_on_odmr_peak == 1: center_freq = self.odmr_meas_freq1 if self.mw_on_odmr_peak == 2: center_freq = self.odmr_meas_freq2 start_freq = center_freq - self.odmr_meas_freq_range / 2 stop_freq = center_freq + self.odmr_meas_freq_range / 2 name_tag = 'odmr_meas_for_nuclear_ops' param = self._odmr_logic.perform_odmr_measurement( freq_start=start_freq, freq_step=self.odmr_meas_step, freq_stop=stop_freq, power=self.odmr_meas_power, runtime=self.odmr_meas_runtime, fit_function='N14', save_after_meas=True, name_tag=name_tag) self.odmr_meas_freq0 = param['Freq. 0']['value'] self.odmr_meas_freq1 = param['Freq. 1']['value'] self.odmr_meas_freq2 = param['Freq. 2']['value'] curr_time = (datetime.datetime.now() - self.start_time).total_seconds() self.measured_odmr_list.append([ curr_time, self.odmr_meas_freq0, self.odmr_meas_freq1, self.odmr_meas_freq2 ]) while self._odmr_logic.getState( ) != 'idle' and not self._stop_requested: time.sleep(0.5) def mw_on(self): """ Start the microwave device. """ self._odmr_logic.MW_on() def mw_off(self): """ Stop the microwave device. """ self._odmr_logic.MW_off() def set_mw_on_odmr_freq(self, freq, power): """ Set the microwave on a the specified freq with the specified power. """ self._odmr_logic.set_frequency(freq) self._odmr_logic.set_power(power) def save_nuclear_operation_measurement(self, name_tag=None, timestamp=None): """ Save the nuclear operation data. @param str name_tag: @param object timestamp: datetime.datetime object, from which everything can be created. """ filepath = self._save_logic.get_path_for_module( module_name='NuclearOperations') if timestamp is None: timestamp = datetime.datetime.now() if name_tag is not None and len(name_tag) > 0: filelabel1 = name_tag + '_nuclear_ops_xy_data' filelabel2 = name_tag + '_nuclear_ops_data_y_matrix' filelabel3 = name_tag + '_nuclear_ops_add_data_matrix' filelabel4 = name_tag + '_nuclear_ops_odmr_data' else: filelabel1 = '_nuclear_ops_data' filelabel2 = '_nuclear_ops_data_matrix' filelabel3 = '_nuclear_ops_add_data_matrix' filelabel4 = '_nuclear_ops_odmr_data' param = OrderedDict() param['Electron Rabi Period (ns)'] = self.electron_rabi_periode * 1e9 param['Pulser Microwave Frequency (MHz)'] = self.pulser_mw_freq / 1e6 param['Pulser MW amp (V)'] = self.pulser_mw_amp param['Pulser MW channel'] = self.pulser_mw_ch param[ 'Nuclear Rabi period Trans 0 (micro-s)'] = self.nuclear_rabi_period0 * 1e6 param['Nuclear Trans freq 0 (MHz)'] = self.pulser_rf_freq0 / 1e6 param['Pulser RF amp 0 (V)'] = self.pulser_rf_amp0 param[ 'Nuclear Rabi period Trans 1 (micro-s)'] = self.nuclear_rabi_period1 * 1e6 param['Nuclear Trans freq 1 (MHz)'] = self.pulser_rf_freq1 / 1e6 param['Pulser RF amp 1 (V)'] = self.pulser_rf_amp1 param['Pulser Rf channel'] = self.pulser_rf_ch param['Pulser Laser length (ns)'] = self.pulser_laser_length * 1e9 param['Pulser Laser amp (V)'] = self.pulser_laser_amp param['Pulser Laser channel'] = self.pulser_laser_ch param[ 'Number of single shot readouts per pulse'] = self.num_singleshot_readout param['Pulser idle Time (ns)'] = self.pulser_idle_time * 1e9 param['Pulser Detect channel'] = self.pulser_detect_ch data1 = OrderedDict() data2 = OrderedDict() data3 = OrderedDict() data4 = OrderedDict() # Measurement Parameter: param[''] = self.current_meas_asset_name if self.current_meas_asset_name in ['Nuclear_Frequency_Scan']: param['x axis start (MHz)'] = self.x_axis_start / 1e6 param['x axis step (MHz)'] = self.x_axis_step / 1e6 param['Current '] = self.current_meas_point / 1e6 data1['RF pulse frequency (MHz)'] = self.x_axis_list data1['Flip Probability'] = self.y_axis_list data2['RF pulse frequency matrix (MHz)'] = self.y_axis_matrix elif self.current_meas_asset_name in [ 'Nuclear_Rabi', 'QSD_-_Artificial_Drive', 'QSD_-_SWAP_FID', 'QSD_-_Entanglement_FID' ]: param['x axis start (micro-s)'] = self.x_axis_start * 1e6 param['x axis step (micro-s)'] = self.x_axis_step * 1e6 param['Current '] = self.current_meas_point * 1e6 data1['RF pulse length (micro-s)'] = self.x_axis_list data1['Flip Probability'] = self.y_axis_list data2['RF pulse length matrix (micro-s)'] = self.y_axis_matrix else: param['x axis start'] = self.x_axis_start param['x axis step'] = self.x_axis_step param['Current '] = self.current_meas_point data1['x axis'] = self.x_axis_list data1['y axis'] = self.y_axis_list data2['y axis matrix)'] = self.y_axis_matrix data3['Additional Data Matrix'] = self.parameter_matrix data4['Measured ODMR Data Matrix'] = np.array(self.measured_odmr_list) param[ 'Number of expected measurement points per run'] = self.x_axis_num_points param['Number of expected measurement runs'] = self.num_of_meas_runs param[ 'Number of current measurement runs'] = self.num_of_current_meas_runs param['Current measurement index'] = self.current_meas_index param['Optimize Period ODMR (s)'] = self.optimize_period_odmr param['Optimize Period Confocal (s)'] = self.optimize_period_confocal param['current ODMR trans freq0 (MHz)'] = self.odmr_meas_freq0 / 1e6 param['current ODMR trans freq1 (MHz)'] = self.odmr_meas_freq1 / 1e6 param['current ODMR trans freq2 (MHz)'] = self.odmr_meas_freq2 / 1e6 param['Runtime of ODMR optimization (s)'] = self.odmr_meas_runtime param[ 'Frequency Range ODMR optimization (MHz)'] = self.odmr_meas_freq_range / 1e6 param[ 'Frequency Step ODMR optimization (MHz)'] = self.odmr_meas_step / 1e6 param['Power of ODMR optimization (dBm)'] = self.odmr_meas_power param['Selected ODMR trans freq (MHz)'] = self.mw_cw_freq / 1e6 param['Selected ODMR trans power (dBm)'] = self.mw_cw_power param['Selected ODMR trans Peak'] = self.mw_on_odmr_peak param[ 'Number of samples in the gated counter'] = self.gc_number_of_samples param['Number of samples per readout'] = self.gc_samples_per_readout param['Elapsed Time (s)'] = self.elapsed_time param['Start of measurement'] = self.start_time.strftime( '%Y-%m-%d %H:%M:%S') self._save_logic.save_data(data1, filepath=filepath, parameters=param, filelabel=filelabel1, timestamp=timestamp) self._save_logic.save_data(data2, filepath=filepath, filelabel=filelabel2, timestamp=timestamp) self._save_logic.save_data(data4, filepath=filepath, filelabel=filelabel4, timestamp=timestamp) # self._save_logic.save_data(data3, # filepath=filepath, # filelabel=filelabel3, # timestamp=timestamp) self.log.info('Nuclear Operation data saved to:\n{0}'.format(filepath))
class LaserScannerLogic(GenericLogic): """This logic module controls scans of DC voltage on the fourth analog output channel of the NI Card. It collects countrate as a function of voltage. """ sig_data_updated = QtCore.Signal() _modclass = 'laserscannerlogic' _modtype = 'logic' # declare connectors confocalscanner1 = Connector(interface='ConfocalScannerInterface') savelogic = Connector(interface='SaveLogic') scan_range = StatusVar('scan_range', [-10, 10]) number_of_repeats = StatusVar(default=10) resolution = StatusVar('resolution', 500) _scan_speed = StatusVar('scan_speed', 10) _static_v = StatusVar('goto_voltage', 5) sigChangeVoltage = QtCore.Signal(float) sigVoltageChanged = QtCore.Signal(float) sigScanNextLine = QtCore.Signal() sigUpdatePlots = QtCore.Signal() sigScanFinished = QtCore.Signal() sigScanStarted = QtCore.Signal() def __init__(self, **kwargs): """ Create VoltageScanningLogic object with connectors. @param dict kwargs: optional parameters """ super().__init__(**kwargs) # locking for thread safety self.threadlock = Mutex() self.stopRequested = False self.fit_x = [] self.fit_y = [] self.plot_x = [] self.plot_y = [] self.plot_y2 = [] def on_activate(self): """ Initialisation performed during activation of the module. """ self._scanning_device = self.get_connector('confocalscanner1') self._save_logic = self.get_connector('savelogic') # Reads in the maximal scanning range. The unit of that scan range is # micrometer! self.a_range = self._scanning_device.get_position_range()[3] # Initialise the current position of all four scanner channels. self.current_position = self._scanning_device.get_scanner_position() # initialise the range for scanning self.set_scan_range(self.scan_range) # Keep track of the current static voltage even while a scan may cause the real-time # voltage to change. self.goto_voltage(self._static_v) # Sets connections between signals and functions self.sigChangeVoltage.connect(self._change_voltage, QtCore.Qt.QueuedConnection) self.sigScanNextLine.connect(self._do_next_line, QtCore.Qt.QueuedConnection) # Initialization of internal counter for scanning self._scan_counter_up = 0 self._scan_counter_down = 0 # Keep track of scan direction self.upwards_scan = True # calculated number of points in a scan, depends on speed and max step size self._num_of_steps = 50 # initialising. This is calculated for a given ramp. ############################# # TODO: allow configuration with respect to measurement duration self.acquire_time = 20 # seconds # default values for clock frequency and slowness # slowness: steps during retrace line self.set_resolution(self.resolution) self._goto_speed = 10 # 0.01 # volt / second self.set_scan_speed(self._scan_speed) self._smoothing_steps = 10 # steps to accelerate between 0 and scan_speed self._max_step = 0.01 # volt ############################## # Initialie data matrix self._initialise_data_matrix(100) def on_deactivate(self): """ Deinitialisation performed during deactivation of the module. """ self.stopRequested = True @QtCore.Slot(float) def goto_voltage(self, volts=None): """Forwarding the desired output voltage to the scanning device. @param float volts: desired voltage (volts) @return int: error code (0:OK, -1:error) """ # print(tag, x, y, z) # Changes the respective value if volts is not None: self._static_v = volts # Checks if the scanner is still running if self.getState() == 'locked' or self._scanning_device.getState( ) == 'locked': self.log.error('Cannot goto, because scanner is locked!') return -1 else: self.sigChangeVoltage.emit(volts) return 0 def _change_voltage(self, new_voltage): """ Threaded method to change the hardware voltage for a goto. @return int: error code (0:OK, -1:error) """ ramp_scan = self._generate_ramp(self.get_current_voltage(), new_voltage, self._goto_speed) self._initialise_scanner() ignored_counts = self._scan_line(ramp_scan) self._close_scanner() self.sigVoltageChanged.emit(new_voltage) return 0 def _goto_during_scan(self, voltage=None): if voltage is None: return -1 goto_ramp = self._generate_ramp(self.get_current_voltage(), voltage, self._goto_speed) ignored_counts = self._scan_line(goto_ramp) return 0 def set_clock_frequency(self, clock_frequency): """Sets the frequency of the clock @param int clock_frequency: desired frequency of the clock @return int: error code (0:OK, -1:error) """ self._clock_frequency = float(clock_frequency) # checks if scanner is still running if self.getState() == 'locked': return -1 else: return 0 def set_resolution(self, resolution): """ Calculate clock rate from scan speed and desired number of pixels """ self.resolution = resolution scan_range = abs(self.scan_range[1] - self.scan_range[0]) duration = scan_range / self._scan_speed new_clock = resolution / duration return self.set_clock_frequency(new_clock) def set_scan_range(self, scan_range): """ Set the scan rnage """ r_max = np.clip(scan_range[1], self.a_range[0], self.a_range[1]) r_min = np.clip(scan_range[0], self.a_range[0], r_max) self.scan_range = [r_min, r_max] def set_voltage(self, volts): """ Set the channel idle voltage """ self._static_v = np.clip(volts, self.a_range[0], self.a_range[1]) self.goto_voltage(self._static_v) def set_scan_speed(self, scan_speed): """ Set scan speed in volt per second """ self._scan_speed = np.clip(scan_speed, 1e-9, 1e6) self._goto_speed = self._scan_speed def set_scan_lines(self, scan_lines): self.number_of_repeats = int(np.clip(scan_lines, 1, 1e6)) def _initialise_data_matrix(self, scan_length): """ Initializing the ODMR matrix plot. """ self.scan_matrix = np.zeros((self.number_of_repeats, scan_length)) self.scan_matrix2 = np.zeros((self.number_of_repeats, scan_length)) self.plot_x = np.linspace(self.scan_range[0], self.scan_range[1], scan_length) self.plot_y = np.zeros(scan_length) self.plot_y2 = np.zeros(scan_length) self.fit_x = np.linspace(self.scan_range[0], self.scan_range[1], scan_length) self.fit_y = np.zeros(scan_length) def get_current_voltage(self): """returns current voltage of hardware device(atm NIDAQ 4th output)""" return self._scanning_device.get_scanner_position()[3] def _initialise_scanner(self): """Initialise the clock and locks for a scan""" self.lock() self._scanning_device.lock() returnvalue = self._scanning_device.set_up_scanner_clock( clock_frequency=self._clock_frequency) if returnvalue < 0: self._scanning_device.unlock() self.unlock() self.set_position('scanner') return -1 returnvalue = self._scanning_device.set_up_scanner() if returnvalue < 0: self._scanning_device.unlock() self.unlock() self.set_position('scanner') return -1 return 0 def start_scanning(self, v_min=None, v_max=None): """Setting up the scanner device and starts the scanning procedure @return int: error code (0:OK, -1:error) """ self.current_position = self._scanning_device.get_scanner_position() print(self.current_position) if v_min is not None: self.scan_range[0] = v_min else: v_min = self.scan_range[0] if v_max is not None: self.scan_range[1] = v_max else: v_max = self.scan_range[1] self._scan_counter_up = 0 self._scan_counter_down = 0 self.upwards_scan = True # TODO: Generate Ramps self._upwards_ramp = self._generate_ramp(v_min, v_max, self._scan_speed) self._downwards_ramp = self._generate_ramp(v_max, v_min, self._scan_speed) self._initialise_data_matrix(len(self._upwards_ramp[3])) # Lock and set up scanner returnvalue = self._initialise_scanner() if returnvalue < 0: # TODO: error message return -1 self.sigScanNextLine.emit() self.sigScanStarted.emit() return 0 def stop_scanning(self): """Stops the scan @return int: error code (0:OK, -1:error) """ with self.threadlock: if self.getState() == 'locked': self.stopRequested = True return 0 def _close_scanner(self): """Close the scanner and unlock""" with self.threadlock: self.kill_scanner() self.stopRequested = False if self.can('unlock'): self.unlock() def _do_next_line(self): """ If stopRequested then finish the scan, otherwise perform next repeat of the scan line """ # stops scanning if self.stopRequested or self._scan_counter_down >= self.number_of_repeats: print(self.current_position) self._goto_during_scan(self._static_v) self._close_scanner() self.sigScanFinished.emit() return if self._scan_counter_up == 0: # move from current voltage to start of scan range. self._goto_during_scan(self.scan_range[0]) if self.upwards_scan: counts = self._scan_line(self._upwards_ramp) self.scan_matrix[self._scan_counter_up] = counts self.plot_y += counts self._scan_counter_up += 1 self.upwards_scan = False else: counts = self._scan_line(self._downwards_ramp) self.scan_matrix2[self._scan_counter_down] = counts self.plot_y2 += counts self._scan_counter_down += 1 self.upwards_scan = True self.sigUpdatePlots.emit() self.sigScanNextLine.emit() def _generate_ramp(self, voltage1, voltage2, speed): """Generate a ramp vrom voltage1 to voltage2 that satisfies the speed, step, smoothing_steps parameters. Smoothing_steps=0 means that the ramp is just linear. @param float voltage1: voltage at start of ramp. @param float voltage2: voltage at end of ramp. """ # It is much easier to calculate the smoothed ramp for just one direction (upwards), # and then to reverse it if a downwards ramp is required. v_min = min(voltage1, voltage2) v_max = max(voltage1, voltage2) if v_min == v_max: ramp = np.array([v_min, v_max]) else: # These values help simplify some of the mathematical expressions linear_v_step = speed / self._clock_frequency smoothing_range = self._smoothing_steps + 1 # Sanity check in case the range is too short # The voltage range covered while accelerating in the smoothing steps v_range_of_accel = sum(n * linear_v_step / smoothing_range for n in range(0, smoothing_range)) # Obtain voltage bounds for the linear part of the ramp v_min_linear = v_min + v_range_of_accel v_max_linear = v_max - v_range_of_accel if v_min_linear > v_max_linear: self.log.warning( 'Voltage ramp too short to apply the ' 'configured smoothing_steps. A simple linear ramp ' 'was created instead.') num_of_linear_steps = np.rint((v_max - v_min) / linear_v_step) ramp = np.linspace(v_min, v_max, num_of_linear_steps) else: num_of_linear_steps = np.rint( (v_max_linear - v_min_linear) / linear_v_step) # Calculate voltage step values for smooth acceleration part of ramp smooth_curve = np.array([ sum(n * linear_v_step / smoothing_range for n in range(1, N)) for N in range(1, smoothing_range) ]) accel_part = v_min + smooth_curve decel_part = v_max - smooth_curve[::-1] linear_part = np.linspace(v_min_linear, v_max_linear, num_of_linear_steps) ramp = np.hstack((accel_part, linear_part, decel_part)) # Reverse if downwards ramp is required if voltage2 < voltage1: ramp = ramp[::-1] # Put the voltage ramp into a scan line for the hardware (4-dimension) spatial_pos = self._scanning_device.get_scanner_position() scan_line = np.vstack((np.ones( (len(ramp), )) * spatial_pos[0], np.ones( (len(ramp), )) * spatial_pos[1], np.ones( (len(ramp), )) * spatial_pos[2], ramp)) return scan_line def _scan_line(self, line_to_scan=None): """do a single voltage scan from voltage1 to voltage2 """ if line_to_scan is None: self.log.error('Voltage scanning logic needs a line to scan!') return -1 try: # scan of a single line counts_on_scan_line = self._scanning_device.scan_line(line_to_scan) return counts_on_scan_line.transpose()[0] except Exception as e: self.log.error('The scan went wrong, killing the scanner.') self.stop_scanning() self.sigScanNextLine.emit() raise e def kill_scanner(self): """Closing the scanner device. @return int: error code (0:OK, -1:error) """ try: self._scanning_device.close_scanner() self._scanning_device.close_scanner_clock() except Exception as e: self.log.exception('Could not even close the scanner, giving up.') raise e try: if self._scanning_device.can('unlock'): self._scanning_device.unlock() except: self.log.exception('Could not unlock scanning device.') return 0 def save_data(self, tag=None, colorscale_range=None, percentile_range=None): """ Save the counter trace data and writes it to a file. @return int: error code (0:OK, -1:error) """ if tag is None: tag = '' self._saving_stop_time = time.time() filepath = self._save_logic.get_path_for_module( module_name='LaserScanning') filepath2 = self._save_logic.get_path_for_module( module_name='LaserScanning') filepath3 = self._save_logic.get_path_for_module( module_name='LaserScanning') timestamp = datetime.datetime.now() if len(tag) > 0: filelabel = tag + '_volt_data' filelabel2 = tag + '_volt_data_raw_trace' filelabel3 = tag + '_volt_data_raw_retrace' else: filelabel = 'volt_data' filelabel2 = 'volt_data_raw_trace' filelabel3 = 'volt_data_raw_retrace' # prepare the data in a dict or in an OrderedDict: data = OrderedDict() data['frequency (Hz)'] = self.plot_x data['trace count data (counts/s)'] = self.plot_y data['retrace count data (counts/s)'] = self.plot_y2 data2 = OrderedDict() data2['count data (counts/s)'] = self.scan_matrix[:self. _scan_counter_up, :] data3 = OrderedDict() data3[ 'count data (counts/s)'] = self.scan_matrix2[:self. _scan_counter_down, :] parameters = OrderedDict() parameters['Number of frequency sweeps (#)'] = self._scan_counter_up parameters['Start Voltage (V)'] = self.scan_range[0] parameters['Stop Voltage (V)'] = self.scan_range[1] parameters['Scan speed [V/s]'] = self._scan_speed parameters['Clock Frequency (Hz)'] = self._clock_frequency fig = self.draw_figure(self.scan_matrix, self.plot_x, self.plot_y, self.fit_x, self.fit_y, cbar_range=colorscale_range, percentile_range=percentile_range) fig2 = self.draw_figure(self.scan_matrix2, self.plot_x, self.plot_y2, self.fit_x, self.fit_y, cbar_range=colorscale_range, percentile_range=percentile_range) self._save_logic.save_data(data, filepath=filepath, parameters=parameters, filelabel=filelabel, fmt='%.6e', delimiter='\t', timestamp=timestamp) self._save_logic.save_data(data2, filepath=filepath2, parameters=parameters, filelabel=filelabel2, fmt='%.6e', delimiter='\t', timestamp=timestamp, plotfig=fig) self._save_logic.save_data(data3, filepath=filepath3, parameters=parameters, filelabel=filelabel3, fmt='%.6e', delimiter='\t', timestamp=timestamp, plotfig=fig2) self.log.info('Laser Scan saved to:\n{0}'.format(filepath)) return 0 def draw_figure(self, matrix_data, freq_data, count_data, fit_freq_vals, fit_count_vals, cbar_range=None, percentile_range=None): """ Draw the summary figure to save with the data. @param: list cbar_range: (optional) [color_scale_min, color_scale_max]. If not supplied then a default of data_min to data_max will be used. @param: list percentile_range: (optional) Percentile range of the chosen cbar_range. @return: fig fig: a matplotlib figure object to be saved to file. """ # If no colorbar range was given, take full range of data if cbar_range is None: cbar_range = np.array([np.min(matrix_data), np.max(matrix_data)]) else: cbar_range = np.array(cbar_range) prefix = ['', 'k', 'M', 'G', 'T'] prefix_index = 0 # Rescale counts data with SI prefix while np.max(count_data) > 1000: count_data = count_data / 1000 fit_count_vals = fit_count_vals / 1000 prefix_index = prefix_index + 1 counts_prefix = prefix[prefix_index] # Rescale frequency data with SI prefix prefix_index = 0 while np.max(freq_data) > 1000: freq_data = freq_data / 1000 fit_freq_vals = fit_freq_vals / 1000 prefix_index = prefix_index + 1 mw_prefix = prefix[prefix_index] # Rescale matrix counts data with SI prefix prefix_index = 0 while np.max(matrix_data) > 1000: matrix_data = matrix_data / 1000 cbar_range = cbar_range / 1000 prefix_index = prefix_index + 1 cbar_prefix = prefix[prefix_index] # Use qudi style plt.style.use(self._save_logic.mpl_qd_style) # Create figure fig, (ax_mean, ax_matrix) = plt.subplots(nrows=2, ncols=1) ax_mean.plot(freq_data, count_data, linestyle=':', linewidth=0.5) # Do not include fit curve if there is no fit calculated. if max(fit_count_vals) > 0: ax_mean.plot(fit_freq_vals, fit_count_vals, marker='None') ax_mean.set_ylabel('Fluorescence (' + counts_prefix + 'c/s)') ax_mean.set_xlim(np.min(freq_data), np.max(freq_data)) matrixplot = ax_matrix.imshow( matrix_data, cmap=plt.get_cmap('inferno'), # reference the right place in qd origin='lower', vmin=cbar_range[0], vmax=cbar_range[1], extent=[ np.min(freq_data), np.max(freq_data), 0, self.number_of_repeats ], aspect='auto', interpolation='nearest') ax_matrix.set_xlabel('Frequency (' + mw_prefix + 'Hz)') ax_matrix.set_ylabel('Scan #') # Adjust subplots to make room for colorbar fig.subplots_adjust(right=0.8) # Add colorbar axis to figure cbar_ax = fig.add_axes([0.85, 0.15, 0.02, 0.7]) # Draw colorbar cbar = fig.colorbar(matrixplot, cax=cbar_ax) cbar.set_label('Fluorescence (' + cbar_prefix + 'c/s)') # remove ticks from colorbar for cleaner image cbar.ax.tick_params(which='both', length=0) # If we have percentile information, draw that to the figure if percentile_range is not None: cbar.ax.annotate(str(percentile_range[0]), xy=(-0.3, 0.0), xycoords='axes fraction', horizontalalignment='right', verticalalignment='center', rotation=90) cbar.ax.annotate(str(percentile_range[1]), xy=(-0.3, 1.0), xycoords='axes fraction', horizontalalignment='right', verticalalignment='center', rotation=90) cbar.ax.annotate('(percentile)', xy=(-0.3, 0.5), xycoords='axes fraction', horizontalalignment='right', verticalalignment='center', rotation=90) return fig
class ProcessControlModifier(GenericLogic, ProcessControlInterface): """ This interfuse can be used to modify a process control on the fly. It needs a 2D array to interpolate General form : [[x_0, y_0], [x_1, y_1], ... , [x_n, y_n]] Example : [[0,0], [1,10]] With this example, the value 0.5 sent from the logic would be transformed to 5 sent to the hardware. This calibration is stored and remembered as a status variable. If this variable is None, the calibration can be read from a simple file with two columns : # X Y 0 0 1 10 """ _modclass = 'ProcessValueModifier' _modtype = 'interfuse' hardware = Connector(interface='ProcessControlInterface') _calibration = StatusVar(default=None) _calibration_file = ConfigOption('calibration_file', None) _force_calibration_from_file = ConfigOption('force_calibration_from_file', False) _interpolated_function = None _interpolated_function_reversed = None _new_unit = ConfigOption('new_unit', None) _last_control_value = None def on_activate(self): """ Activate module. """ self._hardware = self.hardware() if self._force_calibration_from_file and self._calibration_file is None: self.log.error( 'Loading from calibration is enforced but no calibration file has been' 'given.') if self._force_calibration_from_file or (self._calibration is None and self._calibration_file is not None): self.log.info('Loading from calibration file.') calibration = np.loadtxt(self._calibration_file) self.update_calibration(calibration) else: self.update_calibration() def on_deactivate(self): """ Deactivate module. """ pass def update_calibration(self, calibration=None): """ Construct the interpolated function from the calibration data calibration (optional) 2d array : A new calibration to set """ if calibration is not None: self._calibration = calibration if self._calibration is None: self._interpolated_function = lambda x: x self._interpolated_function_reversed = lambda x: x else: self._interpolated_function = interp1d(self._calibration[:, 0], self._calibration[:, 1]) self._interpolated_function_reversed = interp1d( self._calibration[:, 1], self._calibration[:, 0]) if self._last_control_value is not None: self.set_control_value(self._last_control_value) def reset_to_identity(self): """ Reset the calibration data to use identity """ self._calibration = None self.update_calibration() def get_control_value(self): """ Return the original control value """ if self._interpolated_function_reversed is not None: return self._interpolated_function_reversed( self._hardware.get_control_value()) else: self.log.error( 'No calibration was found, please set the control value modifier data first.' ) def set_control_value(self, value): """ Set the control value modified """ if self._interpolated_function is not None: self._hardware.set_control_value( self._interpolated_function(value)) else: self.log.error( 'No calibration was found, please set the control value modifier data first.' ) def get_control_unit(self): """ Return the process unit """ if self._new_unit is not None: return self._new_unit else: return self._hardware.get_control_unit() def get_control_limit(self): """ Return limits within which the controlled value can be set as a tuple of (low limit, high limit) """ mini, maxi = self._hardware.get_control_limit() mini = float(self._interpolated_function_reversed(mini)) maxi = float(self._interpolated_function_reversed(maxi)) return mini, maxi
class NoiseSpectrumGui(GUIBase): """ Class for the alignment of the magnetic field. """ _modclass = 'MagnetControlGui' _modtype = 'gui' # declare connectors fitlogic = Connector(interface='FitLogic') savelogic = Connector(interface='SaveLogic') fc = StatusVar('fits', None) def __init__(self, config, **kwargs): super().__init__(config=config, **kwargs) def on_activate(self): """ Initializes all needed UI files and establishes the connectors. This method executes the all the inits for the differnt GUIs and passes the event argument from fysom to the methods. """ # # Getting an access to all connectors: self._save_logic = self.get_connector('savelogic') self._fit_logic = self.get_connector('fitlogic') self.initMainUI() # initialize the main GUI self.ex = App() self.initAppUI() self._mw.actionOpen.triggered.connect(self.open_file) self.time = np.zeros(1) self.counts1 = np.zeros(1) self.error1 = np.zeros(1) self.counts2 = np.zeros(1) self.error2 = np.zeros(1) self._mw.sequence_order.editingFinished.connect( self.redraw_normalized_plot) self._mw.contrast.editingFinished.connect(self.redraw_normalized_plot) self._mw.n_FFT.setMaximum(3.0e+06) self._mw.n_FFT.setValue(2.0e+06) self._mw.calculate_filter_function.clicked.connect( self._filter_function_button_fired) self._mw.calculate_ns.clicked.connect( self._calculate_noise_spectrum_button_fired) self.fit_x = np.array([0, 1]) self.fit_y = np.array([0, 1]) self.fit_image = pg.PlotDataItem(self.fit_x, self.fit_y, pen=pg.mkPen(palette.c3)) # fit settings self._fsd = FitSettingsDialog(self.fc) self._fsd.sigFitsUpdated.connect( self._mw.fit_methods_ComboBox.setFitFunctions) self._fsd.applySettings() self._mw.actionFit_Settings.triggered.connect(self._fsd.show) self._mw.do_x_fit_PushButton.clicked.connect(self.do_x_fit) if 'fits' in self._statusVariables and isinstance( self._statusVariables['fits'], dict): self.fc.load_from_dict(self._statusVariables['fits']) return def initMainUI(self): """ Definition, configuration and initialisation of the confocal GUI. This init connects all the graphic modules, which were created in the *.ui file and configures the event handling between the modules. Moreover it sets default values. """ self._mw = NVdepthMainWindow() def initAppUI(self): self.ex.setWindowTitle(self.ex.title) self.ex.setGeometry(self.ex.left, self.ex.top, self.ex.width, self.ex.height) # self.openFileNamesDialog() # self.saveFileDialog() # def openFileNameDialog(self): options = QFileDialog.Options() options |= QFileDialog.DontUseNativeDialog fileName, _ = QFileDialog.getOpenFileName( self.ex, "QFileDialog.getOpenFileName()", "", "All Files (*.dat);;Python Files (*.txt)", options=options) if fileName: print(fileName) return str(fileName) def on_deactivate(self): """ Reverse steps of activation @return int: error code (0:OK, -1:error) """ self._mw.close() return 0 def show(self): """Make main window visible and put it above all other windows. """ # Show the Main Magnet Control GUI: self._mw.show() self._mw.activateWindow() self._mw.raise_() def open_file(self): self.myfile = self.openFileNameDialog() self.ex.show() self.ex.exec_() self.show_data() def show_data(self): f = open(self.myfile, 'r') lines = f.readlines() result = [] for x in lines: result.append(x.split('#')[0]) f.close() a = [x for x in result if x != ''] self.time = np.zeros(len(a)) self.counts1 = np.zeros(len(a)) self.error1 = np.zeros(len(a)) self.counts2 = np.zeros(len(a)) self.error2 = np.zeros(len(a)) self._mw.data_plot.clear() self._mw.processeddataplot.clear() for i in range(len(a)): self.time[i] = np.asarray(a[i].split(), dtype=np.float32)[0] self.counts1[i] = np.asarray(a[i].split(), dtype=np.float32)[1] self.error1[i] = np.asarray(a[i].split(), dtype=np.float32)[3] self.counts2[i] = np.asarray(a[i].split(), dtype=np.float32)[2] self.error2[i] = np.asarray(a[i].split(), dtype=np.float32)[4] self.data_image1 = pg.PlotDataItem(self.time, self.counts1, pen=pg.mkPen( palette.c1, style=QtCore.Qt.DotLine), symbol='o', symbolPen=palette.c1, symbolBrush=palette.c1, symbolSize=7) self._mw.data_plot.addItem(self.data_image1) self.data_image2 = pg.PlotDataItem(self.time, self.counts2, pen=pg.mkPen( palette.c3, style=QtCore.Qt.DotLine), symbol='o', symbolPen=palette.c3, symbolBrush=palette.c3, symbolSize=7) self._mw.data_plot.addItem(self.data_image2) self._mw.data_plot.setLabel(axis='left', text='Counts', units='Counts/s') self._mw.data_plot.setLabel(axis='bottom', text='time', units='s') self._mw.data_plot.showGrid(x=True, y=True, alpha=0.8) self.baseline = np.sum(self.counts2 + self.counts1) / len( self.counts2) / 2 C0_up = self.baseline / (1 - 0.01 * self._mw.contrast.value() / 2) C0_down = C0_up * (1 - 0.01 * self._mw.contrast.value()) counts = self.counts2 - self.counts1 self.T = self.time * 8 * self._mw.sequence_order.value() self.normalized_counts = (counts) / (C0_up - C0_down) self.normalized_image = pg.PlotDataItem( self.time, #self.T, self.normalized_counts, pen=pg.mkPen(palette.c2, style=QtCore.Qt.DotLine), symbol='o', symbolPen=palette.c2, symbolBrush=palette.c2, symbolSize=7) self._mw.processeddataplot.addItem(self.normalized_image) self._mw.processeddataplot.setLabel(axis='left', text='Counts', units='Counts/s') self._mw.processeddataplot.setLabel(axis='bottom', text='time', units='s') self._mw.processeddataplot.showGrid(x=True, y=True, alpha=0.8) def _calculate_noise_spectrum_button_fired(self): self._mw.spin_noise_plot.clear() S = -np.log(self.normalized_counts) / self.T # self.S = np.concatenate((self.S, S), axis=0) frequency = 1e+6 * 1e-9 * 0.5e+3 / self.time # (in Hz) # self.frequency = np.concatenate((self.frequency, frequency), axis=0) self.noise_spectrum_image = pg.PlotDataItem( frequency * 1e-6, S, pen=pg.mkPen(palette.c5, style=QtCore.Qt.DotLine), symbol='o', symbolPen=palette.c5, symbolBrush=palette.c5, symbolSize=7) self._mw.spin_noise_plot.addItem(self.noise_spectrum_image) self._mw.spin_noise_plot.setLabel(axis='left', text='Intensity', units='arb.u.') self._mw.spin_noise_plot.setLabel(axis='bottom', text='Frequency', units='MHz') self._mw.spin_noise_plot.showGrid(x=True, y=True, alpha=0.8) def redraw_normalized_plot(self): self._mw.processeddataplot.clear() self.baseline = np.sum(self.counts2 + self.counts1) / len( self.counts2) / 2 C0_up = self.baseline / (1 - 0.01 * self._mw.contrast.value() / 2) C0_down = C0_up * (1 - 0.01 * self._mw.contrast.value()) counts = self.counts2 - self.counts1 self.T = self.time * 8 * self._mw.sequence_order.value() self.normalized_counts = (counts) / (C0_up - C0_down) self.normalized_image = pg.PlotDataItem( self.time, #self.T * 1.0e-6, self.normalized_counts, pen=pg.mkPen(palette.c2, style=QtCore.Qt.DotLine), symbol='o', symbolPen=palette.c2, symbolBrush=palette.c2, symbolSize=7) self._mw.processeddataplot.addItem(self.normalized_image) self._mw.processeddataplot.setLabel(axis='left', text='Counts', units='Counts/s') self._mw.processeddataplot.setLabel(axis='bottom', text='time', units='us') self._mw.processeddataplot.showGrid(x=True, y=True, alpha=0.8) return def _filter_function(self, tau): # generate filter function dt = 1e-9 n = int(tau / dt) v = np.zeros(8 * self._mw.sequence_order.value() * n) T = np.linspace(0, dt * n * 8 * self._mw.sequence_order.value(), num=8 * self._mw.sequence_order.value() * n) v[:n // 2] = 1 k = n / 2 + 1 for j in range(8 * self._mw.sequence_order.value() - 1): v[(n // 2 + j * n):(n // 2 + j * n + n)] = (-1)**(j + 1) k = k + 1 v[8 * self._mw.sequence_order.value() * n - n // 2:8 * self._mw.sequence_order.value() * n] = np.ones((n // 2, ), dtype=np.int) return T, v def _fourier_transform(self, tau): T, v = self._filter_function(tau) g = int(self._mw.n_FFT.value()) signalFFT = np.fft.fft(v, g) yf = (np.abs(signalFFT)** 2) * (1e-9) / (8 * self._mw.sequence_order.value()) xf = np.fft.fftfreq(g, 1e-9) self.FFy = yf[0:g] # take only real part self.FFx = xf[0:g] f1 = (1 / (2 * self.time[0])) * 1.1 # bigger f2 = (1 / (2 * self.time[-1])) * 0.5 # smaller yf1 = self.FFy[np.where(self.FFx <= f1)] xf1 = self.FFx[np.where(self.FFx <= f1)] self.FFy = self.FFy[np.where(xf1 >= f2)] self.FFx = self.FFx[np.where(xf1 >= f2)] return def _filter_function_button_fired(self): self._fourier_transform(self.time[self._mw.N_tau.value()]) self._mw.filter_function.clear() self.filter_function_image = pg.PlotDataItem( self.FFx * 1e-6, self.FFy, pen=pg.mkPen(palette.c4, style=QtCore.Qt.DotLine), symbol='o', symbolPen=palette.c4, symbolBrush=palette.c4, symbolSize=7) self._mw.filter_function.addItem(self.filter_function_image) self._mw.filter_function.setLabel(axis='left', text='Intensity', units='arb.u.') self._mw.filter_function.setLabel(axis='bottom', text='Frequency', units='MHz') self._mw.filter_function.showGrid(x=True, y=True, alpha=0.8) return @fc.constructor def sv_set_fits(self, val): # Setup fit container fc = self.fitlogic().make_fit_container('processed', '1d') fc.set_units(['s', 'c/s']) if isinstance(val, dict) and len(val) > 0: fc.load_from_dict(val) else: d1 = OrderedDict() d1['Gaussian peak'] = { 'fit_function': 'gaussian', 'estimator': 'peak' } d1['Lorentzian peak'] = { 'fit_function': 'lorentzian', 'estimator': 'peak' } d1['Two Lorentzian dips'] = { 'fit_function': 'lorentziandouble', 'estimator': 'dip' } d1['N14'] = { 'fit_function': 'lorentziantriple', 'estimator': 'N14' } d1['N15'] = { 'fit_function': 'lorentziandouble', 'estimator': 'N15' } default_fits = OrderedDict() default_fits['1d'] = d1['Lorentzian peak'] fc.load_from_dict(default_fits) return fc @fc.representer def sv_get_fits(self, val): """ save configured fits """ if len(val.fit_list) > 0: return val.save_to_dict() else: return None def get_fit_x_functions(self): """ Return the hardware constraints/limits @return list(str): list of fit function names """ return list(self.fc.fit_list) def do_x_fit(self, fit_function=None, x_data=None, y_data=None): """ Execute the currently configured fit on the measurement data. Optionally on passed data """ fit_function = self.get_fit_x_functions()[0] if (x_data is None) or (y_data is None): x_data = self.time y_data = self.normalized_counts if fit_function is not None and isinstance(fit_function, str): if fit_function in self.get_fit_x_functions(): self.fc.set_current_fit(fit_function) else: self.fc.set_current_fit('No Fit') if fit_function != 'No Fit': self.log.warning( 'Fit function "{0}" not available in ODMRLogic fit container.' ''.format(fit_function)) self.fit_x, self.fit_y, result = self.fc.do_fit(x_data, y_data) print(result) if result is None: result_str_dict = {} else: result_str_dict = result.result_str_dict self.update_x_fit(self.fit_x, self.fit_y, result_str_dict, self.fc.current_fit) return def update_x_fit(self, x_data, y_data, result_str_dict, current_fit): """ Update the shown fit. """ if current_fit != 'No Fit': # display results as formatted text self._mw.x_fit_result.clear() try: formated_results = units.create_formatted_output( result_str_dict) except: formated_results = 'this fit does not return formatted results' self._mw.x_fit_result.setPlainText(formated_results) self._mw.fit_methods_ComboBox.blockSignals(True) self._mw.fit_methods_ComboBox.setCurrentFit(current_fit) self._mw.fit_methods_ComboBox.blockSignals(False) # check which Fit method is used and remove or add again the # odmr_fit_image, check also whether a odmr_fit_image already exists. if current_fit != 'No Fit': self.fit_image.setData(x=x_data, y=y_data) if self.fit_image not in self._mw.processeddataplot.listDataItems( ): self._mw.processeddataplot.addItem(self.fit_image) else: if self.fit_image in self._mw.processeddataplot.listDataItems(): self._mw.processeddataplot.removeItem(self.fit_image) # self._mw.X_scan.getViewBox().updateAutoRange() return
class OkFpgaPulser(Base, PulserInterface): """ Methods to control Pulse Generator running on OK FPGA. Chan PIN ---------- Ch1 A3 Ch2 C5 Ch3 D6 Ch4 B6 Ch5 C7 Ch6 B8 Ch7 D9 Ch8 C9 Example config for copy-paste: fpga_pulser_ok: module.Class: 'fpga_fastcounter.fast_pulser_qo.OkFpgaPulser' fpga_serial: '143400058N' fpga_type: 'XEM6310_LX150' """ _modclass = 'pulserinterface' _modtype = 'hardware' _fpga_serial = ConfigOption(name='fpga_serial', missing='error') _fpga_type = ConfigOption(name='fpga_type', default='XEM6310_LX150', missing='warn') __current_waveform = StatusVar(name='current_waveform', default=np.zeros(32, dtype='uint8')) __current_waveform_name = StatusVar(name='current_waveform_name', default='') __sample_rate = StatusVar(name='sample_rate', default=950e6) def __init__(self, config, **kwargs): super().__init__(config=config, **kwargs) self.__current_status = -1 self.__currently_loaded_waveform = '' # loaded and armed waveform name self.__samples_written = 0 self._fp3support = False self.fpga = None # Reference to the OK FrontPanel instance def on_activate(self): self.__samples_written = 0 self.__currently_loaded_waveform = '' self.fpga = ok.FrontPanel() self._connect_fpga() self.set_sample_rate(self.__sample_rate) def on_deactivate(self): self._disconnect_fpga() @__current_waveform.representer def _convert_current_waveform(self, waveform_bytearray): return np.frombuffer(waveform_bytearray, dtype='uint8') @__current_waveform.constructor def _recover_current_waveform(self, waveform_nparray): return bytearray(waveform_nparray.tobytes()) def get_constraints(self): """ Retrieve the hardware constrains from the Pulsing device. @return constraints object: object with pulser constraints as attributes. Provides all the constraints (e.g. sample_rate, amplitude, total_length_bins, channel_config, ...) related to the pulse generator hardware to the caller. SEE PulserConstraints CLASS IN pulser_interface.py FOR AVAILABLE CONSTRAINTS!!! If you are not sure about the meaning, look in other hardware files to get an impression. If still additional constraints are needed, then they have to be added to the PulserConstraints class. Each scalar parameter is an ScalarConstraints object defined in cor.util.interfaces. Essentially it contains min/max values as well as min step size, default value and unit of the parameter. PulserConstraints.activation_config differs, since it contain the channel configuration/activation information of the form: {<descriptor_str>: <channel_set>, <descriptor_str>: <channel_set>, ...} If the constraints cannot be set in the pulsing hardware (e.g. because it might have no sequence mode) just leave it out so that the default is used (only zeros). """ constraints = PulserConstraints() constraints.sample_rate.min = 500e6 constraints.sample_rate.max = 950e6 constraints.sample_rate.step = 450e6 constraints.sample_rate.default = 950e6 constraints.a_ch_amplitude.min = 0.0 constraints.a_ch_amplitude.max = 0.0 constraints.a_ch_amplitude.step = 0.0 constraints.a_ch_amplitude.default = 0.0 constraints.a_ch_offset.min = 0.0 constraints.a_ch_offset.max = 0.0 constraints.a_ch_offset.step = 0.0 constraints.a_ch_offset.default = 0.0 constraints.d_ch_low.min = 0.0 constraints.d_ch_low.max = 0.0 constraints.d_ch_low.step = 0.0 constraints.d_ch_low.default = 0.0 constraints.d_ch_high.min = 3.3 constraints.d_ch_high.max = 3.3 constraints.d_ch_high.step = 0.0 constraints.d_ch_high.default = 3.3 constraints.waveform_length.min = 1024 constraints.waveform_length.max = 134217728 constraints.waveform_length.step = 1 constraints.waveform_length.default = 1024 # the name a_ch<num> and d_ch<num> are generic names, which describe UNAMBIGUOUSLY the # channels. Here all possible channel configurations are stated, where only the generic # names should be used. The names for the different configurations can be customary chosen. activation_config = OrderedDict() activation_config['all'] = frozenset( {'d_ch1', 'd_ch2', 'd_ch3', 'd_ch4', 'd_ch5', 'd_ch6', 'd_ch7', 'd_ch8'}) constraints.activation_config = activation_config constraints.sequence_option = SequenceOption.NON return constraints def pulser_on(self): """ Switches the pulsing device on. @return int: error code (0:OK, -1:error) """ self.__current_status = 1 return self.write(0x01) def pulser_off(self): """ Switches the pulsing device off. @return int: error code (0:OK, -1:error) """ self.__current_status = 0 return self.write(0x00) def load_waveform(self, load_dict): """ Loads a waveform to the specified channel of the pulsing device. For devices that have a workspace (i.e. AWG) this will load the waveform from the device workspace into the channel. For a device without mass memory this will make the waveform/pattern that has been previously written with self.write_waveform ready to play. @param dict|list load_dict: a dictionary with keys being one of the available channel index and values being the name of the already written waveform to load into the channel. Examples: {1: rabi_ch1, 2: rabi_ch2} or {1: rabi_ch2, 2: rabi_ch1} If just a list of waveform names if given, the channel association will be invoked from the channel suffix '_ch1', '_ch2' etc. @return dict: Dictionary containing the actually loaded waveforms per channel. """ # Since only one waveform can be present at a time check if only a single name is given if isinstance(load_dict, list): waveforms = list(set(load_dict)) elif isinstance(load_dict, dict): waveforms = list(set(load_dict.values())) else: self.log.error('Method load_waveform expects a list of waveform names or a dict.') return self.get_loaded_assets()[0] if len(waveforms) != 1: self.log.error('FPGA pulser expects exactly one waveform name for load_waveform.') return self.get_loaded_assets()[0] waveform = waveforms[0] if waveform != self.__current_waveform_name: self.log.error('No waveform by the name "{0}" generated for FPGA pulser.\n' 'Only one waveform at a time can be held.'.format(waveform)) return self.get_loaded_assets()[0] # calculate size of the two bytearrays to be transmitted. The biggest part is tranfered # in 1024 byte blocks and the rest is transfered in 32 byte blocks big_bytesize = (len(self.__current_waveform) // 1024) * 1024 small_bytesize = len(self.__current_waveform) - big_bytesize # try repeatedly to upload the samples to the FPGA RAM # stop if the upload was successful loop_count = 0 while True: loop_count += 1 # reset FPGA self.reset() # upload sequence if big_bytesize != 0: # enable sequence write mode in FPGA self.write((255 << 24) + 2) # write to FPGA DDR2-RAM self.fpga.WriteToBlockPipeIn(0x80, 1024, self.__current_waveform[0:big_bytesize]) if small_bytesize != 0: # enable sequence write mode in FPGA self.write((8 << 24) + 2) # write to FPGA DDR2-RAM self.fpga.WriteToBlockPipeIn(0x80, 32, self.__current_waveform[big_bytesize:]) # check if upload was successful self.write(0x00) # start the pulse sequence self.__current_status = 1 self.write(0x01) # wait for 600ms time.sleep(0.6) # get status flags from FPGA flags = self.query() self.__current_status = 0 self.write(0x00) # check if the memory readout works. if flags == 0: self.log.info('Loading of waveform "{0}" to FPGA was successful.\n' 'Upload attempts needed: {1}'.format(waveform, loop_count)) self.__currently_loaded_waveform = waveform break if loop_count == 10: self.log.error('Unable to upload waveform to FPGA.\n' 'Abort loading after 10 failed attempts.') self.reset() break return self.get_loaded_assets()[0] def load_sequence(self, sequence_name): """ Loads a sequence to the channels of the device in order to be ready for playback. For devices that have a workspace (i.e. AWG) this will load the sequence from the device workspace into the channels. For a device without mass memory this will make the waveform/pattern that has been previously written with self.write_waveform ready to play. @param dict|list sequence_name: a dictionary with keys being one of the available channel index and values being the name of the already written waveform to load into the channel. Examples: {1: rabi_ch1, 2: rabi_ch2} or {1: rabi_ch2, 2: rabi_ch1} If just a list of waveform names if given, the channel association will be invoked from the channel suffix '_ch1', '_ch2' etc. @return dict: Dictionary containing the actually loaded waveforms per channel. """ self.log.warning('FPGA digital pulse generator has no sequencing capabilities.\n' 'load_sequence call ignored.') return dict() def get_loaded_assets(self): """ Retrieve the currently loaded asset names for each active channel of the device. The returned dictionary will have the channel numbers as keys. In case of loaded waveforms the dictionary values will be the waveform names. In case of a loaded sequence the values will be the sequence name appended by a suffix representing the track loaded to the respective channel (i.e. '<sequence_name>_1'). @return (dict, str): Dictionary with keys being the channel number and values being the respective asset loaded into the channel, string describing the asset type ('waveform' or 'sequence') """ asset_type = 'waveform' if self.__currently_loaded_waveform else None asset_dict = {chnl_num: self.__currently_loaded_waveform for chnl_num in range(1, 9)} return asset_dict, asset_type def clear_all(self): """ Clears all loaded waveforms from the pulse generators RAM/workspace. @return int: error code (0:OK, -1:error) """ self.pulser_off() self.__currently_loaded_waveform = '' self.__current_waveform_name = '' # just for good measures, write and load a empty waveform self.__current_waveform = bytearray(np.zeros(32)) self.__samples_written = 32 self.load_waveform([self.__current_waveform_name]) return 0 def get_status(self): """ Retrieves the status of the pulsing hardware @return (int, dict): tuple with an integer value of the current status and a corresponding dictionary containing status description for all the possible status variables of the pulse generator hardware. """ status_dic = dict() status_dic[-1] = 'Failed Request or Failed Communication with device.' status_dic[0] = 'Device has stopped, but can receive commands.' status_dic[1] = 'Device is active and running.' return self.__current_status, status_dic def get_sample_rate(self): """ Get the sample rate of the pulse generator hardware @return float: The current sample rate of the device (in Hz) """ return self.__sample_rate def set_sample_rate(self, sample_rate): """ Set the sample rate of the pulse generator hardware. @param float sample_rate: The sampling rate to be set (in Hz) @return float: the sample rate returned from the device (in Hz). Note: After setting the sampling rate of the device, use the actually set return value for further processing. """ if self.__current_status == 1: self.log.error('Can`t change the sample rate while the FPGA is running.') return self.__sample_rate # Round sample rate either to 500MHz or 950MHz since no other values are possible. if sample_rate < 725e6: self.__sample_rate = 500e6 bitfile_name = 'pulsegen_8chnl_500MHz_{0}.bit'.format(self._fpga_type.split('_')[1]) else: self.__sample_rate = 950e6 bitfile_name = 'pulsegen_8chnl_950MHz_{0}.bit'.format(self._fpga_type.split('_')[1]) bitfile_path = os.path.join(get_main_dir(), 'thirdparty', 'qo_fpga', bitfile_name) self.fpga.ConfigureFPGA(bitfile_path) self.log.info('FPGA pulse generator configured with {0}'.format(bitfile_path)) if self.fpga.IsFrontPanel3Supported(): self._fp3support = True else: self._fp3support = False self.log.warning('FrontPanel3 is not supported. ' 'Please check if the FPGA is directly connected by USB3.') self.__current_status = 0 return self.__sample_rate def get_analog_level(self, amplitude=None, offset=None): """ Retrieve the analog amplitude and offset of the provided channels. @param list amplitude: optional, if the amplitude value (in Volt peak to peak, i.e. the full amplitude) of a specific channel is desired. @param list offset: optional, if the offset value (in Volt) of a specific channel is desired. @return: (dict, dict): tuple of two dicts, with keys being the channel descriptor string (i.e. 'a_ch1') and items being the values for those channels. Amplitude is always denoted in Volt-peak-to-peak and Offset in volts. Note: Do not return a saved amplitude and/or offset value but instead retrieve the current amplitude and/or offset directly from the device. If nothing (or None) is passed then the levels of all channels will be returned. If no analog channels are present in the device, return just empty dicts. Example of a possible input: amplitude = ['a_ch1', 'a_ch4'], offset = None to obtain the amplitude of channel 1 and 4 and the offset of all channels {'a_ch1': -0.5, 'a_ch4': 2.0} {'a_ch1': 0.0, 'a_ch2': 0.0, 'a_ch3': 1.0, 'a_ch4': 0.0} """ self.log.warning('The FPGA has no analog channels.') return dict(), dict() def set_analog_level(self, amplitude=None, offset=None): """ Set amplitude and/or offset value of the provided analog channel(s). @param dict amplitude: dictionary, with key being the channel descriptor string (i.e. 'a_ch1', 'a_ch2') and items being the amplitude values (in Volt peak to peak, i.e. the full amplitude) for the desired channel. @param dict offset: dictionary, with key being the channel descriptor string (i.e. 'a_ch1', 'a_ch2') and items being the offset values (in absolute volt) for the desired channel. @return (dict, dict): tuple of two dicts with the actual set values for amplitude and offset for ALL channels. If nothing is passed then the command will return the current amplitudes/offsets. Note: After setting the amplitude and/or offset values of the device, use the actual set return values for further processing. """ self.log.warning('The FPGA has no analog channels.') return dict(), dict() def get_digital_level(self, low=None, high=None): """ Retrieve the digital low and high level of the provided/all channels. @param list low: optional, if the low value (in Volt) of a specific channel is desired. @param list high: optional, if the high value (in Volt) of a specific channel is desired. @return: (dict, dict): tuple of two dicts, with keys being the channel descriptor strings (i.e. 'd_ch1', 'd_ch2') and items being the values for those channels. Both low and high value of a channel is denoted in volts. Note: Do not return a saved low and/or high value but instead retrieve the current low and/or high value directly from the device. If nothing (or None) is passed then the levels of all channels are being returned. If no digital channels are present, return just an empty dict. Example of a possible input: low = ['d_ch1', 'd_ch4'] to obtain the low voltage values of digital channel 1 an 4. A possible answer might be {'d_ch1': -0.5, 'd_ch4': 2.0} {'d_ch1': 1.0, 'd_ch2': 1.0, 'd_ch3': 1.0, 'd_ch4': 4.0} Since no high request was performed, the high values for ALL channels are returned (here 4). """ if low: low_dict = {chnl: 0.0 for chnl in low} else: low_dict = {'d_ch{0:d}'.format(chnl + 1): 0.0 for chnl in range(8)} if high: high_dict = {chnl: 3.3 for chnl in high} else: high_dict = {'d_ch{0:d}'.format(chnl + 1): 3.3 for chnl in range(8)} return low_dict, high_dict def set_digital_level(self, low=None, high=None): """ Set low and/or high value of the provided digital channel. @param dict low: dictionary, with key being the channel descriptor string (i.e. 'd_ch1', 'd_ch2') and items being the low values (in volt) for the desired channel. @param dict high: dictionary, with key being the channel descriptor string (i.e. 'd_ch1', 'd_ch2') and items being the high values (in volt) for the desired channel. @return (dict, dict): tuple of two dicts where first dict denotes the current low value and the second dict the high value for ALL digital channels. Keys are the channel descriptor strings (i.e. 'd_ch1', 'd_ch2') If nothing is passed then the command will return the current voltage levels. Note: After setting the high and/or low values of the device, use the actual set return values for further processing. """ self.log.warning('FPGA pulse generator logic level cannot be adjusted!') return self.get_digital_level() def get_active_channels(self, ch=None): """ Get the active channels of the pulse generator hardware. @param list ch: optional, if specific analog or digital channels are needed to be asked without obtaining all the channels. @return dict: where keys denoting the channel string and items boolean expressions whether channel are active or not. Example for an possible input (order is not important): ch = ['a_ch2', 'd_ch2', 'a_ch1', 'd_ch5', 'd_ch1'] then the output might look like {'a_ch2': True, 'd_ch2': False, 'a_ch1': False, 'd_ch5': True, 'd_ch1': False} If no parameter (or None) is passed to this method all channel states will be returned. """ if ch: d_ch_dict = {chnl: True for chnl in ch} else: d_ch_dict = {'d_ch1': True, 'd_ch2': True, 'd_ch3': True, 'd_ch4': True, 'd_ch5': True, 'd_ch6': True, 'd_ch7': True, 'd_ch8': True} return d_ch_dict def set_active_channels(self, ch=None): """ Set the active/inactive channels for the pulse generator hardware. The state of ALL available analog and digital channels will be returned (True: active, False: inactive). The actually set and returned channel activation must be part of the available activation_configs in the constraints. You can also activate/deactivate subsets of available channels but the resulting activation_config must still be valid according to the constraints. If the resulting set of active channels can not be found in the available activation_configs, the channel states must remain unchanged. @param dict ch: dictionary with keys being the analog or digital string generic names for the channels (i.e. 'd_ch1', 'a_ch2') with items being a boolean value. True: Activate channel, False: Deactivate channel @return dict: with the actual set values for ALL active analog and digital channels If nothing is passed then the command will simply return the unchanged current state. Note: After setting the active channels of the device, use the returned dict for further processing. Example for possible input: ch={'a_ch2': True, 'd_ch1': False, 'd_ch3': True, 'd_ch4': True} to activate analog channel 2 digital channel 3 and 4 and to deactivate digital channel 1. All other available channels will remain unchanged. """ self.log.warning('The channels of the FPGA are always active.') return self.get_active_channels() def write_waveform(self, name, analog_samples, digital_samples, is_first_chunk, is_last_chunk, total_number_of_samples): """ Write a new waveform or append samples to an already existing waveform on the device memory. The flags is_first_chunk and is_last_chunk can be used as indicator if a new waveform should be created or if the write process to a waveform should be terminated. NOTE: All sample arrays in analog_samples and digital_samples must be of equal length! @param str name: the name of the waveform to be created/append to @param dict analog_samples: keys are the generic analog channel names (i.e. 'a_ch1') and values are 1D numpy arrays of type float32 containing the voltage samples. @param dict digital_samples: keys are the generic digital channel names (i.e. 'd_ch1') and values are 1D numpy arrays of type bool containing the marker states. @param bool is_first_chunk: Flag indicating if it is the first chunk to write. If True this method will create a new empty wavveform. If False the samples are appended to the existing waveform. @param bool is_last_chunk: Flag indicating if it is the last chunk to write. Some devices may need to know when to close the appending wfm. @param int total_number_of_samples: The number of sample points for the entire waveform (not only the currently written chunk) @return (int, list): Number of samples written (-1 indicates failed process) and list of created waveform names """ if self.__current_status != 0: self.log.error('FPGA is not idle, so the waveform can`t be written at this time.') return -1, list() if analog_samples: self.log.error('FPGA pulse generator is purely digital and does not support waveform ' 'generation with analog samples.') return -1, list() if not digital_samples: if total_number_of_samples > 0: self.log.warning('No samples handed over for waveform generation.') return -1, list() else: self.__current_waveform = bytearray(np.zeros(32)) self.__samples_written = 32 self.__current_waveform_name = '' return 0, list() # Initialize waveform array if this is the first chunk to write # Also append zero-timebins to waveform if the length is no integer multiple of 32 if is_first_chunk: self.__samples_written = 0 self.__current_waveform_name = name if total_number_of_samples % 32 != 0: number_of_zeros = 32 - (total_number_of_samples % 32) self.__current_waveform = np.zeros(total_number_of_samples + number_of_zeros, dtype='uint8') self.log.warning('FPGA pulse sequence length is no integer multiple of 32 samples.' '\nAppending {0:d} zero-samples to the sequence.' ''.format(number_of_zeros)) else: self.__current_waveform = np.zeros(total_number_of_samples, dtype='uint8') # Determine which part of the waveform array should be written chunk_length = len(digital_samples[list(digital_samples)[0]]) write_end_index = self.__samples_written + chunk_length # Encode samples for each channel in bit mask and create waveform array for chnl, samples in digital_samples.items(): # get channel index in range 0..7 chnl_ind = int(chnl.rsplit('ch', 1)[1]) - 1 # Represent bool values as np.uint8 uint8_samples = samples.view('uint8') # left shift 0/1 values to bit position corresponding to channel index np.left_shift(uint8_samples, chnl_ind, out=uint8_samples) # Add samples to waveform array np.add(self.__current_waveform[self.__samples_written:write_end_index], uint8_samples, out=self.__current_waveform[self.__samples_written:write_end_index]) # Convert numpy array to bytearray self.__current_waveform = bytearray(self.__current_waveform.tobytes()) # increment the current write index self.__samples_written += chunk_length return chunk_length, [self.__current_waveform_name] def write_sequence(self, name, sequence_parameters): """ Write a new sequence on the device memory. @param str name: the name of the waveform to be created/append to @param dict sequence_parameters: dictionary containing the parameters for a sequence @return: int, number of sequence steps written (-1 indicates failed process) """ self.log.warning('FPGA digital pulse generator has no sequencing capabilities.\n' 'write_sequence call ignored.') return -1 def get_waveform_names(self): """ Retrieve the names of all uploaded waveforms on the device. @return list: List of all uploaded waveform name strings in the device workspace. """ waveform_names = list() if self.__current_waveform_name != '' and self.__current_waveform_name is not None: waveform_names = [self.__current_waveform_name] return waveform_names def get_sequence_names(self): """ Retrieve the names of all uploaded sequence on the device. @return list: List of all uploaded sequence name strings in the device workspace. """ return list() def delete_waveform(self, waveform_name): """ Delete the waveform with name "waveform_name" from the device memory. @param str waveform_name: The name of the waveform to be deleted Optionally a list of waveform names can be passed. @return list: a list of deleted waveform names. """ return list() def delete_sequence(self, sequence_name): """ Delete the sequence with name "sequence_name" from the device memory. @param str sequence_name: The name of the sequence to be deleted Optionally a list of sequence names can be passed. @return list: a list of deleted sequence names. """ return list() def get_interleave(self): """ Check whether Interleave is ON or OFF in AWG. @return bool: True: ON, False: OFF Will always return False for pulse generator hardware without interleave. """ return False def set_interleave(self, state=False): """ Turns the interleave of an AWG on or off. @param bool state: The state the interleave should be set to (True: ON, False: OFF) @return bool: actual interleave status (True: ON, False: OFF) Note: After setting the interleave of the device, retrieve the interleave again and use that information for further processing. Unused for pulse generator hardware other than an AWG. """ if state: self.log.error('No interleave functionality available in FPGA pulser.\n' 'Interleave state is always False.') return False def write(self, command): """ Sends a command string to the device. @param str command: string containing the command @return int: error code (0:OK, -1:error) """ if not isinstance(command, int): return -1 self.fpga.SetWireInValue(0x00, command) self.fpga.UpdateWireIns() return 0 def query(self, question=None): """ Asks the device a 'question' and receive and return an answer from it. @param str question: string containing the command @return string: the answer of the device to the 'question' in a string """ self.fpga.UpdateWireOuts() return self.fpga.GetWireOutValue(0x20) def reset(self): """ Reset the device. @return int: error code (0:OK, -1:error) """ self.write(0x04) self.write(0x00) return 0 def _connect_fpga(self): # connect to FPGA by serial number self.fpga.OpenBySerial(self._fpga_serial) # upload configuration bitfile to FPGA self.set_sample_rate(self.__sample_rate) # Check connection if not self.fpga.IsFrontPanelEnabled(): self.current_status = -1 self.log.error('ERROR: FrontPanel is not enabled in FPGA pulse generator!') self.__current_status = -1 return self.__current_status else: self.current_status = 0 self.log.info('FPGA pulse generator connected') return self.__current_status def _disconnect_fpga(self): """ stop FPGA and disconnect """ # set FPGA in reset state self.write(0x04) self.__current_status = -1 del self.fpga return self.__current_status
class SpectrumLogic(GenericLogic): """This logic module gathers data from the spectrometer. """ _modclass = 'spectrumlogic' _modtype = 'logic' # declare connectors spectrometer = Connector(interface='SpectrometerInterface') odmrlogic = Connector(interface='ODMRLogic') savelogic = Connector(interface='SaveLogic') fitlogic = Connector(interface='FitLogic') # declare status variables _spectrum_data = StatusVar('spectrum_data', np.empty((2, 0))) _spectrum_background = StatusVar('spectrum_background', np.empty((2, 0))) _background_correction = StatusVar('background_correction', False) fc = StatusVar('fits', None) # Internal signals sig_specdata_updated = QtCore.Signal() sig_next_diff_loop = QtCore.Signal() # External signals eg for GUI module spectrum_fit_updated_Signal = QtCore.Signal(np.ndarray, dict, str) fit_domain_updated_Signal = QtCore.Signal(np.ndarray) def __init__(self, **kwargs): """ Create SpectrometerLogic object with connectors. @param dict kwargs: optional parameters """ super().__init__(**kwargs) # locking for thread safety self.threadlock = Mutex() def on_activate(self): """ Initialisation performed during activation of the module. """ self._spectrum_data_corrected = np.array([]) self._calculate_corrected_spectrum() self.spectrum_fit = np.array([]) self.fit_domain = np.array([]) self.diff_spec_data_mod_on = np.array([]) self.diff_spec_data_mod_off = np.array([]) self.repetition_count = 0 # count loops for differential spectrum self._spectrometer_device = self.spectrometer() self._odmr_logic = self.odmrlogic() self._save_logic = self.savelogic() self.sig_next_diff_loop.connect(self._loop_differential_spectrum) self.sig_specdata_updated.emit() def on_deactivate(self): """ Deinitialisation performed during deactivation of the module. """ if self.module_state() != 'idle' and self.module_state() != 'deactivated': pass @fc.constructor def sv_set_fits(self, val): """ Set up fit container """ fc = self.fitlogic().make_fit_container('ODMR sum', '1d') fc.set_units(['m', 'c/s']) if isinstance(val, dict) and len(val) > 0: fc.load_from_dict(val) else: d1 = OrderedDict() d1['Gaussian peak'] = { 'fit_function': 'gaussian', 'estimator': 'peak' } default_fits = OrderedDict() default_fits['1d'] = d1 fc.load_from_dict(default_fits) return fc @fc.representer def sv_get_fits(self, val): """ save configured fits """ if len(val.fit_list) > 0: return val.save_to_dict() else: return None def get_single_spectrum(self, background=False): """ Record a single spectrum from the spectrometer. """ # Clear any previous fit self.fc.clear_result() if background: self._spectrum_background = netobtain(self._spectrometer_device.recordSpectrum()) else: self._spectrum_data = netobtain(self._spectrometer_device.recordSpectrum()) self._calculate_corrected_spectrum() # Clearing the differential spectra data arrays so that they do not get # saved with this single spectrum. self.diff_spec_data_mod_on = np.array([]) self.diff_spec_data_mod_off = np.array([]) self.sig_specdata_updated.emit() def _calculate_corrected_spectrum(self): self._spectrum_data_corrected = np.copy(self._spectrum_data) if len(self._spectrum_background) == 2 \ and len(self._spectrum_background[1, :]) == len(self._spectrum_data[1, :]): self._spectrum_data_corrected[1, :] -= self._spectrum_background[1, :] else: self.log.warning('Background spectrum has a different dimension then the acquired spectrum. ' 'Returning raw spectrum. ' 'Try acquiring a new background spectrum.') @property def spectrum_data(self): if self._background_correction: self._calculate_corrected_spectrum() return self._spectrum_data_corrected else: return self._spectrum_data @property def background_correction(self): return self._background_correction @background_correction.setter def background_correction(self, correction=None): if correction is None or correction: self._background_correction = True else: self._background_correction = False self.sig_specdata_updated.emit() def save_raw_spectrometer_file(self, path='', postfix=''): """Ask the hardware device to save its own raw file. """ # TODO: sanity check the passed parameters. self._spectrometer_device.saveSpectrum(path, postfix=postfix) def start_differential_spectrum(self): """Start a differential spectrum acquisition. An initial spectrum is recorded to initialise the data arrays to the right size. """ self._continue_differential = True # Taking a demo spectrum gives us the wavelength values and the length of the spectrum data. demo_data = netobtain(self._spectrometer_device.recordSpectrum()) wavelengths = demo_data[0, :] empty_signal = np.zeros(len(wavelengths)) # Using this information to initialise the differential spectrum data arrays. self._spectrum_data = np.array([wavelengths, empty_signal]) self.diff_spec_data_mod_on = np.array([wavelengths, empty_signal]) self.diff_spec_data_mod_off = np.array([wavelengths, empty_signal]) self.repetition_count = 0 # Starting the measurement loop self._loop_differential_spectrum() def resume_differential_spectrum(self): """Resume a differential spectrum acquisition. """ self._continue_differential = True # Starting the measurement loop self._loop_differential_spectrum() def _loop_differential_spectrum(self): """ This loop toggles the modulation and iteratively records a differential spectrum. """ # If the loop should not continue, then return immediately without # emitting any signal to repeat. if not self._continue_differential: return # Otherwise, we make a measurement and then emit a signal to repeat this loop. # Toggle on, take spectrum and add data to the mod_on data self.toggle_modulation(on=True) these_data = netobtain(self._spectrometer_device.recordSpectrum()) self.diff_spec_data_mod_on[1, :] += these_data[1, :] # Toggle off, take spectrum and add data to the mod_off data self.toggle_modulation(on=False) these_data = netobtain(self._spectrometer_device.recordSpectrum()) self.diff_spec_data_mod_off[1, :] += these_data[1, :] self.repetition_count += 1 # increment the loop count # Calculate the differential spectrum self._spectrum_data[1, :] = self.diff_spec_data_mod_on[ 1, :] - self.diff_spec_data_mod_off[1, :] self.sig_specdata_updated.emit() self.sig_next_diff_loop.emit() def stop_differential_spectrum(self): """Stop an ongoing differential spectrum acquisition """ self._continue_differential = False def toggle_modulation(self, on): """ Toggle the modulation. """ if on: self._odmr_logic.mw_cw_on() elif not on: self._odmr_logic.mw_off() else: print("Parameter 'on' needs to be boolean") def save_spectrum_data(self, background=False, name_tag='', custom_header = None): """ Saves the current spectrum data to a file. @param bool background: Whether this is a background spectrum (dark field) or not. @param string name_tag: postfix name tag for saved filename. @param OrderedDict custom_header: This ordered dictionary is added to the default data file header. It allows arbitrary additional experimental information to be included in the saved data file header. """ filepath = self._save_logic.get_path_for_module(module_name='spectra') if background: filelabel = 'background' spectrum_data = self._spectrum_background else: filelabel = 'spectrum' spectrum_data = self._spectrum_data # Add name_tag as postfix to filename if name_tag != '': filelabel = filelabel + '_' + name_tag # write experimental parameters parameters = OrderedDict() parameters['Spectrometer acquisition repetitions'] = self.repetition_count # add all fit parameter to the saved data: if self.fc.current_fit_result is not None: parameters['Fit function'] = self.fc.current_fit for name, param in self.fc.current_fit_param.items(): parameters[name] = str(param) # add any custom header params if custom_header is not None: for key in custom_header: parameters[key] = custom_header[key] # prepare the data in an OrderedDict: data = OrderedDict() data['wavelength'] = spectrum_data[0, :] # If the differential spectra arrays are not empty, save them as raw data if len(self.diff_spec_data_mod_on) != 0 and len(self.diff_spec_data_mod_off) != 0: data['signal_mod_on'] = self.diff_spec_data_mod_on[1, :] data['signal_mod_off'] = self.diff_spec_data_mod_off[1, :] data['differential'] = spectrum_data[1, :] else: data['signal'] = spectrum_data[1, :] if not background and len(self._spectrum_data_corrected) != 0: data['corrected'] = self._spectrum_data_corrected[1, :] fig = self.draw_figure() # Save to file self._save_logic.save_data(data, filepath=filepath, parameters=parameters, filelabel=filelabel, plotfig=fig) self.log.debug('Spectrum saved to:\n{0}'.format(filepath)) def draw_figure(self): """ Draw the summary plot to save with the data. @return fig fig: a matplotlib figure object to be saved to file. """ wavelength = self.spectrum_data[0, :] * 1e9 # convert m to nm for plot spec_data = self.spectrum_data[1, :] prefix = ['', 'k', 'M', 'G', 'T'] prefix_index = 0 rescale_factor = 1 # Rescale spectrum data with SI prefix while np.max(spec_data) / rescale_factor > 1000: rescale_factor = rescale_factor * 1000 prefix_index = prefix_index + 1 intensity_prefix = prefix[prefix_index] # Prepare the figure to save as a "data thumbnail" plt.style.use(self._save_logic.mpl_qd_style) fig, ax1 = plt.subplots() ax1.plot(wavelength, spec_data / rescale_factor, linestyle=':', linewidth=0.5 ) # If there is a fit, plot it also if self.fc.current_fit_result is not None: ax1.plot(self.spectrum_fit[0] * 1e9, # convert m to nm for plot self.spectrum_fit[1] / rescale_factor, marker='None' ) ax1.set_xlabel('Wavelength (nm)') ax1.set_ylabel('Intensity ({}count)'.format(intensity_prefix)) fig.tight_layout() return fig ################ # Fitting things def get_fit_functions(self): """ Return the hardware constraints/limits @return list(str): list of fit function names """ return list(self.fc.fit_list) def do_fit(self, fit_function=None, x_data=None, y_data=None): """ Execute the currently configured fit on the measurement data. Optionally on passed data @param string fit_function: The name of one of the defined fit functions. @param array x_data: wavelength data for spectrum. @param array y_data: intensity data for spectrum. """ if (x_data is None) or (y_data is None): x_data = self.spectrum_data[0] y_data = self.spectrum_data[1] if self.fit_domain.any(): start_idx = self._find_nearest_idx(x_data, self.fit_domain[0]) stop_idx = self._find_nearest_idx(x_data, self.fit_domain[1]) x_data = x_data[start_idx:stop_idx] y_data = y_data[start_idx:stop_idx] if fit_function is not None and isinstance(fit_function, str): if fit_function in self.get_fit_functions(): self.fc.set_current_fit(fit_function) else: self.fc.set_current_fit('No Fit') if fit_function != 'No Fit': self.log.warning('Fit function "{0}" not available in Spectrum logic ' 'fit container.'.format(fit_function) ) spectrum_fit_x, spectrum_fit_y, result = self.fc.do_fit(x_data, y_data) self.spectrum_fit = np.array([spectrum_fit_x, spectrum_fit_y]) if result is None: result_str_dict = {} else: result_str_dict = result.result_str_dict self.spectrum_fit_updated_Signal.emit(self.spectrum_fit, result_str_dict, self.fc.current_fit ) return def _find_nearest_idx(self, array, value): """ Find array index of element nearest to given value @param list array: array to be searched. @param float value: desired value. @return index of nearest element. """ idx = (np.abs(array-value)).argmin() return idx def set_fit_domain(self, domain=None): """ Set the fit domain to a user specified portion of the data. If no domain is given, then this method sets the fit domain to match the full data domain. @param np.array domain: two-element array containing min and max of domain. """ if domain is not None: self.fit_domain = domain else: self.fit_domain = np.array([self.spectrum_data[0, 0], self.spectrum_data[0, -1]]) self.fit_domain_updated_Signal.emit(self.fit_domain)
class PIDLogic(GenericLogic): """ Control a process via software PID. """ _modclass = 'pidlogic' _modtype = 'logic' ## declare connectors controller = Connector(interface='PIDControllerInterface') savelogic = Connector(interface='SaveLogic') # status vars bufferLength = StatusVar('bufferlength', 1000) timestep = StatusVar(default=100) # signals sigUpdateDisplay = QtCore.Signal() def __init__(self, config, **kwargs): super().__init__(config=config, **kwargs) self.log.debug('The following configuration was found.') #number of lines in the matrix plot self.NumberOfSecondsLog = 100 self.threadlock = Mutex() def on_activate(self): """ Initialisation performed during activation of the module. """ self._controller = self.get_connector('controller') self._save_logic = self.get_connector('savelogic') self.history = np.zeros([3, self.bufferLength]) self.savingState = False self.enabled = False self.timer = QtCore.QTimer() self.timer.setSingleShot(True) self.timer.setInterval(self.timestep) self.timer.timeout.connect(self.loop) def on_deactivate(self): """ Perform required deactivation. """ pass def getBufferLength(self): """ Get the current data buffer length. """ return self.bufferLength def startLoop(self): """ Start the data recording loop. """ self.enabled = True self.timer.start(self.timestep) def stopLoop(self): """ Stop the data recording loop. """ self.enabled = False def loop(self): """ Execute step in the data recording loop: save one of each control and process values """ self.history = np.roll(self.history, -1, axis=1) self.history[0, -1] = self._controller.get_process_value() self.history[1, -1] = self._controller.get_control_value() self.history[2, -1] = self._controller.get_setpoint() self.sigUpdateDisplay.emit() if self.enabled: self.timer.start(self.timestep) def getSavingState(self): """ Return whether we are saving data @return bool: whether we are saving data right now """ return self.savingState def startSaving(self): """ Start saving data. Function does nothing right now. """ pass def saveData(self): """ Stop saving data and write data to file. Function does nothing right now. """ pass def setBufferLength(self, newBufferLength): """ Change buffer length to new value. @param int newBufferLength: new buffer length """ self.bufferLength = newBufferLength self.history = np.zeros([3, self.bufferLength]) def get_kp(self): """ Return the proportional constant. @return float: proportional constant of PID controller """ return self._controller.get_kp() def set_kp(self, kp): """ Set the proportional constant of the PID controller. @prarm float kp: proportional constant of PID controller """ return self._controller.set_kp(kp) def get_ki(self): """ Get the integration constant of the PID controller @return float: integration constant of the PID controller """ return self._controller.get_ki() def set_ki(self, ki): """ Set the integration constant of the PID controller. @param float ki: integration constant of the PID controller """ return self._controller.set_ki(ki) def get_kd(self): """ Get the derivative constant of the PID controller @return float: the derivative constant of the PID controller """ return self._controller.get_kd() def set_kd(self, kd): """ Set the derivative constant of the PID controller @param float kd: the derivative constant of the PID controller """ return self._controller.set_kd(kd) def get_setpoint(self): """ Get the current setpoint of the PID controller. @return float: current set point of the PID controller """ return self.history[2, -1] def set_setpoint(self, setpoint): """ Set the current setpoint of the PID controller. @param float setpoint: new set point of the PID controller """ self._controller.set_setpoint(setpoint) def get_manual_value(self): """ Return the control value for manual mode. @return float: control value for manual mode """ return self._controller.get_manual_value() def set_manual_value(self, manualvalue): """ Set the control value for manual mode. @param float manualvalue: control value for manual mode of controller """ return self._controller.set_manual_value(manualvalue) def get_enabled(self): """ See if the PID controller is controlling a process. @return bool: whether the PID controller is preparing to or conreolling a process """ return self.enabled def set_enabled(self, enabled): """ Set the state of the PID controller. @param bool enabled: desired state of PID controller """ if enabled and not self.enabled: self.startLoop() if not enabled and self.enabled: self.stopLoop() def get_control_limits(self): """ Get the minimum and maximum value of the control actuator. @return list(float): (minimum, maximum) values of the control actuator """ return self._controller.get_control_limits() def set_control_limits(self, limits): """ Set the minimum and maximum value of the control actuator. @param list(float) limits: (minimum, maximum) values of the control actuator This function does nothing, control limits are handled by the control module """ return self._controller.set_control_limits(limits) def get_pv(self): """ Get current process input value. @return float: current process input value """ return self.history[0, -1] def get_cv(self): """ Get current control output value. @return float: control output value """ return self.history[1, -1]
class ManagerGui(GUIBase): """This class provides a GUI to the Qudi manager. @signal sigStartAll: sent when all modules should be loaded @signal str str sigStartThis: load a specific module @signal str str sigReloadThis reload a specific module from Python code @signal str str sigStopThis: stop all actions of a module and remove references It supports module loading, reloading, logging and other administrative tasks. """ # status vars consoleFontSize = StatusVar('console_font_size', 10) # signals sigStartAll = QtCore.Signal() sigStartModule = QtCore.Signal(str, str) sigReloadModule = QtCore.Signal(str, str) sigCleanupStatus = QtCore.Signal(str, str) sigStopModule = QtCore.Signal(str, str) sigLoadConfig = QtCore.Signal(str, bool) sigSaveConfig = QtCore.Signal(str) sigRealQuit = QtCore.Signal() def __init__(self, **kwargs): """Create an instance of the module. @param object manager: @param str name: @param dict config: """ super().__init__(**kwargs) self.modlist = list() self.modules = set() def on_activate(self): """ Activation method called on change to active state. This method creates the Manager main window. """ if _has_pyqtgraph: # set background of pyqtgraph testwidget = QWidget() testwidget.ensurePolished() bgcolor = testwidget.palette().color(QPalette.Normal, testwidget.backgroundRole()) # set manually the background color in hex code according to our # color scheme: pg.setConfigOption('background', bgcolor) # opengl usage if 'useOpenGL' in self._manager.tree['global']: pg.setConfigOption('useOpenGL', self._manager.tree['global']['useOpenGL']) self._mw = ManagerMainWindow() self.restoreWindowPos(self._mw) self.errorDialog = ErrorDialog(self) self._about = AboutDialog() version = self.getSoftwareVersion() configFile = self._manager.configFile self._about.label.setText( '<a href=\"https://github.com/Ulm-IQO/qudi/commit/{0}\"' ' style=\"color: cyan;\"> {0} </a>, on branch {1}.'.format( version[0], version[1])) self.versionLabel = QtWidgets.QLabel() self.versionLabel.setText( '<a href=\"https://github.com/Ulm-IQO/qudi/commit/{0}\"' ' style=\"color: cyan;\"> {0} </a>,' ' on branch {1}, configured from {2}'.format( version[0], version[1], configFile)) self.versionLabel.setOpenExternalLinks(True) self._mw.statusBar().addWidget(self.versionLabel) # Connect up the buttons. self._mw.actionQuit.triggered.connect(self._manager.quit) self._mw.actionLoad_configuration.triggered.connect(self.getLoadFile) self._mw.actionReload_current_configuration.triggered.connect(self.reloadConfig) self._mw.actionSave_configuration.triggered.connect(self.getSaveFile) self._mw.action_Load_all_modules.triggered.connect(self._manager.startAllConfiguredModules) self._mw.actionAbout_Qt.triggered.connect(QtWidgets.QApplication.aboutQt) self._mw.actionAbout_Qudi.triggered.connect(self.showAboutQudi) self._mw.actionReset_to_default_layout.triggered.connect(self.resetToDefaultLayout) self._manager.sigShowManager.connect(self.show) self._manager.sigConfigChanged.connect(self.updateConfigWidgets) self._manager.sigModulesChanged.connect(self.updateConfigWidgets) self._manager.sigShutdownAcknowledge.connect(self.promptForShutdown) # Log widget self._mw.logwidget.setManager(self._manager) for loghandler in logging.getLogger().handlers: if isinstance(loghandler, core.logger.QtLogHandler): loghandler.sigLoggedMessage.connect(self.handleLogEntry) # Module widgets self.sigStartModule.connect(self._manager.startModule) self.sigReloadModule.connect(self._manager.restartModuleRecursive) self.sigCleanupStatus.connect(self._manager.removeStatusFile) self.sigStopModule.connect(self._manager.deactivateModule) self.sigLoadConfig.connect(self._manager.loadConfig) self.sigSaveConfig.connect(self._manager.saveConfig) self.sigRealQuit.connect(self._manager.realQuit) # Module state display self.checkTimer = QtCore.QTimer() self.checkTimer.start(1000) self.updateGUIModuleList() # IPython console widget self.startIPython() self.updateIPythonModuleList() self.startIPythonWidget() # thread widget self._mw.threadWidget.threadListView.setModel(self._manager.tm) # remote widget # hide remote menu item if rpyc is not available self._mw.actionRemoteView.setVisible(self._manager.rm is not None) if (self._manager.rm is not None): self._mw.remoteWidget.remoteModuleListView.setModel(self._manager.rm.remoteModules) if (self._manager.remote_server): self._mw.remoteWidget.hostLabel.setText('Server URL:') self._mw.remoteWidget.portLabel.setText( 'rpyc://{0}:{1}/'.format(self._manager.rm.server.host, self._manager.rm.server.port)) self._mw.remoteWidget.sharedModuleListView.setModel( self._manager.rm.sharedModules) else: self._mw.remoteWidget.hostLabel.setVisible(False) self._mw.remoteWidget.portLabel.setVisible(False) self._mw.remoteWidget.sharedModuleListView.setVisible(False) self._mw.configDisplayDockWidget.hide() self._mw.remoteDockWidget.hide() self._mw.threadDockWidget.hide() self._mw.show() def on_deactivate(self): """Close window and remove connections. """ self.stopIPythonWidget() self.stopIPython() self.checkTimer.stop() if len(self.modlist) > 0: self.checkTimer.timeout.disconnect() self.sigStartModule.disconnect() self.sigReloadModule.disconnect() self.sigStopModule.disconnect() self.sigLoadConfig.disconnect() self.sigSaveConfig.disconnect() self._mw.actionQuit.triggered.disconnect() self._mw.actionLoad_configuration.triggered.disconnect() self._mw.actionSave_configuration.triggered.disconnect() self._mw.action_Load_all_modules.triggered.disconnect() self._mw.actionAbout_Qt.triggered.disconnect() self._mw.actionAbout_Qudi.triggered.disconnect() self.saveWindowPos(self._mw) self._mw.close() def show(self): """Show the window and bring it t the top. """ QtWidgets.QMainWindow.show(self._mw) self._mw.activateWindow() self._mw.raise_() def showAboutQudi(self): """Show a dialog with details about Qudi. """ self._about.show() @QtCore.Slot(bool, bool) def promptForShutdown(self, locked, broken): """ Display a dialog, asking the user to confirm shutdown. """ text = "Some modules are locked right now, really quit?" result = QtWidgets.QMessageBox.question( self._mw, 'Qudi: Really Quit?', text, QtWidgets.QMessageBox.Yes, QtWidgets.QMessageBox.No ) if result == QtWidgets.QMessageBox.Yes: self.sigRealQuit.emit() def resetToDefaultLayout(self): """ Return the dockwidget layout and visibility to its default state """ self._mw.configDisplayDockWidget.setVisible(False) self._mw.consoleDockWidget.setVisible(True) self._mw.remoteDockWidget.setVisible(False) self._mw.threadDockWidget.setVisible(False) self._mw.logDockWidget.setVisible(True) self._mw.actionConfigurationView.setChecked(False) self._mw.actionConsoleView.setChecked(True) self._mw.actionRemoteView.setChecked(False) self._mw.actionThreadsView.setChecked(False) self._mw.actionLogView.setChecked(True) self._mw.configDisplayDockWidget.setFloating(False) self._mw.consoleDockWidget.setFloating(False) self._mw.remoteDockWidget.setFloating(False) self._mw.threadDockWidget.setFloating(False) self._mw.logDockWidget.setFloating(False) self._mw.addDockWidget(QtCore.Qt.DockWidgetArea(8), self._mw.configDisplayDockWidget) self._mw.addDockWidget(QtCore.Qt.DockWidgetArea(2), self._mw.consoleDockWidget) self._mw.addDockWidget(QtCore.Qt.DockWidgetArea(8), self._mw.remoteDockWidget) self._mw.addDockWidget(QtCore.Qt.DockWidgetArea(8), self._mw.threadDockWidget) self._mw.addDockWidget(QtCore.Qt.DockWidgetArea(8), self._mw.logDockWidget) def handleLogEntry(self, entry): """ Forward log entry to log widget and show an error popup if it is an error message. @param dict entry: Log entry """ self._mw.logwidget.addEntry(entry) if entry['level'] == 'error' or entry['level'] == 'critical': self.errorDialog.show(entry) def startIPython(self): """ Create an IPython kernel manager and kernel. Add modules to its namespace. """ # make sure we only log errors and above from ipython logging.getLogger('ipykernel').setLevel(logging.WARNING) self.log.debug('IPy activation in thread {0}'.format( QtCore.QThread.currentThreadId())) self.kernel_manager = QtInProcessKernelManager() self.kernel_manager.start_kernel() self.kernel = self.kernel_manager.kernel self.namespace = self.kernel.shell.user_ns self.namespace.update({ 'np': np, 'config': self._manager.tree['defined'], 'manager': self._manager }) if _has_pyqtgraph: self.namespace['pg'] = pg self.updateIPythonModuleList() self.kernel.gui = 'qt4' self.log.info('IPython has kernel {0}'.format( self.kernel_manager.has_kernel)) self.log.info('IPython kernel alive {0}'.format( self.kernel_manager.is_alive())) self._manager.sigModulesChanged.connect(self.updateIPythonModuleList) def startIPythonWidget(self): """ Create an IPython console widget and connect it to an IPython kernel. """ if (_has_pyqtgraph): banner_modules = 'The numpy and pyqtgraph modules have already ' \ 'been imported as ''np'' and ''pg''.' else: banner_modules = 'The numpy module has already been imported ' \ 'as ''np''.' banner = """ This is an interactive IPython console. {0} Configuration is in 'config', the manager is 'manager' and all loaded modules are in this namespace with their configured name. View the current namespace with dir(). Go, play. """.format(banner_modules) self._mw.consolewidget.banner = banner # font size self.consoleSetFontSize(self.consoleFontSize) # settings self._csd = ConsoleSettingsDialog() self._csd.accepted.connect(self.consoleApplySettings) self._csd.rejected.connect(self.consoleKeepSettings) self._csd.buttonBox.button( QtWidgets.QDialogButtonBox.Apply).clicked.connect( self.consoleApplySettings) self._mw.actionConsoleSettings.triggered.connect(self._csd.exec_) self.consoleKeepSettings() self._mw.consolewidget.kernel_manager = self.kernel_manager self._mw.consolewidget.kernel_client = \ self._mw.consolewidget.kernel_manager.client() self._mw.consolewidget.kernel_client.start_channels() # the linux style theme which is basically the monokai theme self._mw.consolewidget.set_default_style(colors='linux') def stopIPython(self): """ Stop the IPython kernel. """ self.log.debug('IPy deactivation: {0}'.format(QtCore.QThread.currentThreadId())) self.kernel_manager.shutdown_kernel() def stopIPythonWidget(self): """ Disconnect the IPython widget from the kernel. """ self._mw.consolewidget.kernel_client.stop_channels() def updateIPythonModuleList(self): """Remove non-existing modules from namespace, add new modules to namespace, update reloaded modules """ currentModules = set() newNamespace = dict() for base in ['hardware', 'logic', 'gui']: for module in self._manager.tree['loaded'][base]: currentModules.add(module) newNamespace[module] = self._manager.tree[ 'loaded'][base][module] discard = self.modules - currentModules self.namespace.update(newNamespace) for module in discard: self.namespace.pop(module, None) self.modules = currentModules def consoleKeepSettings(self): """ Write old values into config dialog. """ self._csd.fontSizeBox.setProperty('value', self.consoleFontSize) def consoleApplySettings(self): """ Apply values from config dialog to console. """ self.consoleSetFontSize(self._csd.fontSizeBox.value()) def consoleSetFontSize(self, fontsize): self._mw.consolewidget.font_size = fontsize self.consoleFontSize = fontsize self._mw.consolewidget.reset_font() def updateConfigWidgets(self): """ Clear and refill the tree widget showing the configuration. """ self.fillTreeWidget(self._mw.treeWidget, self._manager.tree) def updateGUIModuleList(self): """ Clear and refill the module list widget """ # self.clearModuleList(self) self.fillModuleList(self._mw.guilayout, 'gui') self.fillModuleList(self._mw.logiclayout, 'logic') self.fillModuleList(self._mw.hwlayout, 'hardware') def fillModuleList(self, layout, base): """ Fill the module list widget with module widgets for defined gui modules. @param QLayout layout: layout of th module list widget where module widgest should be addad @param str base: module category to fill """ for module in self._manager.tree['defined'][base]: if not module in self._manager.tree['global']['startup']: widget = ModuleListItem(self._manager, base, module) self.modlist.append(widget) layout.addWidget(widget) widget.sigLoadThis.connect(self.sigStartModule) widget.sigReloadThis.connect(self.sigReloadModule) widget.sigDeactivateThis.connect(self.sigStopModule) widget.sigCleanupStatus.connect(self.sigCleanupStatus) self.checkTimer.timeout.connect(widget.checkModuleState) def fillTreeItem(self, item, value): """ Recursively fill a QTreeWidgeItem with the contents from a dictionary. @param QTreeWidgetItem item: the widget item to fill @param (dict, list, etc) value: value to fill in """ item.setExpanded(True) if type(value) is OrderedDict or type(value) is dict: for key in value: child = QtWidgets.QTreeWidgetItem() child.setText(0, key) item.addChild(child) self.fillTreeItem(child, value[key]) elif type(value) is list: for val in value: child = QtWidgets.QTreeWidgetItem() item.addChild(child) if type(val) is dict: child.setText(0, '[dict]') self.fillTreeItem(child, val) elif type(val) is OrderedDict: child.setText(0, '[odict]') self.fillTreeItem(child, val) elif type(val) is list: child.setText(0, '[list]') self.fillTreeItem(child, val) else: child.setText(0, str(val)) child.setExpanded(True) else: child = QtWidgets.QTreeWidgetItem() child.setText(0, str(value)) item.addChild(child) def getSoftwareVersion(self): """ Try to determine the software version in case the program is in a git repository. """ try: repo = Repo(self.get_main_dir()) branch = repo.active_branch rev = str(repo.head.commit) return (rev, str(branch)) except Exception as e: print('Could not get git repo because:', e) return ('unknown', -1) def fillTreeWidget(self, widget, value): """ Fill a QTreeWidget with the content of a dictionary @param QTreeWidget widget: the tree widget to fill @param dict,OrderedDict value: the dictionary to fill in """ widget.clear() self.fillTreeItem(widget.invisibleRootItem(), value) def reloadConfig(self): """ Reload the current config. """ reply = QtWidgets.QMessageBox.question( self._mw, 'Restart', 'Do you want to restart the current configuration?', QtWidgets.QMessageBox.Yes, QtWidgets.QMessageBox.No ) configFile = self._manager._getConfigFile() restart = (reply == QtWidgets.QMessageBox.Yes) self.sigLoadConfig.emit(configFile, restart) def getLoadFile(self): """ Ask the user for a file where the configuration should be loaded from """ defaultconfigpath = os.path.join(self.get_main_dir(), 'config') filename = QtWidgets.QFileDialog.getOpenFileName( self._mw, 'Load Configration', defaultconfigpath, 'Configuration files (*.cfg)')[0] if filename != '': reply = QtWidgets.QMessageBox.question( self._mw, 'Restart', 'Do you want to restart to use the configuration?', QtWidgets.QMessageBox.Yes, QtWidgets.QMessageBox.No ) restart = (reply == QtWidgets.QMessageBox.Yes) self.sigLoadConfig.emit(filename, restart) def getSaveFile(self): """ Ask the user for a file where the configuration should be saved to. """ defaultconfigpath = os.path.join(self.get_main_dir(), 'config') filename = QtWidgets.QFileDialog.getSaveFileName( self._mw, 'Save Configration', defaultconfigpath, 'Configuration files (*.cfg)')[0] if filename != '': self.sigSaveConfig.emit(filename)
class PulseStreamer(Base, PulserInterface): """ Methods to control the Swabian Instruments Pulse Streamer 8/2 Example config for copy-paste: pulsestreamer: module.Class: 'swabian_instruments.pulse_streamer.PulseStreamer' pulsestreamer_ip: '192.168.1.100' #pulsed_file_dir: 'C:\\Software\\pulsed_files' laser_channel: 0 uw_x_channel: 1 use_external_clock: False external_clock_option: 0 """ #_pulsestreamer_ip = ConfigOption('pulsestreamer_ip', '192.168.1.100', missing='warn') _pulsestreamer_ip = ConfigOption('pulsestreamer_ip', '169.254.8.2', missing='warn') _laser_channel = ConfigOption('laser_channel', 1, missing='warn') _uw_x_channel = ConfigOption('uw_x_channel', 3, missing='warn') _use_external_clock = ConfigOption('use_external_clock', False, missing='info') _external_clock_option = ConfigOption('external_clock_option', 0, missing='info') # 0: Internal (default), 1: External 125 MHz, 2: External 10 MHz __current_waveform = StatusVar(name='current_waveform', default={}) __current_waveform_name = StatusVar(name='current_waveform_name', default='') __sample_rate = StatusVar(name='sample_rate', default=1e9) def __init__(self, config, **kwargs): super().__init__(config=config, **kwargs) self.__current_status = -1 self.__currently_loaded_waveform = '' # loaded and armed waveform name self.__samples_written = 0 self._trigger = ps.TriggerStart.SOFTWARE self._laser_mw_on_state = ps.OutputState([self._laser_channel, self._uw_x_channel], 0, 0) def on_activate(self): """ Establish connection to pulse streamer and tell it to cancel all operations """ self.pulse_streamer = ps.PulseStreamer(self._pulsestreamer_ip) if self._use_external_clock: if int(self._external_clock_option) is 2: self.pulse_streamer.selectClock(ps.ClockSource.EXT_10MHZ) elif int(self._external_clock_option) is 1: self.pulse_streamer.selectClock(ps.ClockSource.EXT_125MHZ) elif int(self._external_clock_option) is 0: self.pulse_streamer.selectClock(ps.ClockSource.INTERNAL) else: self.log.error('pulsestreamer external clock selection not allowed') self.__samples_written = 0 self.__currently_loaded_waveform = '' self.current_status = 0 def on_deactivate(self): self.reset() del self.pulse_streamer def get_constraints(self): """ Retrieve the hardware constrains from the Pulsing device. @return constraints object: object with pulser constraints as attributes. Provides all the constraints (e.g. sample_rate, amplitude, total_length_bins, channel_config, ...) related to the pulse generator hardware to the caller. SEE PulserConstraints CLASS IN pulser_interface.py FOR AVAILABLE CONSTRAINTS!!! If you are not sure about the meaning, look in other hardware files to get an impression. If still additional constraints are needed, then they have to be added to the PulserConstraints class. Each scalar parameter is an ScalarConstraints object defined in core.util.interfaces. Essentially it contains min/max values as well as min step size, default value and unit of the parameter. PulserConstraints.activation_config differs, since it contain the channel configuration/activation information of the form: {<descriptor_str>: <channel_set>, <descriptor_str>: <channel_set>, ...} If the constraints cannot be set in the pulsing hardware (e.g. because it might have no sequence mode) just leave it out so that the default is used (only zeros). # Example for configuration with default values: constraints = PulserConstraints() constraints.sample_rate.min = 10.0e6 constraints.sample_rate.max = 12.0e9 constraints.sample_rate.step = 10.0e6 constraints.sample_rate.default = 12.0e9 constraints.a_ch_amplitude.min = 0.02 constraints.a_ch_amplitude.max = 2.0 constraints.a_ch_amplitude.step = 0.001 constraints.a_ch_amplitude.default = 2.0 constraints.a_ch_offset.min = -1.0 constraints.a_ch_offset.max = 1.0 constraints.a_ch_offset.step = 0.001 constraints.a_ch_offset.default = 0.0 constraints.d_ch_low.min = -1.0 constraints.d_ch_low.max = 4.0 constraints.d_ch_low.step = 0.01 constraints.d_ch_low.default = 0.0 constraints.d_ch_high.min = 0.0 constraints.d_ch_high.max = 5.0 constraints.d_ch_high.step = 0.01 constraints.d_ch_high.default = 5.0 constraints.waveform_length.min = 80 constraints.waveform_length.max = 64800000 constraints.waveform_length.step = 1 constraints.waveform_length.default = 80 constraints.waveform_num.min = 1 constraints.waveform_num.max = 32000 constraints.waveform_num.step = 1 constraints.waveform_num.default = 1 constraints.sequence_num.min = 1 constraints.sequence_num.max = 8000 constraints.sequence_num.step = 1 constraints.sequence_num.default = 1 constraints.subsequence_num.min = 1 constraints.subsequence_num.max = 4000 constraints.subsequence_num.step = 1 constraints.subsequence_num.default = 1 # If sequencer mode is available then these should be specified constraints.repetitions.min = 0 constraints.repetitions.max = 65539 constraints.repetitions.step = 1 constraints.repetitions.default = 0 constraints.event_triggers = ['A', 'B'] constraints.flags = ['A', 'B', 'C', 'D'] constraints.sequence_steps.min = 0 constraints.sequence_steps.max = 8000 constraints.sequence_steps.step = 1 constraints.sequence_steps.default = 0 # the name a_ch<num> and d_ch<num> are generic names, which describe UNAMBIGUOUSLY the # channels. Here all possible channel configurations are stated, where only the generic # names should be used. The names for the different configurations can be customary chosen. activation_conf = OrderedDict() activation_conf['yourconf'] = {'a_ch1', 'd_ch1', 'd_ch2', 'a_ch2', 'd_ch3', 'd_ch4'} activation_conf['different_conf'] = {'a_ch1', 'd_ch1', 'd_ch2'} activation_conf['something_else'] = {'a_ch2', 'd_ch3', 'd_ch4'} constraints.activation_config = activation_conf """ constraints = PulserConstraints() # The file formats are hardware specific. constraints.sample_rate.min = 1e9 constraints.sample_rate.max = 1e9 constraints.sample_rate.step = 0 constraints.sample_rate.default = 1e9 constraints.d_ch_low.min = 0.0 constraints.d_ch_low.max = 0.0 constraints.d_ch_low.step = 0.0 constraints.d_ch_low.default = 0.0 constraints.d_ch_high.min = 3.3 constraints.d_ch_high.max = 3.3 constraints.d_ch_high.step = 0.0 constraints.d_ch_high.default = 3.3 # sample file length max is not well-defined for PulseStreamer, which collates sequential identical pulses into # one. Total number of not-sequentially-identical pulses which can be stored: 1 M. constraints.waveform_length.min = 1 constraints.waveform_length.max = 134217728 constraints.waveform_length.step = 1 constraints.waveform_length.default = 1 # the name a_ch<num> and d_ch<num> are generic names, which describe UNAMBIGUOUSLY the # channels. Here all possible channel configurations are stated, where only the generic # names should be used. The names for the different configurations can be customary chosen. activation_config = OrderedDict() activation_config['all'] = frozenset({'d_ch1', 'd_ch2', 'd_ch3', 'd_ch4', 'd_ch5', 'd_ch6', 'd_ch7', 'd_ch8'}) constraints.activation_config = activation_config return constraints def pulser_on(self): """ Switches the pulsing device on. @return int: error code (0:OK, -1:error) """ if self._seq: self.pulse_streamer.stream(self._seq) self.pulse_streamer.startNow() self.__current_status = 1 return 0 else: self.log.error('no sequence/pulse pattern prepared for the pulse streamer') self.pulser_off() self.__current_status = -1 return -1 def pulser_off(self): """ Switches the pulsing device off. @return int: error code (0:OK, -1:error) """ self.__current_status = 0 self.pulse_streamer.constant(self._laser_mw_on_state) return 0 def load_waveform(self, load_dict): """ Loads a waveform to the specified channel of the pulsing device. @param dict|list load_dict: a dictionary with keys being one of the available channel index and values being the name of the already written waveform to load into the channel. Examples: {1: rabi_ch1, 2: rabi_ch2} or {1: rabi_ch2, 2: rabi_ch1} If just a list of waveform names if given, the channel association will be invoked from the channel suffix '_ch1', '_ch2' etc. {1: rabi_ch1, 2: rabi_ch2} or {1: rabi_ch2, 2: rabi_ch1} If just a list of waveform names if given, the channel association will be invoked from the channel suffix '_ch1', '_ch2' etc. A possible configuration can be e.g. ['rabi_ch1', 'rabi_ch2', 'rabi_ch3'] @return dict: Dictionary containing the actually loaded waveforms per channel. For devices that have a workspace (i.e. AWG) this will load the waveform from the device workspace into the channel. For a device without mass memory, this will make the waveform/pattern that has been previously written with self.write_waveform ready to play. Please note that the channel index used here is not to be confused with the number suffix in the generic channel descriptors (i.e. 'd_ch1', 'a_ch1'). The channel index used here is highly hardware specific and corresponds to a collection of digital and analog channels being associated to a SINGLE wavfeorm asset. """ if isinstance(load_dict, list): waveforms = list(set(load_dict)) elif isinstance(load_dict, dict): waveforms = list(set(load_dict.values())) else: self.log.error('Method load_waveform expects a list of waveform names or a dict.') return self.get_loaded_assets()[0] if len(waveforms) != 1: self.log.error('pulsestreamer pulser expects exactly one waveform name for load_waveform.') return self.get_loaded_assets()[0] waveform = waveforms[0] if waveform != self.__current_waveform_name: self.log.error('No waveform by the name "{0}" generated for pulsestreamer pulser.\n' 'Only one waveform at a time can be held.'.format(waveform)) return self.get_loaded_assets()[0] self._seq = self.pulse_streamer.createSequence() for channel_number, pulse_pattern in self.__current_waveform.items(): #print(pulse_pattern) swabian_channel_number = int(channel_number[-1])-1 self._seq.setDigital(swabian_channel_number,pulse_pattern) self.__currently_loaded_waveform = self.__current_waveform_name return self.get_loaded_assets()[0] def get_loaded_assets(self): """ Retrieve the currently loaded asset names for each active channel of the device. The returned dictionary will have the channel numbers as keys. In case of loaded waveforms the dictionary values will be the waveform names. In case of a loaded sequence the values will be the sequence name appended by a suffix representing the track loaded to the respective channel (i.e. '<sequence_name>_1'). @return (dict, str): Dictionary with keys being the channel number and values being the respective asset loaded into the channel, string describing the asset type ('waveform' or 'sequence') """ asset_type = 'waveform' if self.__currently_loaded_waveform else None asset_dict = {chnl_num: self.__currently_loaded_waveform for chnl_num in range(1, 9)} return asset_dict, asset_type def load_sequence(self, sequence_name): """ Loads a sequence to the channels of the device in order to be ready for playback. For devices that have a workspace (i.e. AWG) this will load the sequence from the device workspace into the channels. For a device without mass memory this will make the waveform/pattern that has been previously written with self.write_waveform ready to play. @param dict|list sequence_name: a dictionary with keys being one of the available channel index and values being the name of the already written waveform to load into the channel. Examples: {1: rabi_ch1, 2: rabi_ch2} or {1: rabi_ch2, 2: rabi_ch1} If just a list of waveform names if given, the channel association will be invoked from the channel suffix '_ch1', '_ch2' etc. @return dict: Dictionary containing the actually loaded waveforms per channel. """ self.log.debug('sequencing not implemented for pulsestreamer') return dict() def clear_all(self): """ Clears all loaded waveforms from the pulse generators RAM/workspace. @return int: error code (0:OK, -1:error) """ self.pulser_off() self.__currently_loaded_waveform = '' self.__current_waveform_name = '' self._seq = dict() self.__current_waveform = dict() def get_status(self): """ Retrieves the status of the pulsing hardware @return (int, dict): tuple with an integer value of the current status and a corresponding dictionary containing status description for all the possible status variables of the pulse generator hardware. """ status_dic = dict() status_dic[-1] = 'Failed Request or Failed Communication with device.' status_dic[0] = 'Device has stopped, but can receive commands.' status_dic[1] = 'Device is active and running.' return self.__current_status, status_dic def get_sample_rate(self): """ Get the sample rate of the pulse generator hardware @return float: The current sample rate of the device (in Hz) Do not return a saved sample rate in a class variable, but instead retrieve the current sample rate directly from the device. """ return self.__sample_rate def set_sample_rate(self, sample_rate): """ Set the sample rate of the pulse generator hardware. @param float sample_rate: The sampling rate to be set (in Hz) @return float: the sample rate returned from the device. Note: After setting the sampling rate of the device, retrieve it again for obtaining the actual set value and use that information for further processing. """ self.log.debug('PulseStreamer sample rate cannot be configured') return self.__sample_rate def get_analog_level(self, amplitude=None, offset=None): """ Retrieve the analog amplitude and offset of the provided channels. @param list amplitude: optional, if the amplitude value (in Volt peak to peak, i.e. the full amplitude) of a specific channel is desired. @param list offset: optional, if the offset value (in Volt) of a specific channel is desired. @return: (dict, dict): tuple of two dicts, with keys being the channel descriptor string (i.e. 'a_ch1') and items being the values for those channels. Amplitude is always denoted in Volt-peak-to-peak and Offset in volts. Note: Do not return a saved amplitude and/or offset value but instead retrieve the current amplitude and/or offset directly from the device. If nothing (or None) is passed then the levels of all channels will be returned. If no analog channels are present in the device, return just empty dicts. Example of a possible input: amplitude = ['a_ch1', 'a_ch4'], offset = None to obtain the amplitude of channel 1 and 4 and the offset of all channels {'a_ch1': -0.5, 'a_ch4': 2.0} {'a_ch1': 0.0, 'a_ch2': 0.0, 'a_ch3': 1.0, 'a_ch4': 0.0} """ return {},{} def set_analog_level(self, amplitude=None, offset=None): """ Set amplitude and/or offset value of the provided analog channel(s). @param dict amplitude: dictionary, with key being the channel descriptor string (i.e. 'a_ch1', 'a_ch2') and items being the amplitude values (in Volt peak to peak, i.e. the full amplitude) for the desired channel. @param dict offset: dictionary, with key being the channel descriptor string (i.e. 'a_ch1', 'a_ch2') and items being the offset values (in absolute volt) for the desired channel. @return (dict, dict): tuple of two dicts with the actual set values for amplitude and offset for ALL channels. If nothing is passed then the command will return the current amplitudes/offsets. Note: After setting the amplitude and/or offset values of the device, use the actual set return values for further processing. """ return {},{} def get_digital_level(self, low=None, high=None): """ Retrieve the digital low and high level of the provided channels. @param list low: optional, if a specific low value (in Volt) of a channel is desired. @param list high: optional, if a specific high value (in Volt) of a channel is desired. @return: (dict, dict): tuple of two dicts, with keys being the channel number and items being the values for those channels. Both low and high value of a channel is denoted in (absolute) Voltage. Note: Do not return a saved low and/or high value but instead retrieve the current low and/or high value directly from the device. If no entries provided then the levels of all channels where simply returned. If no digital channels provided, return just an empty dict. Example of a possible input: low = [1,4] to obtain the low voltage values of digital channel 1 an 4. A possible answer might be {1: -0.5, 4: 2.0} {} since no high request was performed. The major difference to analog signals is that digital signals are either ON or OFF, whereas analog channels have a varying amplitude range. In contrast to analog output levels, digital output levels are defined by a voltage, which corresponds to the ON status and a voltage which corresponds to the OFF status (both denoted in (absolute) voltage) In general there is no bijective correspondence between (amplitude, offset) and (value high, value low)! """ if low is None: low = [] if high is None: high = [] low_dict = {} high_dict = {} if low is [] and high is []: for channel in range(8): low_dict[channel] = 0.0 high_dict[channel] = 3.3 else: for channel in low: low_dict[channel] = 0.0 for channel in high: high_dict[channel] = 3.3 return low_dict, high_dict def set_digital_level(self, low=None, high=None): """ Set low and/or high value of the provided digital channel. @param dict low: dictionary, with key being the channel and items being the low values (in volt) for the desired channel. @param dict high: dictionary, with key being the channel and items being the high values (in volt) for the desired channel. @return (dict, dict): tuple of two dicts where first dict denotes the current low value and the second dict the high value. If nothing is passed then the command will return two empty dicts. Note: After setting the high and/or low values of the device, retrieve them again for obtaining the actual set value(s) and use that information for further processing. The major difference to analog signals is that digital signals are either ON or OFF, whereas analog channels have a varying amplitude range. In contrast to analog output levels, digital output levels are defined by a voltage, which corresponds to the ON status and a voltage which corresponds to the OFF status (both denoted in (absolute) voltage) In general there is no bijective correspondence between (amplitude, offset) and (value high, value low)! """ if low is None: low = {} if high is None: high = {} self.log.warning('PulseStreamer logic level cannot be adjusted!') return self.get_digital_level() def get_active_channels(self, ch=None): """ Get the active channels of the pulse generator hardware. @param list ch: optional, if specific analog or digital channels are needed to be asked without obtaining all the channels. @return dict: where keys denoting the channel string and items boolean expressions whether channel are active or not. Example for an possible input (order is not important): ch = ['a_ch2', 'd_ch2', 'a_ch1', 'd_ch5', 'd_ch1'] then the output might look like {'a_ch2': True, 'd_ch2': False, 'a_ch1': False, 'd_ch5': True, 'd_ch1': False} If no parameter (or None) is passed to this method all channel states will be returned. """ if ch is None: ch = {} d_ch_dict = {} if len(ch) < 1: for chnl in range(1, 9): d_ch_dict['d_ch{0}'.format(chnl)] = True else: for channel in ch: d_ch_dict[channel] = True return d_ch_dict def set_active_channels(self, ch=None): """ Set the active/inactive channels for the pulse generator hardware. The state of ALL available analog and digital channels will be returned (True: active, False: inactive). The actually set and returned channel activation must be part of the available activation_configs in the constraints. You can also activate/deactivate subsets of available channels but the resulting activation_config must still be valid according to the constraints. If the resulting set of active channels can not be found in the available activation_configs, the channel states must remain unchanged. @param dict ch: dictionary with keys being the analog or digital string generic names for the channels (i.e. 'd_ch1', 'a_ch2') with items being a boolean value. True: Activate channel, False: Deactivate channel @return dict: with the actual set values for ALL active analog and digital channels If nothing is passed then the command will simply return the unchanged current state. Note: After setting the active channels of the device, use the returned dict for further processing. Example for possible input: ch={'a_ch2': True, 'd_ch1': False, 'd_ch3': True, 'd_ch4': True} to activate analog channel 2 digital channel 3 and 4 and to deactivate digital channel 1. All other available channels will remain unchanged. """ if ch is None: ch = {} d_ch_dict = { 'd_ch1': True, 'd_ch2': True, 'd_ch3': True, 'd_ch4': True, 'd_ch5': True, 'd_ch6': True, 'd_ch7': True, 'd_ch8': True} return d_ch_dict def write_waveform(self, name, analog_samples, digital_samples, is_first_chunk, is_last_chunk, total_number_of_samples): """ Write a new waveform or append samples to an already existing waveform on the device memory. The flags is_first_chunk and is_last_chunk can be used as indicator if a new waveform should be created or if the write process to a waveform should be terminated. NOTE: All sample arrays in analog_samples and digital_samples must be of equal length! @param str name: the name of the waveform to be created/append to @param dict analog_samples: keys are the generic analog channel names (i.e. 'a_ch1') and values are 1D numpy arrays of type float32 containing the voltage samples. @param dict digital_samples: keys are the generic digital channel names (i.e. 'd_ch1') and values are 1D numpy arrays of type bool containing the marker states. @param bool is_first_chunk: Flag indicating if it is the first chunk to write. If True this method will create a new empty wavveform. If False the samples are appended to the existing waveform. @param bool is_last_chunk: Flag indicating if it is the last chunk to write. Some devices may need to know when to close the appending wfm. @param int total_number_of_samples: The number of sample points for the entire waveform (not only the currently written chunk) @return (int, list): Number of samples written (-1 indicates failed process) and list of created waveform names """ if analog_samples: self.log.debug('Analog not yet implemented for pulse streamer') return -1, list() if is_first_chunk: self.__current_waveform_name = name self.__samples_written = 0 # initalise to a dict of lists that describe pulse pattern in swabian language self.__current_waveform = {key:[] for key in digital_samples.keys()} for channel_number, samples in digital_samples.items(): new_channel_indices = np.where(samples[:-1] != samples[1:])[0] new_channel_indices = np.unique(new_channel_indices) # add in indices for the start and end of the sequence to simplify iteration new_channel_indices = np.insert(new_channel_indices, 0, [-1]) new_channel_indices = np.insert(new_channel_indices, new_channel_indices.size, [samples.shape[0] - 1]) pulses = [] for new_channel_index in range(1, new_channel_indices.size): pulse = [new_channel_indices[new_channel_index] - new_channel_indices[new_channel_index - 1], samples[new_channel_indices[new_channel_index - 1] + 1].astype(np.byte)] pulses.append(pulse) # extend (as opposed to rewrite) for chunky business #print(pulses) self.__current_waveform[channel_number].extend(pulses) return len(samples), [self.__current_waveform_name] def write_sequence(self, name, sequence_parameters): """ Write a new sequence on the device memory. @param str name: the name of the waveform to be created/append to @param list sequence_parameters: List containing tuples of length 2. Each tuple represents a sequence step. The first entry of the tuple is a list of waveform names (str); one for each channel. The second tuple element is a SequenceStep instance containing the sequencing parameters for this step. @return: int, number of sequence steps written (-1 indicates failed process) """ self.log.debug('Sequencing not yet implemented for pulse streamer') return -1 def get_waveform_names(self): """ Retrieve the names of all uploaded waveforms on the device. @return list: List of all uploaded waveform name strings in the device workspace. """ waveform_names = list() if self.__current_waveform_name != '' and self.__current_waveform_name is not None: waveform_names = [self.__current_waveform_name] return waveform_names def get_sequence_names(self): """ Retrieve the names of all uploaded sequence on the device. @return list: List of all uploaded sequence name strings in the device workspace. """ return list() def delete_waveform(self, waveform_name): """ Delete the waveform with name "waveform_name" from the device memory. @param str waveform_name: The name of the waveform to be deleted Optionally a list of waveform names can be passed. @return list: a list of deleted waveform names. """ return list() def delete_sequence(self, sequence_name): """ Delete the sequence with name "sequence_name" from the device memory. @param str sequence_name: The name of the sequence to be deleted Optionally a list of sequence names can be passed. @return list: a list of deleted sequence names. """ return list() def get_interleave(self): """ Check whether Interleave is ON or OFF in AWG. @return bool: True: ON, False: OFF Will always return False for pulse generator hardware without interleave. """ return False def set_interleave(self, state=False): """ Turns the interleave of an AWG on or off. @param bool state: The state the interleave should be set to (True: ON, False: OFF) @return bool: actual interleave status (True: ON, False: OFF) Note: After setting the interleave of the device, retrieve the interleave again and use that information for further processing. Unused for pulse generator hardware other than an AWG. """ if state: self.log.error('No interleave functionality available in FPGA pulser.\n' 'Interleave state is always False.') return False def reset(self): """ Reset the device. @return int: error code (0:OK, -1:error) """ self.pulse_streamer.reset() self.__currently_loaded_waveform = '' def has_sequence_mode(self): """ Asks the pulse generator whether sequence mode exists. @return: bool, True for yes, False for no. """ return False
class ODMRLogic(GenericLogic): """This is the Logic class for ODMR.""" _modclass = 'odmrlogic' _modtype = 'logic' # declare connectors odmrcounter = Connector(interface='ODMRCounterInterface') fitlogic = Connector(interface='FitLogic') microwave1 = Connector(interface='mwsourceinterface') savelogic = Connector(interface='SaveLogic') taskrunner = Connector(interface='TaskRunner') # config option mw_scanmode = ConfigOption('scanmode', 'LIST', missing='warn', converter=lambda x: MicrowaveMode[x.upper()]) clock_frequency = StatusVar('clock_frequency', 200) cw_mw_frequency = StatusVar('cw_mw_frequency', 2870e6) cw_mw_power = StatusVar('cw_mw_power', -30) sweep_mw_power = StatusVar('sweep_mw_power', -30) mw_start = StatusVar('mw_start', 2800e6) mw_stop = StatusVar('mw_stop', 2950e6) mw_step = StatusVar('mw_step', 2e6) run_time = StatusVar('run_time', 60) number_of_lines = StatusVar('number_of_lines', 50) fc = StatusVar('fits', None) lines_to_average = StatusVar('lines_to_average', 0) _oversampling = StatusVar('oversampling', default=10) _lock_in_active = StatusVar('lock_in_active', default=False) # Internal signals sigNextLine = QtCore.Signal() # Update signals, e.g. for GUI module sigParameterUpdated = QtCore.Signal(dict) sigOutputStateUpdated = QtCore.Signal(str, bool) sigOdmrPlotsUpdated = QtCore.Signal(np.ndarray, np.ndarray, np.ndarray) sigOdmrFitUpdated = QtCore.Signal(np.ndarray, np.ndarray, dict, str) sigOdmrElapsedTimeUpdated = QtCore.Signal(float, int) def __init__(self, config, **kwargs): super().__init__(config=config, **kwargs) self.threadlock = Mutex() def on_activate(self): """ Initialisation performed during activation of the module. """ # Get connectors self._mw_device = self.microwave1() self._fit_logic = self.fitlogic() self._odmr_counter = self.odmrcounter() self._save_logic = self.savelogic() self._taskrunner = self.taskrunner() # Get hardware constraints limits = self.get_hw_constraints() # Set/recall microwave source parameters self.cw_mw_frequency = limits.frequency_in_range(self.cw_mw_frequency) self.cw_mw_power = limits.power_in_range(self.cw_mw_power) self.sweep_mw_power = limits.power_in_range(self.sweep_mw_power) self.mw_start = limits.frequency_in_range(self.mw_start) self.mw_stop = limits.frequency_in_range(self.mw_stop) self.mw_step = limits.list_step_in_range(self.mw_step) self._odmr_counter.oversampling = self._oversampling self._odmr_counter.lock_in_active = self._lock_in_active # Set the trigger polarity (RISING/FALLING) of the mw-source input trigger # theoretically this can be changed, but the current counting scheme will not support that self.mw_trigger_pol = TriggerEdge.RISING self.set_trigger(self.mw_trigger_pol, self.clock_frequency) # Elapsed measurement time and number of sweeps self.elapsed_time = 0.0 self.elapsed_sweeps = 0 # Set flags # for stopping a measurement self._stopRequested = False # for clearing the ODMR data during a measurement self._clearOdmrData = False # Initalize the ODMR data arrays (mean signal and sweep matrix) self._initialize_odmr_plots() # Raw data array self.odmr_raw_data = np.zeros([ self.number_of_lines, len(self._odmr_counter.get_odmr_channels()), self.odmr_plot_x.size ]) # Switch off microwave and set CW frequency and power self.mw_off() self.set_cw_parameters(self.cw_mw_frequency, self.cw_mw_power) # Connect signals self.sigNextLine.connect(self._scan_odmr_line, QtCore.Qt.QueuedConnection) return def on_deactivate(self): """ Deinitialisation performed during deactivation of the module. """ # Stop measurement if it is still running if self.module_state() == 'locked': self.stop_odmr_scan() timeout = 30.0 start_time = time.time() while self.module_state() == 'locked': time.sleep(0.5) timeout -= (time.time() - start_time) if timeout <= 0.0: self.log.error( 'Failed to properly deactivate odmr logic. Odmr scan is still ' 'running but can not be stopped after 30 sec.') break # Switch off microwave source for sure (also if CW mode is active or module is still locked) self._mw_device.off() # Disconnect signals self.sigNextLine.disconnect() @fc.constructor def sv_set_fits(self, val): # Setup fit container fc = self.fitlogic().make_fit_container('ODMR sum', '1d') fc.set_units(['Hz', 'c/s']) if isinstance(val, dict) and len(val) > 0: fc.load_from_dict(val) else: d1 = OrderedDict() d1['Lorentzian dip'] = { 'fit_function': 'lorentzian', 'estimator': 'dip' } d1['Two Lorentzian dips'] = { 'fit_function': 'lorentziandouble', 'estimator': 'dip' } d1['N14'] = { 'fit_function': 'lorentziantriple', 'estimator': 'N14' } d1['N15'] = { 'fit_function': 'lorentziandouble', 'estimator': 'N15' } d1['Two Gaussian dips'] = { 'fit_function': 'gaussiandouble', 'estimator': 'dip' } default_fits = OrderedDict() default_fits['1d'] = d1 fc.load_from_dict(default_fits) return fc @fc.representer def sv_get_fits(self, val): """ save configured fits """ if len(val.fit_list) > 0: return val.save_to_dict() else: return None def _initialize_odmr_plots(self): """ Initializing the ODMR plots (line and matrix). """ self.odmr_plot_x = np.arange(self.mw_start, self.mw_stop + self.mw_step, self.mw_step) self.odmr_plot_y = np.zeros( [len(self.get_odmr_channels()), self.odmr_plot_x.size]) self.odmr_fit_x = np.arange(self.mw_start, self.mw_stop + self.mw_step, self.mw_step) self.odmr_fit_y = np.zeros(self.odmr_fit_x.size) self.odmr_plot_xy = np.zeros([ self.number_of_lines, len(self.get_odmr_channels()), self.odmr_plot_x.size ]) self.sigOdmrPlotsUpdated.emit(self.odmr_plot_x, self.odmr_plot_y, self.odmr_plot_xy) current_fit = self.fc.current_fit self.sigOdmrFitUpdated.emit(self.odmr_fit_x, self.odmr_fit_y, {}, current_fit) return def set_trigger(self, trigger_pol, frequency): """ Set trigger polarity of external microwave trigger (for list and sweep mode). @param object trigger_pol: one of [TriggerEdge.RISING, TriggerEdge.FALLING] @param float frequency: trigger frequency during ODMR scan @return object: actually set trigger polarity returned from hardware """ if self._lock_in_active: frequency = frequency / self._oversampling if self.module_state() != 'locked': self.mw_trigger_pol, triggertime = self._mw_device.set_ext_trigger( trigger_pol, 1 / frequency) else: self.log.warning('set_trigger failed. Logic is locked.') update_dict = {'trigger_pol': self.mw_trigger_pol} self.sigParameterUpdated.emit(update_dict) return self.mw_trigger_pol def set_average_length(self, lines_to_average): """ Sets the number of lines to average for the sum of the data @param int lines_to_average: desired number of lines to average (0 means all) @return int: actually set lines to average """ self.lines_to_average = int(lines_to_average) if self.lines_to_average <= 0: self.odmr_plot_y = np.mean( self.odmr_raw_data[:max(1, self.elapsed_sweeps), :, :], axis=0, dtype=np.float64) else: self.odmr_plot_y = np.mean(self.odmr_raw_data[:max( 1, min(self.lines_to_average, self.elapsed_sweeps)), :, :], axis=0, dtype=np.float64) self.sigOdmrPlotsUpdated.emit(self.odmr_plot_x, self.odmr_plot_y, self.odmr_plot_xy) self.sigParameterUpdated.emit( {'average_length': self.lines_to_average}) return self.lines_to_average def set_clock_frequency(self, clock_frequency): """ Sets the frequency of the counter clock @param int clock_frequency: desired frequency of the clock @return int: actually set clock frequency """ # checks if scanner is still running if self.module_state() != 'locked' and isinstance( clock_frequency, (int, float)): self.clock_frequency = int(clock_frequency) else: self.log.warning( 'set_clock_frequency failed. Logic is either locked or input value is ' 'no integer or float.') update_dict = {'clock_frequency': self.clock_frequency} self.sigParameterUpdated.emit(update_dict) return self.clock_frequency @property def oversampling(self): return self._oversampling @oversampling.setter def oversampling(self, oversampling): """ Sets the frequency of the counter clock @param int oversampling: desired oversampling per frequency step """ # checks if scanner is still running if self.module_state() != 'locked' and isinstance( oversampling, (int, float)): self._oversampling = int(oversampling) self._odmr_counter.oversampling = self._oversampling else: self.log.warning( 'setter of oversampling failed. Logic is either locked or input value is ' 'no integer or float.') update_dict = {'oversampling': self._oversampling} self.sigParameterUpdated.emit(update_dict) def set_oversampling(self, oversampling): self.oversampling = oversampling return self.oversampling @property def lock_in(self): return self._lock_in_active @lock_in.setter def lock_in(self, active): """ Sets the frequency of the counter clock @param bool active: specify if signal should be detected with lock in """ # checks if scanner is still running if self.module_state() != 'locked' and isinstance(active, bool): self._lock_in_active = active self._odmr_counter.lock_in_active = self._lock_in_active else: self.log.warning( 'setter of lock in failed. Logic is either locked or input value is no boolean.' ) update_dict = {'lock_in': self._lock_in_active} self.sigParameterUpdated.emit(update_dict) def set_lock_in(self, active): self.lock_in = active return self.lock_in def set_matrix_line_number(self, number_of_lines): """ Sets the number of lines in the ODMR matrix @param int number_of_lines: desired number of matrix lines @return int: actually set number of matrix lines """ if isinstance(number_of_lines, int): self.number_of_lines = number_of_lines else: self.log.warning('set_matrix_line_number failed. ' 'Input parameter number_of_lines is no integer.') update_dict = {'number_of_lines': self.number_of_lines} self.sigParameterUpdated.emit(update_dict) return self.number_of_lines def set_runtime(self, runtime): """ Sets the runtime for ODMR measurement @param float runtime: desired runtime in seconds @return float: actually set runtime in seconds """ if isinstance(runtime, (int, float)): self.run_time = runtime else: self.log.warning( 'set_runtime failed. Input parameter runtime is no integer or float.' ) update_dict = {'run_time': self.run_time} self.sigParameterUpdated.emit(update_dict) return self.run_time def set_cw_parameters(self, frequency, power): """ Set the desired new cw mode parameters. @param float frequency: frequency to set in Hz @param float power: power to set in dBm @return (float, float): actually set frequency in Hz, actually set power in dBm """ if self.module_state() != 'locked' and isinstance( frequency, (int, float)) and isinstance(power, (int, float)): constraints = self.get_hw_constraints() frequency_to_set = constraints.frequency_in_range(frequency) power_to_set = constraints.power_in_range(power) self.cw_mw_frequency, self.cw_mw_power, dummy = self._mw_device.set_cw( frequency_to_set, power_to_set) else: self.log.warning( 'set_cw_frequency failed. Logic is either locked or input value is ' 'no integer or float.') param_dict = { 'cw_mw_frequency': self.cw_mw_frequency, 'cw_mw_power': self.cw_mw_power } self.sigParameterUpdated.emit(param_dict) return self.cw_mw_frequency, self.cw_mw_power def set_sweep_parameters(self, start, stop, step, power): """ Set the desired frequency parameters for list and sweep mode @param float start: start frequency to set in Hz @param float stop: stop frequency to set in Hz @param float step: step frequency to set in Hz @param float power: mw power to set in dBm @return float, float, float, float: current start_freq, current stop_freq, current freq_step, current power """ limits = self.get_hw_constraints() if self.module_state() != 'locked': if isinstance(start, (int, float)): self.mw_start = limits.frequency_in_range(start) if isinstance(stop, (int, float)) and isinstance(step, (int, float)): if stop <= start: stop = start + step self.mw_stop = limits.frequency_in_range(stop) if self.mw_scanmode == MicrowaveMode.LIST: self.mw_step = limits.list_step_in_range(step) elif self.mw_scanmode == MicrowaveMode.SWEEP: self.mw_step = limits.sweep_step_in_range(step) if isinstance(power, (int, float)): self.sweep_mw_power = limits.power_in_range(power) else: self.log.warning('set_sweep_parameters failed. Logic is locked.') param_dict = { 'mw_start': self.mw_start, 'mw_stop': self.mw_stop, 'mw_step': self.mw_step, 'sweep_mw_power': self.sweep_mw_power } self.sigParameterUpdated.emit(param_dict) return self.mw_start, self.mw_stop, self.mw_step, self.sweep_mw_power def mw_cw_on(self): """ Switching on the mw source in cw mode. @return str, bool: active mode ['cw', 'list', 'sweep'], is_running """ if self.module_state() == 'locked': self.log.error( 'Can not start microwave in CW mode. ODMRLogic is already locked.' ) else: self.cw_mw_frequency, \ self.cw_mw_power, \ mode = self._mw_device.set_cw(self.cw_mw_frequency, self.cw_mw_power) param_dict = { 'cw_mw_frequency': self.cw_mw_frequency, 'cw_mw_power': self.cw_mw_power } self.sigParameterUpdated.emit(param_dict) if mode != 'cw': self.log.error('Switching to CW microwave output mode failed.') else: err_code = self._mw_device.cw_on() if err_code < 0: self.log.error('Activation of microwave output failed.') mode, is_running = self._mw_device.get_status() self.sigOutputStateUpdated.emit(mode, is_running) return mode, is_running def mw_sweep_on(self): """ Switching on the mw source in list/sweep mode. @return str, bool: active mode ['cw', 'list', 'sweep'], is_running """ limits = self.get_hw_constraints() param_dict = {} if self.mw_scanmode == MicrowaveMode.LIST: if np.abs(self.mw_stop - self.mw_start) / self.mw_step >= limits.list_maxentries: self.log.warning( 'Number of frequency steps too large for microwave device. ' 'Lowering resolution to fit the maximum length.') self.mw_step = np.abs(self.mw_stop - self.mw_start) / ( limits.list_maxentries - 1) self.sigParameterUpdated.emit({'mw_step': self.mw_step}) # adjust the end frequency in order to have an integer multiple of step size # The master module (i.e. GUI) will be notified about the changed end frequency num_steps = int( np.rint((self.mw_stop - self.mw_start) / self.mw_step)) end_freq = self.mw_start + num_steps * self.mw_step freq_list = np.linspace(self.mw_start, end_freq, num_steps + 1) freq_list, self.sweep_mw_power, mode = self._mw_device.set_list( freq_list, self.sweep_mw_power) self.mw_start = freq_list[0] self.mw_stop = freq_list[-1] self.mw_step = (self.mw_stop - self.mw_start) / (len(freq_list) - 1) param_dict = { 'mw_start': self.mw_start, 'mw_stop': self.mw_stop, 'mw_step': self.mw_step, 'sweep_mw_power': self.sweep_mw_power } elif self.mw_scanmode == MicrowaveMode.SWEEP: if np.abs(self.mw_stop - self.mw_start) / self.mw_step >= limits.sweep_maxentries: self.log.warning( 'Number of frequency steps too large for microwave device. ' 'Lowering resolution to fit the maximum length.') self.mw_step = np.abs(self.mw_stop - self.mw_start) / ( limits.list_maxentries - 1) self.sigParameterUpdated.emit({'mw_step': self.mw_step}) sweep_return = self._mw_device.set_sweep(self.mw_start, self.mw_stop, self.mw_step, self.sweep_mw_power) self.mw_start, self.mw_stop, self.mw_step, self.sweep_mw_power, mode = sweep_return param_dict = { 'mw_start': self.mw_start, 'mw_stop': self.mw_stop, 'mw_step': self.mw_step, 'sweep_mw_power': self.sweep_mw_power } else: self.log.error( 'Scanmode not supported. Please select SWEEP or LIST.') self.sigParameterUpdated.emit(param_dict) if mode != 'list' and mode != 'sweep': self.log.error( 'Switching to list/sweep microwave output mode failed.') elif self.mw_scanmode == MicrowaveMode.SWEEP: err_code = self._mw_device.sweep_on() if err_code < 0: self.log.error('Activation of microwave output failed.') else: err_code = self._mw_device.list_on() if err_code < 0: self.log.error('Activation of microwave output failed.') mode, is_running = self._mw_device.get_status() self.sigOutputStateUpdated.emit(mode, is_running) return mode, is_running def reset_sweep(self): """ Resets the list/sweep mode of the microwave source to the first frequency step. """ if self.mw_scanmode == MicrowaveMode.SWEEP: self._mw_device.reset_sweeppos() elif self.mw_scanmode == MicrowaveMode.LIST: self._mw_device.reset_listpos() return def mw_off(self): """ Switching off the MW source. @return str, bool: active mode ['cw', 'list', 'sweep'], is_running """ error_code = self._mw_device.off() if error_code < 0: self.log.error('Switching off microwave source failed.') mode, is_running = self._mw_device.get_status() self.sigOutputStateUpdated.emit(mode, is_running) return mode, is_running def _start_odmr_counter(self): """ Starting the ODMR counter and set up the clock for it. @return int: error code (0:OK, -1:error) """ clock_status = self._odmr_counter.set_up_odmr_clock( clock_frequency=self.clock_frequency) if clock_status < 0: return -1 counter_status = self._odmr_counter.set_up_odmr() if counter_status < 0: self._odmr_counter.close_odmr_clock() return -1 return 0 def _stop_odmr_counter(self): """ Stopping the ODMR counter. @return int: error code (0:OK, -1:error) """ ret_val1 = self._odmr_counter.close_odmr() if ret_val1 != 0: self.log.error('ODMR counter could not be stopped!') ret_val2 = self._odmr_counter.close_odmr_clock() if ret_val2 != 0: self.log.error('ODMR clock could not be stopped!') # Check with a bitwise or: return ret_val1 | ret_val2 def start_odmr_scan(self): """ Starting an ODMR scan. @return int: error code (0:OK, -1:error) """ with self.threadlock: if self.module_state() == 'locked': self.log.error( 'Can not start ODMR scan. Logic is already locked.') return -1 self.set_trigger(self.mw_trigger_pol, self.clock_frequency) self.module_state.lock() self._clearOdmrData = False self.stopRequested = False self.fc.clear_result() self.elapsed_sweeps = 0 self.elapsed_time = 0.0 self._startTime = time.time() self.sigOdmrElapsedTimeUpdated.emit(self.elapsed_time, self.elapsed_sweeps) odmr_status = self._start_odmr_counter() if odmr_status < 0: mode, is_running = self._mw_device.get_status() self.sigOutputStateUpdated.emit(mode, is_running) self.module_state.unlock() return -1 mode, is_running = self.mw_sweep_on() if not is_running: self._stop_odmr_counter() self.module_state.unlock() return -1 self._initialize_odmr_plots() # initialize raw_data array estimated_number_of_lines = self.run_time * self.clock_frequency / self.odmr_plot_x.size estimated_number_of_lines = int( 1.5 * estimated_number_of_lines) # Safety if estimated_number_of_lines < self.number_of_lines: estimated_number_of_lines = self.number_of_lines self.log.debug('Estimated number of raw data lines: {0:d}' ''.format(estimated_number_of_lines)) self.odmr_raw_data = np.zeros([ estimated_number_of_lines, len(self._odmr_counter.get_odmr_channels()), self.odmr_plot_x.size ]) self.sigNextLine.emit() return 0 def continue_odmr_scan(self): """ Continue ODMR scan. @return int: error code (0:OK, -1:error) """ with self.threadlock: if self.module_state() == 'locked': self.log.error( 'Can not start ODMR scan. Logic is already locked.') return -1 self.set_trigger(self.mw_trigger_pol, self.clock_frequency) self.module_state.lock() self.stopRequested = False self.fc.clear_result() self._startTime = time.time() - self.elapsed_time self.sigOdmrElapsedTimeUpdated.emit(self.elapsed_time, self.elapsed_sweeps) odmr_status = self._start_odmr_counter() if odmr_status < 0: mode, is_running = self._mw_device.get_status() self.sigOutputStateUpdated.emit(mode, is_running) self.module_state.unlock() return -1 mode, is_running = self.mw_sweep_on() if not is_running: self._stop_odmr_counter() self.module_state.unlock() return -1 self.sigNextLine.emit() return 0 def stop_odmr_scan(self): """ Stop the ODMR scan. @return int: error code (0:OK, -1:error) """ with self.threadlock: if self.module_state() == 'locked': self.stopRequested = True return 0 def clear_odmr_data(self): """¨Set the option to clear the curret ODMR data. The clear operation has to be performed within the method _scan_odmr_line. This method just sets the flag for that. """ with self.threadlock: if self.module_state() == 'locked': self._clearOdmrData = True return def _scan_odmr_line(self): """ Scans one line in ODMR (from mw_start to mw_stop in steps of mw_step) """ with self.threadlock: # If the odmr measurement is not running do nothing if self.module_state() != 'locked': return # Stop measurement if stop has been requested if self.stopRequested: self.stopRequested = False self.mw_off() self._stop_odmr_counter() self.module_state.unlock() return # if during the scan a clearing of the ODMR data is needed: if self._clearOdmrData: self.elapsed_sweeps = 0 self._startTime = time.time() # reset position so every line starts from the same frequency self.reset_sweep() # Acquire count data error, new_counts = self._odmr_counter.count_odmr( length=self.odmr_plot_x.size) if error: self.stopRequested = True self.sigNextLine.emit() return # Add new count data to raw_data array and append if array is too small if self._clearOdmrData: self.odmr_raw_data[:, :, :] = 0 self._clearOdmrData = False if self.elapsed_sweeps == (self.odmr_raw_data.shape[0] - 1): expanded_array = np.zeros(self.odmr_raw_data.shape) self.odmr_raw_data = np.concatenate( (self.odmr_raw_data, expanded_array), axis=0) self.log.warning( 'raw data array in ODMRLogic was not big enough for the entire ' 'measurement. Array will be expanded.\nOld array shape was ' '({0:d}, {1:d}), new shape is ({2:d}, {3:d}).' ''.format( self.odmr_raw_data.shape[0] - self.number_of_lines, self.odmr_raw_data.shape[1], self.odmr_raw_data.shape[0], self.odmr_raw_data.shape[1])) # shift data in the array "up" and add new data at the "bottom" self.odmr_raw_data = np.roll(self.odmr_raw_data, 1, axis=0) self.odmr_raw_data[0] = new_counts # Add new count data to mean signal if self._clearOdmrData: self.odmr_plot_y[:, :] = 0 if self.lines_to_average <= 0: self.odmr_plot_y = np.mean( self.odmr_raw_data[:max(1, self.elapsed_sweeps), :, :], axis=0, dtype=np.float64) else: self.odmr_plot_y = np.mean(self.odmr_raw_data[:max( 1, min(self.lines_to_average, self.elapsed_sweeps)), :, :], axis=0, dtype=np.float64) # Set plot slice of matrix self.odmr_plot_xy = self.odmr_raw_data[:self.number_of_lines, :, :] # Update elapsed time/sweeps self.elapsed_sweeps += 1 self.elapsed_time = time.time() - self._startTime if self.elapsed_time >= self.run_time: self.stopRequested = True # Fire update signals self.sigOdmrElapsedTimeUpdated.emit(self.elapsed_time, self.elapsed_sweeps) self.sigOdmrPlotsUpdated.emit(self.odmr_plot_x, self.odmr_plot_y, self.odmr_plot_xy) self.sigNextLine.emit() return def get_odmr_channels(self): return self._odmr_counter.get_odmr_channels() def get_hw_constraints(self): """ Return the names of all ocnfigured fit functions. @return object: Hardware constraints object """ constraints = self._mw_device.get_limits() return constraints def get_fit_functions(self): """ Return the hardware constraints/limits @return list(str): list of fit function names """ return list(self.fc.fit_list) def do_fit(self, fit_function=None, x_data=None, y_data=None, channel_index=0): """ Execute the currently configured fit on the measurement data. Optionally on passed data """ if (x_data is None) or (y_data is None): x_data = self.odmr_plot_x y_data = self.odmr_plot_y[channel_index] if fit_function is not None and isinstance(fit_function, str): if fit_function in self.get_fit_functions(): self.fc.set_current_fit(fit_function) else: self.fc.set_current_fit('No Fit') if fit_function != 'No Fit': self.log.warning( 'Fit function "{0}" not available in ODMRLogic fit container.' ''.format(fit_function)) self.odmr_fit_x, self.odmr_fit_y, result = self.fc.do_fit( x_data, y_data) if result is None: result_str_dict = {} else: result_str_dict = result.result_str_dict self.sigOdmrFitUpdated.emit(self.odmr_fit_x, self.odmr_fit_y, result_str_dict, self.fc.current_fit) return def save_odmr_data(self, tag=None, colorscale_range=None, percentile_range=None): """ Saves the current ODMR data to a file.""" timestamp = datetime.datetime.now() if tag is None: tag = '' for nch, channel in enumerate(self.get_odmr_channels()): # two paths to save the raw data and the odmr scan data. filepath = self._save_logic.get_path_for_module(module_name='ODMR') filepath2 = self._save_logic.get_path_for_module( module_name='ODMR') if len(tag) > 0: filelabel = '{0}_ODMR_data_ch{1}'.format(tag, nch) filelabel2 = '{0}_ODMR_data_ch{1}_raw'.format(tag, nch) else: filelabel = 'ODMR_data_ch{0}'.format(nch) filelabel2 = 'ODMR_data_ch{0}_raw'.format(nch) # prepare the data in a dict or in an OrderedDict: data = OrderedDict() data2 = OrderedDict() data['frequency (Hz)'] = self.odmr_plot_x data['count data (counts/s)'] = self.odmr_plot_y[nch] data2['count data (counts/s)'] = self.odmr_raw_data[:self. elapsed_sweeps, nch, :] parameters = OrderedDict() parameters['Microwave CW Power (dBm)'] = self.cw_mw_power parameters['Microwave Sweep Power (dBm)'] = self.sweep_mw_power parameters['Run Time (s)'] = self.run_time parameters['Number of frequency sweeps (#)'] = self.elapsed_sweeps parameters['Start Frequency (Hz)'] = self.mw_start parameters['Stop Frequency (Hz)'] = self.mw_stop parameters['Step size (Hz)'] = self.mw_step parameters['Clock Frequency (Hz)'] = self.clock_frequency parameters['Channel'] = '{0}: {1}'.format(nch, channel) if self.fc.current_fit != 'No Fit': parameters['Fit function'] = self.fc.current_fit # add all fit parameter to the saved data: for name, param in self.fc.current_fit_param.items(): parameters[name] = str(param) fig = self.draw_figure(nch, cbar_range=colorscale_range, percentile_range=percentile_range) self._save_logic.save_data(data, filepath=filepath, parameters=parameters, filelabel=filelabel, fmt='%.6e', delimiter='\t', timestamp=timestamp, plotfig=fig) self._save_logic.save_data(data2, filepath=filepath2, parameters=parameters, filelabel=filelabel2, fmt='%.6e', delimiter='\t', timestamp=timestamp) self.log.info('ODMR data saved to:\n{0}'.format(filepath)) return def draw_figure(self, channel_number, cbar_range=None, percentile_range=None): """ Draw the summary figure to save with the data. @param: list cbar_range: (optional) [color_scale_min, color_scale_max]. If not supplied then a default of data_min to data_max will be used. @param: list percentile_range: (optional) Percentile range of the chosen cbar_range. @return: fig fig: a matplotlib figure object to be saved to file. """ freq_data = self.odmr_plot_x count_data = self.odmr_plot_y[channel_number] fit_freq_vals = self.odmr_fit_x fit_count_vals = self.odmr_fit_y matrix_data = self.odmr_plot_xy[:, channel_number] # If no colorbar range was given, take full range of data if cbar_range is None: cbar_range = np.array([np.min(matrix_data), np.max(matrix_data)]) else: cbar_range = np.array(cbar_range) prefix = ['', 'k', 'M', 'G', 'T'] prefix_index = 0 # Rescale counts data with SI prefix while np.max(count_data) > 1000: count_data = count_data / 1000 fit_count_vals = fit_count_vals / 1000 prefix_index = prefix_index + 1 counts_prefix = prefix[prefix_index] # Rescale frequency data with SI prefix prefix_index = 0 while np.max(freq_data) > 1000: freq_data = freq_data / 1000 fit_freq_vals = fit_freq_vals / 1000 prefix_index = prefix_index + 1 mw_prefix = prefix[prefix_index] # Rescale matrix counts data with SI prefix prefix_index = 0 while np.max(matrix_data) > 1000: matrix_data = matrix_data / 1000 cbar_range = cbar_range / 1000 prefix_index = prefix_index + 1 cbar_prefix = prefix[prefix_index] # Use qudi style plt.style.use(self._save_logic.mpl_qd_style) # Create figure fig, (ax_mean, ax_matrix) = plt.subplots(nrows=2, ncols=1) ax_mean.plot(freq_data, count_data, linestyle=':', linewidth=0.5) # Do not include fit curve if there is no fit calculated. if max(fit_count_vals) > 0: ax_mean.plot(fit_freq_vals, fit_count_vals, marker='None') ax_mean.set_ylabel('Fluorescence (' + counts_prefix + 'c/s)') ax_mean.set_xlim(np.min(freq_data), np.max(freq_data)) matrixplot = ax_matrix.imshow( matrix_data, cmap=plt.get_cmap('inferno'), # reference the right place in qd origin='lower', vmin=cbar_range[0], vmax=cbar_range[1], extent=[ np.min(freq_data), np.max(freq_data), 0, self.number_of_lines ], aspect='auto', interpolation='nearest') ax_matrix.set_xlabel('Frequency (' + mw_prefix + 'Hz)') ax_matrix.set_ylabel('Scan #') # Adjust subplots to make room for colorbar fig.subplots_adjust(right=0.8) # Add colorbar axis to figure cbar_ax = fig.add_axes([0.85, 0.15, 0.02, 0.7]) # Draw colorbar cbar = fig.colorbar(matrixplot, cax=cbar_ax) cbar.set_label('Fluorescence (' + cbar_prefix + 'c/s)') # remove ticks from colorbar for cleaner image cbar.ax.tick_params(which=u'both', length=0) # If we have percentile information, draw that to the figure if percentile_range is not None: cbar.ax.annotate(str(percentile_range[0]), xy=(-0.3, 0.0), xycoords='axes fraction', horizontalalignment='right', verticalalignment='center', rotation=90) cbar.ax.annotate(str(percentile_range[1]), xy=(-0.3, 1.0), xycoords='axes fraction', horizontalalignment='right', verticalalignment='center', rotation=90) cbar.ax.annotate('(percentile)', xy=(-0.3, 0.5), xycoords='axes fraction', horizontalalignment='right', verticalalignment='center', rotation=90) return fig def perform_odmr_measurement(self, freq_start, freq_step, freq_stop, power, channel, runtime, fit_function='No Fit', save_after_meas=True, name_tag=''): """ An independant method, which can be called by a task with the proper input values to perform an odmr measurement. @return """ timeout = 30 start_time = time.time() while self.module_state() != 'idle': time.sleep(0.5) timeout -= (time.time() - start_time) if timeout <= 0: self.log.error( 'perform_odmr_measurement failed. Logic module was still locked ' 'and 30 sec timeout has been reached.') return tuple() # set all relevant parameter: self.set_sweep_parameters(freq_start, freq_stop, freq_step, power) self.set_runtime(runtime) # start the scan self.start_odmr_scan() # wait until the scan has started while self.module_state() != 'locked': time.sleep(1) # wait until the scan has finished while self.module_state() == 'locked': time.sleep(1) # Perform fit if requested if fit_function != 'No Fit': self.do_fit(fit_function, channel_index=channel) fit_params = self.fc.current_fit_param else: fit_params = None # Save data if requested if save_after_meas: self.save_odmr_data(tag=name_tag) return self.odmr_plot_x, self.odmr_plot_y, fit_params
class PoiManagerLogic(GenericLogic): """ This is the Logic class for mapping and tracking bright features in the confocal scan. """ _modclass = 'poimanagerlogic' _modtype = 'logic' # declare connectors optimizer1 = Connector(interface='OptimizerLogic') scannerlogic = Connector(interface='ConfocalLogic') savelogic = Connector(interface='SaveLogic') # status vars poi_list = StatusVar(default=OrderedDict()) roi_name = StatusVar(default='') active_poi = StatusVar(default=None) signal_timer_updated = QtCore.Signal() signal_poi_updated = QtCore.Signal() signal_poi_deleted = QtCore.Signal(str) signal_confocal_image_updated = QtCore.Signal() signal_periodic_opt_started = QtCore.Signal() signal_periodic_opt_duration_changed = QtCore.Signal() signal_periodic_opt_stopped = QtCore.Signal() def __init__(self, config, **kwargs): super().__init__(config=config, **kwargs) self._current_poi_key = None self.go_to_crosshair_after_refocus = False # default value # timer and its handling for the periodic refocus self.timer = None self.time_left = 0 self.timer_step = 0 self.timer_duration = 300 # locking for thread safety self.threadlock = Mutex() def on_activate(self): """ Initialisation performed during activation of the module. """ self._optimizer_logic = self.optimizer1() self._confocal_logic = self.scannerlogic() self._save_logic = self.savelogic() # listen for the refocus to finish self._optimizer_logic.sigRefocusFinished.connect(self._refocus_done) # listen for the deactivation of a POI caused by moving to a different position self._confocal_logic.signal_change_position.connect(self.user_move_deactivates_poi) # Initialise the roi_map_data (xy confocal image) self.roi_map_data = self._confocal_logic.xy_image def on_deactivate(self): return def user_move_deactivates_poi(self, tag): """ Deactivate the active POI if the confocal microscope scanner position is moved by anything other than the optimizer """ pass def add_poi(self, position=None, key=None, emit_change=True): """ Creates a new poi and adds it to the list. @return int: key of this new poi A position can be provided (such as during re-loading a saved ROI). If no position is provided, then the current crosshair position is used. """ # If there are only 2 POIs (sample and crosshair) then the newly added POI needs to start the sample drift logging. if len(self.poi_list) == 2: self.poi_list['sample']._creation_time = time.time() # When the poimanager is activated the 'sample' poi is created because it is needed # from the beginning for various functionalities. If the tracking of the sample is started it has # to be reset such that this first point is deleted here # Probably this can be solved a lot nicer. self.poi_list['sample'].delete_last_position(empty_array_completely=True) self.poi_list['sample'].add_position_to_history(position=[0, 0, 0]) self.poi_list['sample'].set_coords_in_sample(coords=[0, 0, 0]) if position is None: position = self._confocal_logic.get_position()[:3] if len(position) != 3: self.log.error('Given position is not 3-dimensional.' 'Please pass POIManager a 3-dimensional position to set a POI.') return new_poi = PoI(pos=position, key=key) self.poi_list[new_poi.get_key()] = new_poi # The POI coordinates are set relative to the last known sample position most_recent_sample_pos = self.poi_list['sample'].get_position_history()[-1, :][1:4] this_poi_coords = position - most_recent_sample_pos new_poi.set_coords_in_sample(coords=this_poi_coords) # Since POI was created at current scanner position, it automatically # becomes the active POI. self.set_active_poi(poikey=new_poi.get_key()) if emit_change: self.signal_poi_updated.emit() return new_poi.get_key() def get_confocal_image_data(self): """ Get the current confocal xy scan data to hold as image of ROI""" # get the roi_map_data (xy confocal image) self.roi_map_data = self._confocal_logic.xy_image self.signal_confocal_image_updated.emit() def get_all_pois(self, abc_sort=False): """ Returns a list of the names of all existing POIs. @return string[]: List of names of the POIs Also crosshair and sample are included. """ if abc_sort is False: return sorted(self.poi_list.keys()) elif abc_sort is True: # First create a dictionary with poikeys indexed against names poinames = [''] * len(self.poi_list.keys()) for i, poikey in enumerate(self.poi_list.keys()): poiname = self.poi_list[poikey].get_name() poinames[i] = [poiname, poikey] # Sort names in the way that humans expect (site1, site2, site11, etc) # Regular expressions to make sorting key convert = lambda text: int(text) if text.isdigit() else text alphanum_key = lambda key: [convert(c) for c in re.split('([0-9]+)', key[0])] # Now we can sort poinames by name and return keys in that order return [key for [name, key] in sorted(poinames, key=alphanum_key)] else: # TODO: produce sensible error about unknown value of abc_sort. self.log.debug('fix TODO!') # TODO: Find a way to return a list of POI keys sorted in order of the POI names. def delete_last_position(self, poikey=None): """ Delete the last position in the history. @param string poikey: the key of the poi @return int: error code (0:OK, -1:error) """ if poikey is not None and poikey in self.poi_list.keys(): self.poi_list[poikey].delete_last_position() self.poi_list['sample'].delete_last_position() self.signal_poi_updated.emit() return 0 else: self.log.error('The last position of given POI ({0}) could not be deleted.'.format( poikey)) return -1 def delete_poi(self, poikey=None): """ Completely deletes the whole given poi. @param string poikey: the key of the poi @return int: error code (0:OK, -1:error) Does not delete the crosshair and sample. """ if poikey is not None and poikey in self.poi_list.keys(): if poikey is 'crosshair' or poikey is 'sample': self.log.warning('You cannot delete the crosshair or sample.') return -1 del self.poi_list[poikey] # If the active poi was deleted, there is no way to automatically choose # another active POI, so we deactivate POI if self.active_poi is not None and poikey == self.active_poi.get_key(): self._deactivate_poi() self.signal_poi_updated.emit() self.signal_poi_deleted.emit(poikey) return 0 elif poikey is None: self.log.warning('No POI for deletion specified.') else: self.log.error('X. The given POI ({0}) does not exist.'.format( poikey)) return -1 def optimise_poi(self, poikey=None): """ Starts the optimisation procedure for the given poi. @param string poikey: the key of the poi @return int: error code (0:OK, -1:error) This is threaded, so it returns directly. The function _refocus_done handles the data when the optimisation returns. """ if poikey is not None and poikey in self.poi_list.keys(): self.poi_list['crosshair'].add_position_to_history(position=self._confocal_logic.get_position()[:3]) self._current_poi_key = poikey self._optimizer_logic.start_refocus( initial_pos=self.get_poi_position(poikey=poikey), caller_tag='poimanager') return 0 else: self.log.error( 'Z. The given POI ({0}) does not exist.'.format(poikey)) return -1 def go_to_poi(self, poikey=None): """ Goes to the given poi and saves it as the current one. @param string poikey: the key of the poi @return int: error code (0:OK, -1:error) """ if poikey is not None and poikey in self.poi_list.keys(): self._current_poi_key = poikey x, y, z = self.get_poi_position(poikey=poikey) self._confocal_logic.set_position('poimanager', x=x, y=y, z=z) else: self.log.error('The given POI ({0}) does not exist.'.format( poikey)) return -1 # This is now the active POI to send to save logic for naming in any saved filenames. self.set_active_poi(poikey) #Fixme: After pressing the Go to Poi button the active poi is empty and the following lines do fix this # The time.sleep is somehow needed if not active_poi can not be set time.sleep(0.001) self.active_poi = self.poi_list[poikey] self.signal_poi_updated.emit() def get_poi_position(self, poikey=None): """ Returns the current position of the given poi, calculated from the POI coords in sample and the current sample position. @param string poikey: the key of the poi @return """ if poikey is not None and poikey in self.poi_list.keys(): poi_coords = self.poi_list[poikey].get_coords_in_sample() sample_pos = self.poi_list['sample'].get_position_history()[-1, :][1:4] return sample_pos + poi_coords else: self.log.error('G. The given POI ({0}) does not exist.'.format( poikey)) return [-1., -1., -1.] def set_new_position(self, poikey=None, newpos=None): """ Moves the given POI to a new position, and uses this information to update the sample position. @param string poikey: the key of the poi @param float[3] newpos: coordinates of the new position @return int: error code (0:OK, -1:error) """ # If no new position is given, take the current confocal crosshair position if newpos is None: newpos = self._confocal_logic.get_position()[:3] if poikey is not None and poikey in self.poi_list.keys(): if len(newpos) != 3: self.log.error('Length of set poi is not 3.') return -1 # Add new position to trace of POI self.poi_list[poikey].add_position_to_history(position=newpos) # Calculate sample shift and add it to the trace of 'sample' POI sample_shift = newpos - self.get_poi_position(poikey=poikey) sample_shift += self.poi_list['sample'].get_position_history()[-1, :][1:4] self.poi_list['sample'].add_position_to_history(position=sample_shift) # signal POI has been updated (this will cause GUI to redraw) if (poikey is not 'crosshair') and (poikey is not 'sample'): self.signal_poi_updated.emit() return 0 self.log.error('J. The given POI ({0}) does not exist.'.format(poikey)) return -1 def move_coords(self, poikey=None, newpos=None): """Updates the coords of a given POI, and adds a position to the POI history, but DOES NOT update the sample position. """ if newpos is None: newpos = self._confocal_logic.get_position()[:3] if poikey is not None and poikey in self.poi_list.keys(): if len(newpos) != 3: self.log.error('Length of set poi is not 3.') return -1 this_poi = self.poi_list[poikey] return_val = this_poi.add_position_to_history(position=newpos) sample_pos = self.poi_list['sample'].get_position_history()[-1, :][1:4] new_coords = newpos - sample_pos this_poi.set_coords_in_sample(new_coords) self.signal_poi_updated.emit() return return_val self.log.error('JJ. The given POI ({0}) does not exist.'.format(poikey)) return -1 def rename_poi(self, poikey=None, name=None, emit_change=True): """ Sets the name of the given poi. @param string poikey: the key of the poi @param string name: name of the poi to be set @return int: error code (0:OK, -1:error) """ if poikey is not None and name is not None and poikey in self.poi_list.keys(): success = self.poi_list[poikey].set_name(name=name) # if this is the active POI then we need to update poi tag in savelogic if self.poi_list[poikey] == self.active_poi: self.update_poi_tag_in_savelogic() if emit_change: self.signal_poi_updated.emit() return success else: self.log.error('AAAThe given POI ({0}) does not exist.'.format( poikey)) return -1 def start_periodic_refocus(self, poikey=None): """ Starts the perodic refocussing of the poi. @param float duration: (optional) the time between periodic optimization @param string poikey: (optional) the key of the poi to be set and refocussed on. @return int: error code (0:OK, -1:error) """ if poikey is not None and poikey in self.poi_list.keys(): self._current_poi_key = poikey else: # Todo: warning message that active POI used by default self._current_poi_key = self.active_poi.get_key() self.log.info('Periodic refocus on {0}.'.format(self._current_poi_key)) self.timer_step = 0 self.timer = QtCore.QTimer() self.timer.setSingleShot(False) self.timer.timeout.connect(self._periodic_refocus_loop) self.timer.start(300) self.signal_periodic_opt_started.emit() return 0 def set_periodic_optimize_duration(self, duration=None): """ Change the duration of the periodic optimize timer during active periodic refocussing. @param float duration: (optional) the time between periodic optimization. """ if duration is not None: self.timer_duration = duration else: self.log.warning('No timer duration given, using {0} s.'.format( self.timer_duration)) self.signal_periodic_opt_duration_changed.emit() def _periodic_refocus_loop(self): """ This is the looped function that does the actual periodic refocus. If the time has run out, it refocussed the current poi. Otherwise it just updates the time that is left. """ self.time_left = self.timer_step - time.time() + self.timer_duration self.signal_timer_updated.emit() if self.time_left <= 0: self.timer_step = time.time() self.optimise_poi(poikey=self._current_poi_key) def stop_periodic_refocus(self): """ Stops the perodic refocussing of the poi. @return int: error code (0:OK, -1:error) """ if self.timer is None: self.log.warning('No timer to stop.') return -1 self.timer.stop() self.timer = None self.signal_periodic_opt_stopped.emit() return 0 def _refocus_done(self, caller_tag, optimal_pos): """ Gets called automatically after the refocus is done and saves the new position to the poi history. Also it tracks the sample and may go back to the crosshair. @return int: error code (0:OK, -1:error) """ # We only need x, y, z optimized_position = optimal_pos[0:3] # If the refocus was on the crosshair, then only update crosshair POI and don't # do anything with sample position. caller_tags = ['confocalgui', 'magnet_logic', 'singleshot_logic'] if caller_tag in caller_tags: self.poi_list['crosshair'].add_position_to_history(position=optimized_position) # If the refocus was initiated here by poimanager, then update POI and sample elif caller_tag == 'poimanager': if self._current_poi_key is not None and self._current_poi_key in self.poi_list.keys(): self.set_new_position(poikey=self._current_poi_key, newpos=optimized_position) if self.go_to_crosshair_after_refocus: temp_key = self._current_poi_key self.go_to_poi(poikey='crosshair') self._current_poi_key = temp_key else: self.go_to_poi(poikey=self._current_poi_key) return 0 else: self.log.error('The given POI ({0}) does not exist.'.format( self._current_poi_key)) return -1 else: self.log.warning("Unknown caller_tag for the optimizer. POI " "Manager does not know what to do with optimized " "position, and has done nothing.") def reset_roi(self): del self.poi_list self.poi_list = dict() self.active_poi = None self.roi_name = '' # initally add crosshair to the pois crosshair = PoI(pos=[0, 0, 0], name='crosshair') crosshair._key = 'crosshair' self.poi_list[crosshair._key] = crosshair # Re-initialise sample in the poi list sample = PoI(pos=[0, 0, 0], name='sample') sample._key = 'sample' self.poi_list[sample._key] = sample self.signal_poi_updated.emit() def set_active_poi(self, poikey=None): """ Set the active POI object. """ if poikey is None: # If poikey is none and no active poi is set, then do nothing if self.active_poi is None: return else: self.active_poi = None elif poikey in self.get_all_pois(): # If poikey is the current active POI then do nothing if self.poi_list[poikey] == self.active_poi: return else: self.active_poi = self.poi_list[poikey] else: # todo: error poikey unknown return -1 self.update_poi_tag_in_savelogic() self.signal_poi_updated.emit() # todo: this breaks the emit_change = false case def _deactivate_poi(self): self.set_active_poi(poikey=None) def update_poi_tag_in_savelogic(self): if self.active_poi is not None: self._save_logic.active_poi_name = self.active_poi.get_name() else: self._save_logic.active_poi_name = '' def save_poi_map_as_roi(self): """ Save a list of POIs with their coordinates to a file. """ # File path and name filepath = self._save_logic.get_path_for_module(module_name='ROIs') # We will fill the data OderedDict to send to savelogic data = OrderedDict() # Lists for each column of the output file poinames = [] poikeys = [] x_coords = [] y_coords = [] z_coords = [] for poikey in self.get_all_pois(abc_sort=True): if poikey is not 'sample' and poikey is not 'crosshair': thispoi = self.poi_list[poikey] poinames.append(thispoi.get_name()) poikeys.append(poikey) x_coords.append(thispoi.get_coords_in_sample()[0]) y_coords.append(thispoi.get_coords_in_sample()[1]) z_coords.append(thispoi.get_coords_in_sample()[2]) data['POI Name'] = np.array(poinames) data['POI Key'] = np.array(poikeys) data['X'] = np.array(x_coords) data['Y'] = np.array(y_coords) data['Z'] = np.array(z_coords) self._save_logic.save_data( data, filepath=filepath, filelabel=self.roi_name, fmt=['%s', '%s', '%.6e', '%.6e', '%.6e'] ) self.log.debug('ROI saved to:\n{0}'.format(filepath)) return 0 def load_roi_from_file(self, filename=None): if filename is None: return -1 with open(filename, 'r') as roifile: for line in roifile: if line[0] != '#' and line.split()[0] != 'NaN': saved_poi_name = line.split()[0] saved_poi_key = line.split()[1] saved_poi_coords = [ float(line.split()[2]), float(line.split()[3]), float(line.split()[4])] this_poi_key = self.add_poi( position=saved_poi_coords, key=saved_poi_key, emit_change=False) self.rename_poi(poikey=this_poi_key, name=saved_poi_name, emit_change=False) # Now that all the POIs are created, emit the signal for other things (ie gui) to update self.signal_poi_updated.emit() return 0 @poi_list.constructor def dict_to_poi_list(self, val): pdict = {} # initially add crosshair to the pois crosshair = PoI(pos=[0, 0, 0], name='crosshair') crosshair._key = 'crosshair' pdict[crosshair._key] = crosshair # initally add sample to the pois sample = PoI(pos=[0, 0, 0], name='sample') sample._key = 'sample' pdict[sample._key] = sample if isinstance(val, dict): for key, poidict in val.items(): try: if len(poidict['pos']) >= 3: newpoi = PoI(name=poidict['name'], key=poidict['key']) newpoi.set_coords_in_sample(poidict['pos']) newpoi._creation_time = poidict['time'] newpoi._position_time_trace = poidict['history'] pdict[key] = newpoi except Exception as e: self.log.exception('Could not load PoI {0}: {1}'.format(key, poidict)) return pdict @poi_list.representer def poi_list_to_dict(self, val): pdict = { key: poi.to_dict() for key, poi in val.items() } return pdict @active_poi.representer def active_poi_to_dict(self, val): if isinstance(val, PoI): return val.to_dict() return None @active_poi.constructor def dict_to_active_poi(self, val): try: if isinstance(val, dict): if len(val['pos']) >= 3: newpoi = PoI(pos=val['pos'], name=val['name'], key=val['key']) newpoi._creation_time = val['time'] newpoi._position_time_trace = val['history'] return newpoi except Exception as e: self.log.exception('Could not load active poi {0}'.format(val)) return None def triangulate(self, r, a1, b1, c1, a2, b2, c2): """ Reorients a coordinate r that is known relative to reference points a1, b1, c1 to produce a new vector rnew that has exactly the same relation to rotated/shifted/tilted reference positions a2, b2, c2. @param np.array r: position to be remapped. @param np.array a1: initial location of ref1. @param np.array a2: final location of ref1. @param np.array b1, b2, c1, c2: similar for ref2 and ref3 """ ab_old = b1 - a1 ac_old = c1 - a1 ab_new = b2 - a2 ac_new = c2 - a2 # Firstly, find the angle to rotate ab_old onto ab_new. This rotation must be done in # the plane that contains these two vectors, which means rotating about an axis # perpendicular to both of them (the cross product). axis1 = np.cross(ab_old, ab_new) # Only works if ab_old and ab_new are not parallel axis1length = np.sqrt((axis1 * axis1).sum()) if axis1length == 0: ab_olddif = ab_old + np.array([100, 0, 0]) axis1 = np.cross(ab_old, ab_olddif) # normalising the axis1 vector axis1 = axis1 / np.sqrt((axis1 * axis1).sum()) # The dot product gives the angle between ab_old and ab_new dot = np.dot(ab_old, ab_new) x_modulus = np.sqrt((ab_old * ab_old).sum()) y_modulus = np.sqrt((ab_new * ab_new).sum()) # float errors can cause the division to be slightly above 1 for 90 degree rotations, which # will confuse arccos. cos_angle = min(dot / x_modulus / y_modulus, 1) angle1 = np.arccos(cos_angle) # angle in radians # Construct a rotational matrix for axis1 n1 = axis1[0] n2 = axis1[1] n3 = axis1[2] m1 = np.matrix(((((n1 * n1) * (1 - np.cos(angle1)) + np.cos(angle1)), ((n1 * n2) * (1 - np.cos(angle1)) - n3 * np.sin(angle1)), ((n1 * n3) * (1 - np.cos(angle1)) + n2 * np.sin(angle1)) ), (((n2 * n1) * (1 - np.cos(angle1)) + n3 * np.sin(angle1)), ((n2 * n2) * (1 - np.cos(angle1)) + np.cos(angle1)), ((n2 * n3) * (1 - np.cos(angle1)) - n1 * np.sin(angle1)) ), (((n3 * n1) * (1 - np.cos(angle1)) - n2 * np.sin(angle1)), ((n3 * n2) * (1 - np.cos(angle1)) + n1 * np.sin(angle1)), ((n3 * n3) * (1 - np.cos(angle1)) + np.cos(angle1)) ) ) ) # Now that ab_old can be rotated to overlap with ab_new, we need to rotate in another # axis to fix "tilt". By choosing ab_new as the rotation axis we ensure that the # ab vectors stay where they need to be. # ac_old_rot is the rotated ac_old (around axis1). We need to find the angle to rotate # ac_old_rot around ab_new to get ac_new. ac_old_rot = np.array(np.dot(m1, ac_old))[0] axis2 = -ab_new # TODO: check maths to find why this negative sign is necessary. Empirically it is now working. axis2 = axis2 / np.sqrt((axis2 * axis2).sum()) # To get the angle of rotation it is most convenient to work in the plane for which axis2 is the normal. # We must project vectors ac_old_rot and ac_new into this plane. a = ac_old_rot - np.dot(ac_old_rot, axis2) * axis2 # projection of ac_old_rot in the plane of rotation about axis2 b = ac_new - np.dot(ac_new, axis2) * axis2 # projection of ac_new in the plane of rotation about axis2 # The dot product gives the angle of rotation around axis2 dot = np.dot(a, b) x_modulus = np.sqrt((a * a).sum()) y_modulus = np.sqrt((b * b).sum()) cos_angle = min(dot / x_modulus / y_modulus, 1) # float errors can cause the division to be slightly above 1 for 90 degree rotations, which will confuse arccos. angle2 = np.arccos(cos_angle) # angle in radians # Construct a rotation matrix around axis2 n1 = axis2[0] n2 = axis2[1] n3 = axis2[2] m2 = np.matrix(((((n1 * n1) * (1 - np.cos(angle2)) + np.cos(angle2)), ((n1 * n2) * (1 - np.cos(angle2)) - n3 * np.sin(angle2)), ((n1 * n3) * (1 - np.cos(angle2)) + n2 * np.sin(angle2)) ), (((n2 * n1) * (1 - np.cos(angle2)) + n3 * np.sin(angle2)), ((n2 * n2) * (1 - np.cos(angle2)) + np.cos(angle2)), ((n2 * n3) * (1 - np.cos(angle2)) - n1 * np.sin(angle2)) ), (((n3 * n1) * (1 - np.cos(angle2)) - n2 * np.sin(angle2)), ((n3 * n2) * (1 - np.cos(angle2)) + n1 * np.sin(angle2)), ((n3 * n3) * (1 - np.cos(angle2)) + np.cos(angle2)) ) ) ) # To find the new position of r, displace by (a2 - a1) and do the rotations a1r = r - a1 rnew = a2 + np.array(np.dot(m2, np.array(np.dot(m1, a1r))[0]))[0] return rnew def reorient_roi(self, ref1_coords, ref2_coords, ref3_coords, ref1_newpos, ref2_newpos, ref3_newpos): """ Move and rotate the ROI to a new position specified by the newpos of 3 reference POIs from the saved ROI. @param ref1_coords: coordinates (from ROI save file) of reference 1. @param ref2_coords: similar, ref2. @param ref3_coords: similar, ref3. @param ref1_newpos: the new (current) position of POI reference 1. @param ref2_newpos: similar, ref2. @param ref3_newpos: similar, ref3. """ for poikey in self.get_all_pois(abc_sort=True): if poikey is not 'sample' and poikey is not 'crosshair': thispoi = self.poi_list[poikey] old_coords = thispoi.get_coords_in_sample() new_coords = self.triangulate(old_coords, ref1_coords, ref2_coords, ref3_coords, ref1_newpos, ref2_newpos, ref3_newpos) self.move_coords(poikey=poikey, newpos=new_coords) def autofind_pois(self, neighborhood_size=1, min_threshold=10000, max_threshold=1e6): """Automatically search the xy scan image for POIs. @param neighborhood_size: size in microns. Only the brightest POI per neighborhood will be found. @param min_threshold: POIs must have c/s above this threshold. @param max_threshold: POIs must have c/s below this threshold. """ # Calculate the neighborhood size in pixels from the image range and resolution x_range_microns = np.max(self.roi_map_data[:, :, 0]) - np.min(self.roi_map_data[:, :, 0]) y_range_microns = np.max(self.roi_map_data[:, :, 1]) - np.min(self.roi_map_data[:, :, 1]) y_pixels = len(self.roi_map_data) x_pixels = len(self.roi_map_data[1, :]) pixels_per_micron = np.max([x_pixels, y_pixels]) / np.max([x_range_microns, y_range_microns]) # The neighborhood in pixels is nbhd_size * pixels_per_um, but it must be 1 or greater neighborhood_pix = int(np.max([math.ceil(pixels_per_micron * neighborhood_size), 1])) data = self.roi_map_data[:, :, 3] data_max = filters.maximum_filter(data, neighborhood_pix) maxima = (data == data_max) data_min = filters.minimum_filter(data, 3 * neighborhood_pix) diff = ((data_max - data_min) > min_threshold) maxima[diff is False] = 0 labeled, num_objects = ndimage.label(maxima) xy = np.array(ndimage.center_of_mass(data, labeled, range(1, num_objects + 1))) for count, pix_pos in enumerate(xy): poi_pos = self.roi_map_data[pix_pos[0], pix_pos[1], :][0:3] this_poi_key = self.add_poi(position=poi_pos, emit_change=False) self.rename_poi(poikey=this_poi_key, name='spot' + str(count), emit_change=False) # Now that all the POIs are created, emit the signal for other things (ie gui) to update self.signal_poi_updated.emit()
class ProcessValueModifier(GenericLogic, ProcessInterface): """ This interfuse can be used to modify a process value on the fly. It needs a 2D array to interpolate General form : [[x_0, y_0], [x_1, y_1], ... , [x_n, y_n]] Example : [[0,0], [1,10]] With this example, the value 0.5 read from the hardware would be transformed to 5 sent to the logic. process_value_modifier: module.Class: 'interfuse.process_value_modifier.ProcessValueModifier' connect: hardware: 'processdummy' calibration_file: 'PATH/process_modifier.calib' force_calibration_from_file: False This calibration is stored and remembered as a status variable. If this variable is None, the calibration can be read from a simple file with two columns : # X Y 0 0 1 10 """ _modclass = 'ProcessValueModifier' _modtype = 'interfuse' hardware = Connector(interface='ProcessInterface') _calibration = StatusVar(default=None) _calibration_file = ConfigOption('calibration_file', None) _force_calibration_from_file = ConfigOption('force_calibration_from_file', False) _interpolated_function = None _new_unit = ConfigOption('new_unit', None) def on_activate(self): """ Activate module. """ self._hardware = self.hardware() if self._force_calibration_from_file and self._calibration_file is None: self.log.error('Loading from calibration is enforced but no calibration file has been' 'given.') if self._force_calibration_from_file or (self._calibration is None and self._calibration_file is not None): self.log.info('Loading from calibration file.') calibration = np.loadtxt(self._calibration_file) self.update_calibration(calibration) else: self.update_calibration() def on_deactivate(self): """ Deactivate module. """ pass def update_calibration(self, calibration=None): """ Construct the interpolated function from the calibration data calibration (optional) 2d array : A new calibration to set """ if calibration is not None: self._calibration = calibration if self._calibration is None: self._interpolated_function = lambda x: x else: self._interpolated_function = interp1d(self._calibration[:, 0], self._calibration[:, 1]) def reset_to_identity(self): """ Reset the calibration data to use identity """ self._calibration = None self.update_calibration() def get_process_value(self): """ Return the process value modified """ if self._interpolated_function is not None: return float(self._interpolated_function(self._hardware.get_process_value())) else: self.log.error('No calibration was found, please set the process value modifier data first.') return 0 def get_process_unit(self): """ Return the process unit """ if self._new_unit is not None: return self._new_unit else: return self._hardware.get_process_unit()