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)
Пример #2
0
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
Пример #3
0
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
Пример #4
0
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)
Пример #5
0
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
Пример #6
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
Пример #7
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
Пример #8
0
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}
Пример #9
0
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
Пример #10
0
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))
Пример #11
0
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
Пример #12
0
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
Пример #13
0
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
Пример #14
0
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
Пример #15
0
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)
Пример #16
0
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]
Пример #17
0
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)
Пример #18
0
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
Пример #19
0
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
Пример #20
0
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()
Пример #21
0
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()