Exemplo n.º 1
0
class TestLogic(GenericLogic):
    """ This is the Logic class for testing. """

    # connectors
    firsthardware = Connector(interface='FirstTestInterface')
    secondhardware = Connector(interface='SecondTestInterface')

    testvar = StatusVar(name='testvar', default=None)

    def __init__(self, config, **kwargs):
        super().__init__(config=config, **kwargs)
        return

    def on_activate(self):
        """	
        Initialisation performed during activation of the module.
        """
        return

    def on_deactivate(self):
        """ Deinitialisation performed during deactivation of the module.	
        """
        return

    def call_test1(self):
        return self.firsthardware().test()

    def call_test2(self):
        return self.secondhardware().test()
Exemplo n.º 2
0
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)
Exemplo n.º 3
0
class ODMRLogic(GenericLogic):
    """This is the Logic class for ODMR."""

    # declare connectors
    odmrcounter = Connector(interface='ODMRCounterInterface')
    fitlogic = Connector(interface='FitLogic')
    microwave1 = Connector(interface='MicrowaveInterface')
    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
        # in case of sweep parameters being updated, so the data arrays need
        # to be resized
        self._sweep_params_updated = False

        # Initalize the ODMR data arrays (mean signal and sweep matrix)
        self._initialize_odmr_plots()

        # 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.elapsed_sweeps = 0
        self.elapsed_time = 0.0
        self.sigOdmrElapsedTimeUpdated.emit(self.elapsed_time, self.elapsed_sweeps)
        
        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)
        self.odmr_raw_data = np.zeros(
                    [1,
                    len(self._odmr_counter.get_odmr_channels()),
                    self.odmr_plot_x.size]
                )
        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)
        self._sweep_params_updated = True
        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.stopRequested = False
            self.fc.clear_result()

            # If sweep parameters have been updated since last call,
            # need to clear the data and re-initialise the buffers
            if self._sweep_params_updated:
                self._initialize_odmr_plots()
                self._sweep_params_updated = False

            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):
        """¨Clear ODMR data and elapsed time

        Only works when a scan is not currently running
        """
        with self.threadlock:
            if self.module_state() != 'locked':
                self._initialize_odmr_plots()
        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

            # 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
            self.odmr_raw_data = np.insert(self.odmr_raw_data, 0, new_counts, 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
                )

            # Get xy plot data
            pad_amount = self.number_of_lines - self.odmr_raw_data.shape[0]
            if pad_amount > 0:
                # Pad out data if needed to fill the requested size of plot
                self.odmr_plot_xy = np.concatenate((self.odmr_raw_data, 
                                                   np.zeros((pad_amount, 
                                                   *self.odmr_raw_data.shape[1:]))))
            else:
                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, colorscale_range=None, percentile_range=None):
        """ Saves the current ODMR data to a file."""
        timestamp = datetime.datetime.now()

        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')

            # Label file with alphanumeric characters in channel name
            filelabel = 'ODMR_{0}'.format("".join(x for x in channel if x.isalnum()))
            filelabel2 = 'ODMR_{0}_raw'.format("".join(x for x in channel if x.isalnum()))

            # 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['Acquisiton Time (s)'] = self.elapsed_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['Lock-in'] = self._lock_in_active
            parameters['Oversampling'] = self._oversampling
            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))
        
        mode, is_running = self._mw_device.get_status()
        self.sigOutputStateUpdated.emit(mode, is_running)
        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
Exemplo n.º 4
0
class ODMRLogic(GenericLogic):
    """This is the Logic class for ODMR."""

    # declare connectors
    odmrcounter = Connector(interface='ODMRCounterInterface')
    fitlogic = Connector(interface='FitLogic')
    microwave1 = Connector(interface='MicrowaveInterface')
    savelogic = Connector(interface='SaveLogic')
    taskrunner = Connector(interface='TaskRunner')
    # Connecting to camera logic
    camera = Connector(interface='CameraLogic')

    # config option
    mw_scanmode = ConfigOption(
        'scanmode',
        'LIST',
        missing='warn',
        converter=lambda x: MicrowaveMode[x.upper()])
    # Default clock frequency is set dependant on exp. time. here f is in
    # milliseconds.
    f = 1
    exp_time = StatusVar('exp_time', f)
    clock_frequency = StatusVar('clock_frequency', 1. / ((f / 1000.) + 0.0))
    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()
        self._camera = self.camera()

        # 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]
        )
        # The array for images of the entire sweep is intialized.
        self.sweep_images = np.zeros(
            (self.odmr_plot_x.size, *np.flip(self._camera.get_size(), axis=0))
        )
        # 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()
        # The camera's deactivate function is called as well.
        self._camera.on_deactivate()
        # 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', 'contrast'])
        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, _ = 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
        ## This must be dependant on the exposure time
        @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)
            exp_res_dict = {0: 1000., 1: 1000000.}
            self.clock_frequency = 1. / ((self.exp_time / exp_res_dict[self._camera.get_exposure_resolution()]) + 0.0)
        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 set_exp_time(self, exp, cur_res_index):
        '''Exp time in mseconds in set from the GUI and updated for the cam class as well as in the variable
        in the ODMR logic class. The clock frequency variable is updated as well since it depends on the exp
        time of the camera.

        @param int: exp ~ 1ms - 10000ms
        '''
        self._camera.set_exposure_resolution(cur_res_index)
        self._camera.set_exposure(exp)
        self.exp_time = exp
        exp_res_dict = {0: 1000., 1: 1000000.}
        self.clock_frequency = 1. / ((exp / exp_res_dict[cur_res_index]) + 0.0)

    def _start_odmr_counter(self):
        """
        Starting the ODMR counter and set up the clock for it.
        No counter is actually set up instead the clock is set up and the camera trigger mode is set
        to edge trigger for every image in the sequence.

        @return int: error code (0:OK, -1:error)
        """

        clock_status = self._odmr_counter.set_up_odmr_clock(
            clock_frequency=self.clock_frequency, no_x=self.odmr_plot_x.size)
        # Seting exposure mode on camera via logic "Ext Trig Internal"
        # self._camera.set_trigger_seq("Ext Trig Internal")
        self._camera.set_trigger_seq("Edge Trigger")
        if clock_status < 0:
            return -1
        # Try not running the counter since we dont need it
        # 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)
        """
        # Added a line in DAQmx hardware to wait until task is done when closing so as to allow camera to
        # complete the acquisition
        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

    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]
            )
            # Sweep images are set to zero at every new scan
            self.sweep_images = np.zeros(
                (self.odmr_plot_x.size, *np.flip(self._camera.get_size(), axis=0))
            )
            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()
                self._camera.set_trigger_seq("Internal Trigger")
                return

            # if during the scan a clearing of the ODMR data is needed:
            if self._clearOdmrData:
                self.elapsed_sweeps = 0
                self.sweep_images = np.zeros(
                    (self.odmr_plot_x.size, *np.flip(self._camera.get_size(), axis=0))
                )
                self._startTime = time.time()

            # reset position so every line starts from the same frequency
            self.reset_sweep()

            # Acquire count data
            # The trigger sequcne is started below after the specified delay.
            # The camera is also set to acquire and the code only goes below that line after
            # all the images in the sequence has been acquired. The triggers
            # are all stopped after that.
            error, new_counts = self._odmr_counter.count_odmr(
                length=self.odmr_plot_x.size)
            self._camera.start_trigger_seq(self.odmr_plot_x.size * 2)
            # self._odmr_counter.stop_tasks()
            # The collected frames are then acquired by the logic here from cam logic Should consider memory issues
            # for the future.
            frames = self._camera.get_last_image().astype('float')
            # The reference images from switch off time should be subtracted as below. For dummy measurements
            # so as to not be just left with noise we can do a dummy
            # subtraction.
            new_counts = (frames[0::2] - frames[1::2]) / (frames[1::2] + frames[0::2]) * 100
            # Remove for actual mesurements
            # new_counts = frames[1::2] - np.full_like(frames[0::2], 1)
            # The sweep images are added up and the new counts are taken as the mean of the image which is what
            # ends up being plotted as odmr_plot_y
            self.sweep_images += new_counts
            new_counts = np.mean(new_counts, axis=(1, 2))

            if error==-1:
                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 ['Prime95B']

    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 print_coords(self, event, x, y, flags, param):
        '''The coords for finding the pixel spectrum are determined here from the mouse click callback.
        '''
        if (event == cv2.EVENT_LBUTTONDOWN):
            self.coord = (y, x)
            cv2.destroyAllWindows()

    def do_pixel_spectrum(self, frames):
        '''This is called from the fit depending on which button is clicked in the GUI. It displays an image
        which is the sum of all the sweep images divided by number of images so as to make sure all the features
        are seen.
        The coords of selected point are then found by mouse callback and the spectrum made into the new odmr_plot_y
        data as seen in do_fit()
        '''
        # So as to have a smaller images. Hence the times 2 in the print_coords
        # function to get actual coords.
        frame = np.sum(frames, axis=(0)) / np.shape(frames)[0]
        frame = frame.astype(np.uint16)
        # Needed because cv2 can handle only uint8(?) images.
        frame = cv2.normalize(
            frame,
            dst=None,
            alpha=0,
            beta=65535,
            norm_type=cv2.NORM_MINMAX)
        cv2.imshow(f'Sweep Image : {np.shape(frames)[0]}', frame)
        cv2.setMouseCallback(
            f'Sweep Image : {np.shape(frames)[0]}',
            self.print_coords)
        cv2.waitKey(0)

    def do_fit(
            self,
            fit_function=None,
            x_data=None,
            y_data=None,
            channel_index=0,
            pixel_fit=False):
        """
        Execute the currently configured fit on the measurement data. Optionally on passed data
        """
        # To enable default odmr_plot_y if no pixel is clicke and imshow is
        # just closed. Good for preview.
        self.coord = None
        if pixel_fit and np.count_nonzero(self.sweep_images) != 0:
            frames = self.sweep_images / self.elapsed_sweeps
            frames1 = np.zeros((np.shape(frames)[0], 600, 600))
            frames1[:] = [
                cv2.resize(
                    cv2.flip(frame, 0), (600, 600), interpolation=cv2.INTER_AREA) for frame in frames]
            frames = frames1
            self.do_pixel_spectrum(frames)
            # If no mouse click happens the odmr_plot_y data is not updated and stays the same.
            # This ends up allowing us to have a preview of the entire sweep as
            # well.

            if self.coord is not None:
                x_data = self.odmr_plot_x
                y_data = np.zeros(
                    [len(self.get_odmr_channels()), self.odmr_plot_x.size])
                y_data[0] = frames[:, self.coord[0], self.coord[1]]
                self.sigOdmrPlotsUpdated.emit(
                    x_data, y_data, self.odmr_plot_xy)
                y_data = y_data[0]
        # This enables us to reset to actual odmr_plot_y values after looking
        # at the pixel spectrum
        if not pixel_fit:
            self.sigOdmrPlotsUpdated.emit(
                self.odmr_plot_x, self.odmr_plot_y, self.odmr_plot_xy)

        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,
            save_stack=False):
        """ 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['Arb. counts'] = self.odmr_plot_y[nch]
            data2['Arb. counts'] = 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
            # Exposure time is added as well to the save parameters.
            parameters['Exposure time (ms)'] = self.exp_time
            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)
            # The files is saved as a compressed .npz file which can be looaed by np.load('.npz')['sweep_images']
            # Provides best possible compression for array storage. Saved with almost the same timestamp
            # as used in save_logic
            if save_stack:
                loc = filepath + '/' + \
                    timestamp.strftime("%Y%m%d-%H%M-%S") + '_' + filelabel + '_sweep'
                np.savez_compressed(
                loc,
                sweep_images=(self.sweep_images /
                                self.elapsed_sweeps))
            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('Arb. (' + counts_prefix + 'units)')
        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('Arb. (' + cbar_prefix + 'units)')

        # 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
Exemplo n.º 5
0
class OptimizerLogic(GenericLogic):
    """This is the Logic class for optimizing scanner position on bright features.
    """

    # 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.confocalscanner1()
        self._fit_logic = self.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.module_state() == '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'):  # TODO: change the logic here****
        """ 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 + self.opt_channel].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.
        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 self.x_range[0] <= result_2D_gaus.best_values[
                        'center_x'] <= self.x_range[1]:
                    if self.y_range[0] <= 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 = {
                '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 self.z_range[0] <= 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.module_state.lock()
        clock_status = self._scanning_device.set_up_scanner_clock(
            clock_frequency=self._clock_frequency)
        if clock_status < 0:
            self.module_state.unlock()
            return -1

        scanner_status = self._scanning_device.set_up_scanner()
        if scanner_status < 0:
            self._scanning_device.close_scanner_clock()
            self.module_state.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.module_state.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)
Exemplo n.º 6
0
class QDPlotterGui(GUIBase):
    """ GUI  for displaying up to 3 custom plots.
    The plots are held in tabified DockWidgets and can either be manipulated in the logic
    or by corresponding parameter DockWidgets.

    Example config for copy-paste:

    qdplotter:
        module.Class: 'qdplotter.qdplotter_gui.QDPlotterGui'
        pen_color_list: [[100, 100, 100], 'c', 'm', 'g']
        connect:
            qdplot_logic: 'qdplotlogic'
    """

    sigPlotParametersChanged = QtCore.Signal(int, dict)
    sigAutoRangeClicked = QtCore.Signal(int, bool, bool)
    sigDoFit = QtCore.Signal(str, int)
    sigRemovePlotClicked = QtCore.Signal(int)

    # declare connectors
    qdplot_logic = Connector(interface='QDPlotLogic')
    # declare config options
    _pen_color_list = ConfigOption(name='pen_color_list',
                                   default=['b', 'y', 'm', 'g'])
    # declare status variables
    widget_alignment = StatusVar(name='widget_alignment', default='tabbed')

    _allowed_colors = {'b', 'g', 'r', 'c', 'm', 'y', 'k', 'w'}

    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self._plot_logic = None
        self._mw = None
        self._fsd = None

        self._plot_dockwidgets = list()
        self._pen_colors = list()
        self._plot_curves = list()
        self._fit_curves = list()
        self._pg_signal_proxys = list()

    def on_activate(self):
        """ Definition and initialisation of the GUI.
        """
        self._plot_logic = self.qdplot_logic()

        if not isinstance(self._pen_color_list,
                          (list, tuple)) or len(self._pen_color_list) < 1:
            self.log.warning(
                'The ConfigOption pen_color_list needs to be a list of strings but was "{0}".'
                ' Will use the following pen colors as default: {1}.'
                ''.format(self._pen_color_list, ['b', 'y', 'm', 'g']))
            self._pen_color_list = ['b', 'y', 'm', 'g']
        else:
            self._pen_color_list = list(self._pen_color_list)

        for index, color in enumerate(self._pen_color_list):
            if (isinstance(color, (list, tuple)) and len(color) == 3) or \
                    (isinstance(color, str) and color in self._allowed_colors):
                pass
            else:
                self.log.warning(
                    'The color was "{0}" but needs to be from this list: {1} '
                    'or a 3 element tuple with values from 0 to 255 for RGB.'
                    ' Setting color to "b".'.format(color,
                                                    self._allowed_colors))
                self._pen_color_list[index] = 'b'

        # Use the inherited class 'QDPlotMainWindow' to create the GUI window
        self._mw = QDPlotMainWindow()

        # Fit settings dialogs
        self._fsd = FitSettingsDialog(self._plot_logic.fit_container)
        self._fsd.applySettings()
        self._mw.fit_settings_Action.triggered.connect(self._fsd.show)

        # Connect the main window restore view actions
        self._mw.restore_tabbed_view_Action.triggered.connect(
            self.restore_tabbed_view)
        self._mw.restore_side_by_side_view_Action.triggered.connect(
            self.restore_side_by_side_view)
        self._mw.restore_arc_view_Action.triggered.connect(
            self.restore_arc_view)
        self._mw.save_all_Action.triggered.connect(self.save_all_clicked)

        # Initialize dock widgets
        self._plot_dockwidgets = list()
        self._pen_colors = list()
        self._plot_curves = list()
        self._fit_curves = list()
        self._pg_signal_proxys = list()
        self.update_number_of_plots(self._plot_logic.number_of_plots)
        # Update all plot parameters and data from logic
        for index, _ in enumerate(self._plot_dockwidgets):
            self.update_data(index)
            self.update_fit_data(index)
            self.update_plot_parameters(index)
        self.restore_view()

        # Connect signal to logic
        self.sigPlotParametersChanged.connect(
            self._plot_logic.update_plot_parameters,
            QtCore.Qt.QueuedConnection)
        self.sigAutoRangeClicked.connect(self._plot_logic.update_auto_range,
                                         QtCore.Qt.QueuedConnection)
        self.sigDoFit.connect(self._plot_logic.do_fit,
                              QtCore.Qt.QueuedConnection)
        self.sigRemovePlotClicked.connect(self._plot_logic.remove_plot,
                                          QtCore.Qt.QueuedConnection)
        self._mw.new_plot_Action.triggered.connect(self._plot_logic.add_plot,
                                                   QtCore.Qt.QueuedConnection)
        # Connect signals from logic
        self._plot_logic.sigPlotDataUpdated.connect(self.update_data,
                                                    QtCore.Qt.QueuedConnection)
        self._plot_logic.sigPlotParamsUpdated.connect(
            self.update_plot_parameters, QtCore.Qt.QueuedConnection)
        self._plot_logic.sigPlotNumberChanged.connect(
            self.update_number_of_plots, QtCore.Qt.QueuedConnection)
        self._plot_logic.sigFitUpdated.connect(self.update_fit_data,
                                               QtCore.Qt.QueuedConnection)

        self.show()

    def show(self):
        """ Make window visible and put it above all other windows. """
        self._mw.show()
        self._mw.activateWindow()
        self._mw.raise_()

    def on_deactivate(self):
        """ Deactivate the module """
        # disconnect fit
        self._mw.fit_settings_Action.triggered.disconnect()

        self._mw.restore_tabbed_view_Action.triggered.disconnect()
        self._mw.restore_side_by_side_view_Action.triggered.disconnect()
        self._mw.restore_arc_view_Action.triggered.disconnect()
        self._mw.save_all_Action.triggered.disconnect()

        # Disconnect signal to logic
        self.sigPlotParametersChanged.disconnect()
        self.sigAutoRangeClicked.disconnect()
        self.sigDoFit.disconnect()
        self.sigRemovePlotClicked.disconnect()
        self._mw.new_plot_Action.triggered.disconnect()
        # Disconnect signals from logic
        self._plot_logic.sigPlotDataUpdated.disconnect(self.update_data)
        self._plot_logic.sigPlotParamsUpdated.disconnect(
            self.update_plot_parameters)
        self._plot_logic.sigPlotNumberChanged.disconnect(
            self.update_number_of_plots)
        self._plot_logic.sigFitUpdated.disconnect(self.update_fit_data)

        # disconnect GUI elements
        self.update_number_of_plots(0)

        self._fsd.sigFitsUpdated.disconnect()
        self._mw.close()

    @QtCore.Slot(int)
    def update_number_of_plots(self, count):
        """ Adjust number of QDockWidgets to current number of plots. Does NO initialization of the
        contents.

        @param int count: Number of plots to display.
        """
        # Remove dock widgets if plot count decreased
        while count < len(self._plot_dockwidgets):
            index = len(self._plot_dockwidgets) - 1
            self._disconnect_plot_signals(index)
            self._plot_dockwidgets[-1].setParent(None)
            del self._plot_curves[-1]
            del self._fit_curves[-1]
            del self._pen_colors[-1]
            del self._plot_dockwidgets[-1]
            del self._pg_signal_proxys[-1]
        # Add dock widgets if plot count increased
        while count > len(self._plot_dockwidgets):
            index = len(self._plot_dockwidgets)
            dockwidget = PlotDockWidget('Plot {0:d}'.format(index + 1),
                                        self._mw)
            dockwidget.widget().fit_comboBox.setFitFunctions(
                self._fsd.currentFits)
            dockwidget.widget().show_fit_checkBox.setChecked(False)
            dockwidget.widget().show_controls_checkBox.setChecked(False)
            dockwidget.widget().fit_groupBox.setVisible(False)
            dockwidget.widget().controls_groupBox.setVisible(False)
            self._plot_dockwidgets.append(dockwidget)
            self._pen_colors.append(cycle(self._pen_color_list))
            self._plot_curves.append(list())
            self._fit_curves.append(list())
            self._pg_signal_proxys.append([None, None])
            self._connect_plot_signals(index)
            self.restore_view()

    def _connect_plot_signals(self, index):
        dockwidget = self._plot_dockwidgets[index].widget()
        self._fsd.sigFitsUpdated.connect(
            dockwidget.fit_comboBox.setFitFunctions)
        dockwidget.fit_pushButton.clicked.connect(
            functools.partial(self.fit_clicked, index))

        x_lim_callback = functools.partial(self.x_limits_changed, index)
        dockwidget.x_lower_limit_DoubleSpinBox.valueChanged.connect(
            x_lim_callback)
        dockwidget.x_upper_limit_DoubleSpinBox.valueChanged.connect(
            x_lim_callback)
        y_lim_callback = functools.partial(self.y_limits_changed, index)
        dockwidget.y_lower_limit_DoubleSpinBox.valueChanged.connect(
            y_lim_callback)
        dockwidget.y_upper_limit_DoubleSpinBox.valueChanged.connect(
            y_lim_callback)

        dockwidget.x_label_lineEdit.editingFinished.connect(
            functools.partial(self.x_label_changed, index))
        dockwidget.x_unit_lineEdit.editingFinished.connect(
            functools.partial(self.x_unit_changed, index))
        dockwidget.y_label_lineEdit.editingFinished.connect(
            functools.partial(self.y_label_changed, index))
        dockwidget.y_unit_lineEdit.editingFinished.connect(
            functools.partial(self.y_unit_changed, index))

        dockwidget.x_auto_PushButton.clicked.connect(
            functools.partial(self.x_auto_range_clicked, index))
        dockwidget.y_auto_PushButton.clicked.connect(
            functools.partial(self.y_auto_range_clicked, index))
        dockwidget.save_pushButton.clicked.connect(
            functools.partial(self.save_clicked, index))
        dockwidget.remove_pushButton.clicked.connect(
            functools.partial(self.remove_clicked, index))
        self._pg_signal_proxys[index][0] = SignalProxy(
            dockwidget.plot_PlotWidget.sigXRangeChanged,
            delay=0.2,
            slot=lambda args: self._pyqtgraph_x_limits_changed(index, args[1]))
        self._pg_signal_proxys[index][1] = SignalProxy(
            dockwidget.plot_PlotWidget.sigYRangeChanged,
            delay=0.2,
            slot=lambda args: self._pyqtgraph_y_limits_changed(index, args[1]))

    def _disconnect_plot_signals(self, index):
        dockwidget = self._plot_dockwidgets[index].widget()
        self._fsd.sigFitsUpdated.disconnect(
            dockwidget.fit_comboBox.setFitFunctions)
        dockwidget.fit_pushButton.clicked.disconnect()

        dockwidget.x_lower_limit_DoubleSpinBox.valueChanged.disconnect()
        dockwidget.x_upper_limit_DoubleSpinBox.valueChanged.disconnect()
        dockwidget.y_lower_limit_DoubleSpinBox.valueChanged.disconnect()
        dockwidget.y_upper_limit_DoubleSpinBox.valueChanged.disconnect()

        dockwidget.x_label_lineEdit.editingFinished.disconnect()
        dockwidget.x_unit_lineEdit.editingFinished.disconnect()
        dockwidget.y_label_lineEdit.editingFinished.disconnect()
        dockwidget.y_unit_lineEdit.editingFinished.disconnect()

        dockwidget.x_auto_PushButton.clicked.disconnect()
        dockwidget.y_auto_PushButton.clicked.disconnect()
        dockwidget.save_pushButton.clicked.disconnect()
        dockwidget.remove_pushButton.clicked.disconnect()
        for sig_proxy in self._pg_signal_proxys[index]:
            sig_proxy.sigDelayed.disconnect()
            sig_proxy.disconnect()

    @property
    def pen_color_list(self):
        return self._pen_color_list.copy()

    @pen_color_list.setter
    def pen_color_list(self, value):
        if not isinstance(value, (list, tuple)) or len(value) < 1:
            self.log.warning(
                'The parameter pen_color_list needs to be a list of strings but was "{0}".'
                ' Will use the following old pen colors: {1}.'
                ''.format(value, self._pen_color_list))
            return
        for index, color in enumerate(self._pen_color_list):
            if (isinstance(color, (list, tuple)) and len(color) == 3) or \
                    (isinstance(color, str) and color in self._allowed_colors):
                pass
            else:
                self.log.warning(
                    'The color was "{0}" but needs to be from this list: {1} '
                    'or a 3 element tuple with values from 0 to 255 for RGB.'
                    ''.format(color, self._allowed_colors))
                return
        else:
            self._pen_color_list = list(value)

    def restore_side_by_side_view(self):
        """ Restore the arrangement of DockWidgets to the default """
        self.restore_view(alignment='side_by_side')

    def restore_arc_view(self):
        """ Restore the arrangement of DockWidgets to the default """
        self.restore_view(alignment='arc')

    def restore_tabbed_view(self):
        """ Restore the arrangement of DockWidgets to the default """
        self.restore_view(alignment='tabbed')

    @QtCore.Slot()
    def restore_view(self, alignment=None):
        """ Restore the arrangement of DockWidgets to the default """

        if alignment is None:
            alignment = self.widget_alignment
        if alignment not in ('side_by_side', 'arc', 'tabbed'):
            alignment = 'tabbed'
        self.widget_alignment = alignment

        self._mw.setDockNestingEnabled(True)
        self._mw.centralwidget.setVisible(False)

        for i, dockwidget in enumerate(self._plot_dockwidgets):
            dockwidget.show()
            dockwidget.setFloating(False)
            dockwidget.widget().show_fit_checkBox.setChecked(False)
            dockwidget.widget().show_controls_checkBox.setChecked(False)
            if alignment == 'tabbed':
                self._mw.addDockWidget(QtCore.Qt.TopDockWidgetArea, dockwidget)
                if i > 0:
                    self._mw.tabifyDockWidget(self._plot_dockwidgets[0],
                                              dockwidget)
            elif alignment == 'arc':
                mod = i % 3
                if mod == 0:
                    self._mw.addDockWidget(QtCore.Qt.TopDockWidgetArea,
                                           dockwidget)
                    if i > 2:
                        self._mw.tabifyDockWidget(self._plot_dockwidgets[0],
                                                  dockwidget)
                elif mod == 1:
                    self._mw.addDockWidget(QtCore.Qt.BottomDockWidgetArea,
                                           dockwidget)
                    if i > 2:
                        self._mw.tabifyDockWidget(self._plot_dockwidgets[1],
                                                  dockwidget)
                elif mod == 2:
                    self._mw.addDockWidget(QtCore.Qt.BottomDockWidgetArea,
                                           dockwidget)
                    if i > 2:
                        self._mw.tabifyDockWidget(self._plot_dockwidgets[2],
                                                  dockwidget)
            elif alignment == 'side_by_side':
                self._mw.addDockWidget(QtCore.Qt.TopDockWidgetArea, dockwidget)
        if alignment == 'arc':
            if len(self._plot_dockwidgets) > 2:
                self._mw.resizeDocks(
                    [self._plot_dockwidgets[1], self._plot_dockwidgets[2]],
                    [1, 1], QtCore.Qt.Horizontal)
        elif alignment == 'side_by_side':
            self._mw.resizeDocks(self._plot_dockwidgets,
                                 [1] * len(self._plot_dockwidgets),
                                 QtCore.Qt.Horizontal)

    @QtCore.Slot(int, list, list, bool)
    def update_data(self,
                    plot_index,
                    x_data=None,
                    y_data=None,
                    clear_old=None):
        """ Function creates empty plots, grabs the data and sends it to them. """
        if not (0 <= plot_index < len(self._plot_dockwidgets)):
            self.log.warning(
                'Tried to update plot with invalid index {0:d}'.format(
                    plot_index))
            return

        if x_data is None:
            x_data = self._plot_logic.get_x_data(plot_index)
        if y_data is None:
            y_data = self._plot_logic.get_y_data(plot_index)
        if clear_old is None:
            clear_old = self._plot_logic.clear_old_data(plot_index)

        dockwidget = self._plot_dockwidgets[plot_index].widget()
        if clear_old:
            dockwidget.plot_PlotWidget.clear()
            self._pen_colors[plot_index] = cycle(self._pen_color_list)

        self._plot_curves[plot_index] = list()
        self._fit_curves[plot_index] = list()

        for line, xd in enumerate(x_data):
            yd = y_data[line]
            pen_color = next(self._pen_colors[plot_index])
            self._plot_curves[plot_index].append(
                dockwidget.plot_PlotWidget.plot(
                    pen=mkColor(pen_color),
                    symbol='d',
                    symbolSize=6,
                    symbolBrush=mkColor(pen_color)))
            self._plot_curves[plot_index][-1].setData(x=xd, y=yd)
            self._fit_curves[plot_index].append(
                dockwidget.plot_PlotWidget.plot())
            self._fit_curves[plot_index][-1].setPen('r')

    @QtCore.Slot(int)
    @QtCore.Slot(int, dict)
    def update_plot_parameters(self, plot_index, params=None):
        """ Function updated limits, labels and units in the plot and parameter widgets. """
        if not (0 <= plot_index < len(self._plot_dockwidgets)):
            self.log.warning(
                'Tried to update plot with invalid index {0:d}'.format(
                    plot_index))
            return

        dockwidget = self._plot_dockwidgets[plot_index].widget()
        if params is None:
            params = dict()
            params['x_label'] = self._plot_logic.get_x_label(plot_index)
            params['y_label'] = self._plot_logic.get_y_label(plot_index)
            params['x_unit'] = self._plot_logic.get_x_unit(plot_index)
            params['y_unit'] = self._plot_logic.get_y_unit(plot_index)
            params['x_limits'] = self._plot_logic.get_x_limits(plot_index)
            params['y_limits'] = self._plot_logic.get_y_limits(plot_index)

        if 'x_label' in params or 'x_unit' in params:
            label = params.get('x_label', None)
            unit = params.get('x_unit', None)
            if label is None:
                label = self._plot_logic.get_x_label(plot_index)
            if unit is None:
                unit = self._plot_logic.get_x_unit(plot_index)
            dockwidget.plot_PlotWidget.setLabel('bottom', label, units=unit)
            dockwidget.x_label_lineEdit.blockSignals(True)
            dockwidget.x_unit_lineEdit.blockSignals(True)
            dockwidget.x_label_lineEdit.setText(label)
            dockwidget.x_unit_lineEdit.setText(unit)
            dockwidget.x_label_lineEdit.blockSignals(False)
            dockwidget.x_unit_lineEdit.blockSignals(False)
        if 'y_label' in params or 'y_unit' in params:
            label = params.get('y_label', None)
            unit = params.get('y_unit', None)
            if label is None:
                label = self._plot_logic.get_y_label(plot_index)
            if unit is None:
                unit = self._plot_logic.get_y_unit(plot_index)
            dockwidget.plot_PlotWidget.setLabel('left', label, units=unit)
            dockwidget.y_label_lineEdit.blockSignals(True)
            dockwidget.y_unit_lineEdit.blockSignals(True)
            dockwidget.y_label_lineEdit.setText(label)
            dockwidget.y_unit_lineEdit.setText(unit)
            dockwidget.y_label_lineEdit.blockSignals(False)
            dockwidget.y_unit_lineEdit.blockSignals(False)
        if 'x_limits' in params:
            limits = params['x_limits']
            self._pg_signal_proxys[plot_index][0].block = True
            dockwidget.plot_PlotWidget.setXRange(*limits, padding=0)
            self._pg_signal_proxys[plot_index][0].block = False
            dockwidget.x_lower_limit_DoubleSpinBox.blockSignals(True)
            dockwidget.x_upper_limit_DoubleSpinBox.blockSignals(True)
            dockwidget.x_lower_limit_DoubleSpinBox.setValue(limits[0])
            dockwidget.x_upper_limit_DoubleSpinBox.setValue(limits[1])
            dockwidget.x_lower_limit_DoubleSpinBox.blockSignals(False)
            dockwidget.x_upper_limit_DoubleSpinBox.blockSignals(False)
        if 'y_limits' in params:
            limits = params['y_limits']
            self._pg_signal_proxys[plot_index][1].block = True
            dockwidget.plot_PlotWidget.setYRange(*limits, padding=0)
            self._pg_signal_proxys[plot_index][1].block = False
            dockwidget.y_lower_limit_DoubleSpinBox.blockSignals(True)
            dockwidget.y_upper_limit_DoubleSpinBox.blockSignals(True)
            dockwidget.y_lower_limit_DoubleSpinBox.setValue(limits[0])
            dockwidget.y_upper_limit_DoubleSpinBox.setValue(limits[1])
            dockwidget.y_lower_limit_DoubleSpinBox.blockSignals(False)
            dockwidget.y_upper_limit_DoubleSpinBox.blockSignals(False)

    def save_clicked(self, plot_index):
        """ Handling the save button to save the data into a file. """
        self._flush_pg_proxy(plot_index)
        self._plot_logic.save_data(plot_index=plot_index)

    def save_all_clicked(self):
        """ Handling the save button to save the data into a file. """
        for plot_index, _ in enumerate(self._plot_dockwidgets):
            self.save_clicked(plot_index)

    def remove_clicked(self, plot_index):
        self._flush_pg_proxy(plot_index)
        self.sigRemovePlotClicked.emit(plot_index)

    def x_auto_range_clicked(self, plot_index):
        """ Set the parameter_1_x_limits to the min/max of the data values """
        self.sigAutoRangeClicked.emit(plot_index, True, False)

    def y_auto_range_clicked(self, plot_index):
        """ Set the parameter_1_y_limits to the min/max of the data values """
        self.sigAutoRangeClicked.emit(plot_index, False, True)

    def x_limits_changed(self, plot_index):
        """ Handling the change of the parameter_1_x_limits. """
        dockwidget = self._plot_dockwidgets[plot_index].widget()
        self.sigPlotParametersChanged.emit(
            plot_index, {
                'x_limits': [
                    dockwidget.x_lower_limit_DoubleSpinBox.value(),
                    dockwidget.x_upper_limit_DoubleSpinBox.value()
                ]
            })

    def y_limits_changed(self, plot_index):
        """ Handling the change of the parameter_1_y_limits. """
        dockwidget = self._plot_dockwidgets[plot_index].widget()
        self.sigPlotParametersChanged.emit(
            plot_index, {
                'y_limits': [
                    dockwidget.y_lower_limit_DoubleSpinBox.value(),
                    dockwidget.y_upper_limit_DoubleSpinBox.value()
                ]
            })

    def x_label_changed(self, plot_index):
        """ Set the x-label """
        dockwidget = self._plot_dockwidgets[plot_index].widget()
        self.sigPlotParametersChanged.emit(
            plot_index, {'x_label': dockwidget.x_label_lineEdit.text()})

    def y_label_changed(self, plot_index):
        """ Set the y-label and the uni of plot 1 """
        dockwidget = self._plot_dockwidgets[plot_index].widget()
        self.sigPlotParametersChanged.emit(
            plot_index, {'y_label': dockwidget.y_label_lineEdit.text()})

    def x_unit_changed(self, plot_index):
        """ Set the x-label """
        dockwidget = self._plot_dockwidgets[plot_index].widget()
        self.sigPlotParametersChanged.emit(
            plot_index, {'x_unit': dockwidget.x_unit_lineEdit.text()})

    def y_unit_changed(self, plot_index):
        """ Set the y-label and the uni of plot 1 """
        dockwidget = self._plot_dockwidgets[plot_index].widget()
        self.sigPlotParametersChanged.emit(
            plot_index, {'y_unit': dockwidget.y_unit_lineEdit.text()})

    def fit_clicked(self, plot_index=0):
        """ Triggers the fit to be done. Attention, this runs in the GUI thread. """
        current_fit_method = self._plot_dockwidgets[plot_index].widget(
        ).fit_comboBox.getCurrentFit()[0]
        self.sigDoFit.emit(current_fit_method, plot_index)

    @QtCore.Slot(int, np.ndarray, str, str)
    def update_fit_data(self,
                        plot_index,
                        fit_data=None,
                        formatted_fitresult=None,
                        fit_method=None):
        """ Function that handles the fit results received from the logic via a signal.

        @param int plot_index: index of the plot the fit was performed for in the range for 0 to 2
        @param 3-dimensional np.ndarray fit_data: the fit data in a 2-d array for each data set
        @param str formatted_fitresult: string containing the parameters already formatted
        @param str fit_method: the fit_method used
        """
        dockwidget = self._plot_dockwidgets[plot_index].widget()

        if fit_data is None or formatted_fitresult is None or fit_method is None:
            fit_data, formatted_fitresult, fit_method = self._plot_logic.get_fit_data(
                plot_index)

        if not fit_method:
            fit_method = 'No Fit'

        dockwidget.fit_comboBox.blockSignals(True)

        dockwidget.show_fit_checkBox.setChecked(True)
        dockwidget.fit_textBrowser.clear()
        dockwidget.fit_comboBox.setCurrentFit(fit_method)
        if fit_method == 'No Fit':
            for index, curve in enumerate(self._fit_curves[plot_index]):
                if curve in dockwidget.plot_PlotWidget.items():
                    dockwidget.plot_PlotWidget.removeItem(curve)
        else:
            dockwidget.fit_textBrowser.setPlainText(formatted_fitresult)
            for index, curve in enumerate(self._fit_curves[plot_index]):
                if curve not in dockwidget.plot_PlotWidget.items():
                    dockwidget.plot_PlotWidget.addItem(curve)
                curve.setData(x=fit_data[index][0], y=fit_data[index][1])

        dockwidget.fit_comboBox.blockSignals(False)

    def _pyqtgraph_x_limits_changed(self, plot_index, limits):
        plot_item = self._plot_dockwidgets[plot_index].widget(
        ).plot_PlotWidget.getPlotItem()
        if plot_item.ctrl.logXCheck.isChecked(
        ) or plot_item.ctrl.fftCheck.isChecked():
            return
        self.sigPlotParametersChanged.emit(plot_index, {'x_limits': limits})

    def _pyqtgraph_y_limits_changed(self, plot_index, limits):
        plot_item = self._plot_dockwidgets[plot_index].widget(
        ).plot_PlotWidget.getPlotItem()
        if plot_item.ctrl.logYCheck.isChecked(
        ) or plot_item.ctrl.fftCheck.isChecked():
            return
        self.sigPlotParametersChanged.emit(plot_index, {'y_limits': limits})

    def _flush_pg_proxy(self, plot_index):
        x_proxy, y_proxy = self._pg_signal_proxys[plot_index]
        x_proxy.flush()
        y_proxy.flush()
Exemplo n.º 7
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)

    # 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_figure=True):
        """ 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
        @param bool save_figure: select whether png and pdf should be saved

        @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')

            if save_figure:
                fig = self.draw_figure(data=np.array(self._data_to_save))
            else:
                fig = None
            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()
        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
Exemplo n.º 8
0
class RoiLogic(GenericLogic):
    """
    This is the Logic class for selecting regions of interest.
    """

    # declare connectors
    stage = Connector(interface='MotorInterface')

    # status vars
    _roi_list = StatusVar(
        default=dict())  # Notice constructor and representer further below
    _active_roi = StatusVar(default=None)
    _roi_width = StatusVar(
        default=50
    )  # check if unit is correct when used with real translation stage. Value corresponds to FOV ??

    # Signals
    sigRoiUpdated = QtCore.Signal(
        str, str, np.ndarray)  # old_name, new_name, current_position
    sigActiveRoiUpdated = QtCore.Signal(str)
    sigRoiListUpdated = QtCore.Signal(
        dict)  # Dict containing ROI parameters to update
    sigWidthUpdated = QtCore.Signal(float)

    sigStageMoved = QtCore.Signal(np.ndarray)  # current_position
    sigUpdateStagePosition = QtCore.Signal(tuple)
    sigTrackingModeStopped = QtCore.Signal(
    )  # important to emit this when tracking mode is programmatically stopped to reestablish correct GUI state
    sigDisableTracking = QtCore.Signal()
    sigEnableTracking = QtCore.Signal()
    sigDisableRoiActions = QtCore.Signal()
    sigEnableRoiActions = QtCore.Signal()

    # variables from mosaic settings dialog and default values
    _mosaic_x_start = 0
    _mosaic_y_start = 0
    _mosaic_roi_width = 0
    _mosaic_number_x = 0  # width
    _mosaic_number_y = 0  # height

    tracking = False

    timer = None
    tracking_interval = 1

    def __init__(self, config, **kwargs):
        super().__init__(config=config, **kwargs)

        # not needed in this version but remember to use it when starting to handle threads
        # threading
        # self._threadlock = Mutex()

        self.threadpool = QtCore.QThreadPool()

    def on_activate(self):
        """ Initialisation performed during activation of the module.
        """

        # Initialise the ROI camera image (xy image) if not present
        # if self._roi_list.cam_image is None:
        #     self.set_cam_image(False)

        self.sigRoiListUpdated.emit({
            'name':
            self.roi_list_name,
            'rois':
            self.roi_positions,
            'cam_image':
            self.roi_list_cam_image,
            'cam_image_extent':
            self.roi_list_cam_image_extent
        })
        self.sigActiveRoiUpdated.emit(
            '' if self.active_roi is None else self.active_roi)

    def on_deactivate(self):
        pass

    @property
    def active_roi(self):
        return self._active_roi

    @active_roi.setter
    def active_roi(self, name):
        self.set_active_roi(name)

    @property
    def roi_names(self):
        return self._roi_list.roi_names

    @property
    def roi_positions(self):
        return self._roi_list.roi_positions

    @property
    def roi_list_name(self):
        return self._roi_list.name

    @roi_list_name.setter
    def roi_list_name(self, name):
        self.rename_roi_list(new_name=name)

    @property
    def roi_list_origin(self):
        return self._roi_list.origin

    @property
    def roi_list_creation_time(self):
        return self._roi_list.creation_time

    @property
    def roi_list_creation_time_as_str(self):
        return self._roi_list.creation_time_as_str

    @property
    def roi_list_cam_image(self):
        return self._roi_list.cam_image

    @property
    def roi_list_cam_image_extent(self):
        return self._roi_list.cam_image_extent

    @property
    def roi_width(self):
        return float(self._roi_width)

    @roi_width.setter
    def roi_width(self, new_width):
        self.set_roi_width(new_width)

    # formerly returned as list instead of tuple. in case error appears .. it worked correctly with a list
    @property
    def stage_position(self):
        pos = self.stage().get_pos(
        )  # this returns a dictionary of the format {'x': pos_x, 'y': pos_y}
        if len(pos) == 2 and 'z' not in pos.keys(
        ):  # case for the 2 axes stage
            pos['z'] = 0  # add an artificial z component so that add_roi method can be called which expects a tuple (x, y, z)
        return tuple(
            pos.values())[:3]  # get only the dictionary values as a tuple.
        # [:3] as safety to get only the x y axis and (eventually empty) z value, in case more axis are configured (such as for the motor_dummy)

    # even if called with a name not None, a generic name is set. The specified one is not taken into account. This is handled in the add_roi method of RegionOfInterestList class
    @QtCore.Slot()
    @QtCore.Slot(np.ndarray)
    def add_roi(self, position=None, name=None, emit_change=True):
        """
        Creates a new ROI and adds it to the current ROI list.
        ROI can be optionally initialized with position.

        @param str name: Name for the ROI (must be unique within ROI list).
                         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 list origin. None (default) causes the current
                                   stage position to be used.
        @param bool emit_change: Flag indicating if the changed ROI set should be signaled.
        """
        # Get current stage position from motor interface if no position is provided.
        if position is None:
            position = self.stage_position

        current_roi_set = set(self.roi_names)

        # Add ROI to current ROI list
        self._roi_list.add_roi(position=position, name=name)

        # Get newly added ROI name from comparing ROI names before and after addition of new ROI
        roi_name = set(self.roi_names).difference(current_roi_set).pop()

        # Notify about a changed set of ROIs if necessary
        if emit_change:
            self.sigRoiUpdated.emit('', roi_name,
                                    self.get_roi_position(roi_name))

        # Set newly created ROI as active roi
        self.set_active_roi(roi_name)
        return None

    # delete_roi can be called with a name present in the list (which will only be the generic names)
    @QtCore.Slot()
    def delete_roi(self, name=None):
        """
        Deletes the given roi from the roi list.

        @param str name: Name of the roi to delete. If None (default) delete active roi.
        """
        if len(self.roi_names) == 0:
            self.log.warning('Can not delete ROI. No ROI present in ROI list.')
            return None
        if name is None:
            if self.active_roi is None:
                self.log.error('No ROI name to delete and no active ROI set.')
                return None
            else:
                name = self.active_roi

        self._roi_list.delete_roi(
            name)  # see method defined in RegionOfInterestList class

        if self.active_roi == name:
            if len(self.roi_names) > 0:
                self.set_active_roi(self.roi_names[0])
            else:
                self.set_active_roi(None)

        # Notify about a changed set of ROIs if necessary
        self.sigRoiUpdated.emit(name, '', np.zeros(3))
        return None

    @QtCore.Slot()
    def delete_all_roi(self):
        self.active_roi = None
        for name in self.roi_names:
            self._roi_list.delete_roi(name)
            self.sigRoiUpdated.emit(name, '', np.zeros(3))
        return None

    @QtCore.Slot(str)
    def set_active_roi(self, name=None):
        """
        Set the name of the currently active ROI
        @param name:
        """
        if not isinstance(name, str) and name is not None:
            self.log.error('ROI name must be of type str or None.')
        elif name is None or name == '':
            self._active_roi = None
        elif name in self.roi_names:
            self._active_roi = str(name)
        else:
            self.log.error(
                'No ROI with name "{0}" found in ROI list.'.format(name))

        self.sigActiveRoiUpdated.emit(
            '' if self.active_roi is None else self.active_roi)
        return None

    def get_roi_position(self, name=None):
        """
        Returns the ROI position of the specified ROI or the active ROI if none is given.

        @param str name: Name of the ROI to return the position for.
                             If None (default) the active ROI position is returned.
        @return float[3]: Coordinates of the desired ROI (x,y,z)
        """
        if name is None:
            name = self.active_roi
        return self._roi_list.get_roi_position(name)

    @QtCore.Slot(str)
    def rename_roi_list(self, new_name):
        if not isinstance(new_name, str) or new_name == '':
            self.log.error('ROI list name to set must be str of length > 0.')
            return None
        self._roi_list.name = new_name
        self.sigRoiListUpdated.emit({'name': self.roi_list_name})
        return None

    @QtCore.Slot()
    def go_to_roi(self, name=None):
        """
        Move translation stage to the given roi.

        @param str name: the name of the ROI, default is the active roi
        """
        if name is None:
            name = self.active_roi
        if not isinstance(name, str):
            self.log.error('ROI name to move to must be of type str.')
            return None
        self._move_stage(self.get_roi_position(name))
        return None

    def go_to_roi_xy(self, name=None):
        """
        Move translation stage to the xy position of the given roi.

        @param str name: the name of the ROI, default is the active roi
        """
        if name is None:
            name = self.active_roi
        if not isinstance(name, str):
            self.log.error('ROI name to move to must be of type str.')
            return None
        x_roi, y_roi, z_roi = self.get_roi_position(name)
        x_stage, y_stage, z_stage = self.stage_position
        target_pos = np.array(
            (x_roi, y_roi, z_stage
             ))  # conversion from tuple to np.ndarray for call of _move_stage
        self._move_stage(target_pos)
        return None

    def _move_stage(self, position):
        """ 
        Move the translation stage to position.
        
        @param float position: np.ndarray[3]
        """
        # this functions accepts a tuple (x, y, z) as argument because it will be called with the roi position as argument.
        # Hence, the input argument has to be converted into a dictionary of format {'x': x, 'y': y} to be passed to the translation stage function.
        if len(position) != 3:
            self.log.error(
                'Stage position to set must be iterable of length 3.')
            return None
        axis_label = ('x', 'y', 'z')
        pos_dict = dict([*zip(axis_label, position)])
        self.stage().move_abs(pos_dict)
        self.sigStageMoved.emit(position)
        return None

    #    @QtCore.Slot()
    #    def set_cam_image(self, emit_change=True):
    #        """ Get the current xy scan data and set as scan_image of ROI. """
    #        self._roi_list.set_scan_image() # add the camera logic as connector to get the image ?? or do not show image on ROI map ?
    #
    #        if emit_change:
    #            self.sigRoiListUpdated.emit({'scan_image': self.roi_list_scan_image,
    #                                     'scan_image_extent': self.roi_scan_image_extent})
    #        return None

    @QtCore.Slot()
    def reset_roi_list(self):
        self._roi_list = RegionOfInterestList(
        )  # create an instance of the RegionOfInterestList class
        # self.set_cam_image()
        self.sigRoiListUpdated.emit({
            'name':
            self.roi_list_name,
            'rois':
            self.roi_positions,
            'cam_image':
            self.roi_list_cam_image,
            'cam_image_extent':
            self.roi_list_cam_image_extent
        })
        self.set_active_roi(None)
        return None

    # @QtCore.Slot()
    # def set_cam_image(self, emit_change=True):
    #     """ test if this helps to solve the distance measuremetn problem when roi_image is not initialized
    #     """
    #     self._roi_list.set_cam_image()
    #
    #     if emit_change:
    #         self.sigRoiListUpdated.emit({'scan_image': self.roi_list_cam_image,
    #                                  'scan_image_extent': self.roi_cam_image_extent})
    #     return None

    @QtCore.Slot(float)
    def set_roi_width(self, width):
        self._roi_width = float(width)
        self.sigWidthUpdated.emit(width)
        return None

    def save_roi_list(self, path, filename):
        """
        Save the current roi_list to a file. A dictionary format is used.
        """
        # convert the roi_list to a dictionary
        roi_list_dict = self.roi_list_to_dict(self._roi_list)

        if not os.path.exists(path):
            try:
                os.makedirs(
                    path)  # recursive creation of all directories on the path
            except Exception as e:
                self.log.error('Error {0}'.format(e))

        p = os.path.join(path, filename)

        try:
            with open(p + '.json', 'w') as file:
                json.dump(roi_list_dict, file)
            self.log.info('ROI list saved to file {}.json'.format(p))
        except Exception as e:
            self.log.warning('ROI list not saved: {}'.format(e))

        return None

    # to solve: problem with marker size when loading a new list
    def load_roi_list(self, complete_path=None):
        """ 
        Load a selected roi_list from .json file. 
        """
        # if no path given do nothing
        if complete_path is None:
            self.log.warning('No path to ROI list given')
            return None

        try:
            with open(complete_path, 'r') as file:
                roi_list_dict = json.load(file)

            self._roi_list = self.dict_to_roi(roi_list_dict)

            self.sigRoiListUpdated.emit({
                'name':
                self.roi_list_name,
                'rois':
                self.roi_positions,
                'cam_image':
                self.roi_list_cam_image,
                'cam_image_extent':
                self.roi_list_cam_image_extent
            })
            self.set_active_roi(None if len(self.roi_names) ==
                                0 else self.roi_names[0])
            self.log.info('Loaded ROI list from {}'.format(complete_path))
        except Exception as e:
            self.log.warning('ROI list not loaded: {}'.format(e))
        return None

    @_roi_list.constructor
    def dict_to_roi(self, roi_dict):
        return RegionOfInterestList.from_dict(roi_dict)

    @_roi_list.representer
    def roi_list_to_dict(self, roi_list):
        return roi_list.to_dict()

    ################### mosaic tools
    def add_mosaic(self,
                   roi_width,
                   width,
                   height,
                   x_center_pos=0,
                   y_center_pos=0,
                   z_pos=0,
                   add=False):
        """
        Defines a new list containing a serpentine scan. Parameters can be specified in the settings dialog on GUI option menu.

        @param roi_width: (better distance)
        @param width: number of tiles in x direction
        @param height: number of tiles in y direction
        @param x_center_pos:
        @param y_center_pos:
        @param z_pos: current z position of the stage if there is one; or 0 for two axes stage
        @param bool add: add the mosaic to the present list (True) or start a new one (False)

        @returns: None
        """
        try:
            if not add:
                self.reset_roi_list()  # create a new list

            # create a grid of the central points
            grid = self.make_serpentine_grid(width, height)
            # type conversion from list to np array for making linear, elementwise operations
            grid_array = np.array(grid)
            # shift and stretch the grid to create the roi centers
            # mind the 3rd dimension so that it can be passed to the add_roi method
            # calculate start positions (lower left corner of the grid) given the central position
            x_start_pos = x_center_pos - roi_width * (width - 1) / 2
            y_start_pos = y_center_pos - roi_width * (height - 1) / 2
            roi_centers = grid_array * roi_width + [
                x_start_pos, y_start_pos, z_pos
            ]

            for item in roi_centers:
                self.add_roi(item)
        except Exception:
            self.log.error('Could not create mosaic')

    def make_serpentine_grid(self, width, height):
        """ creates the grid points for a serpentine scan, with ascending x values in even numbered rows and descending x values in odd values rows.
        Each element is appended with z = 0.

        @params: int width: number of columns (x direction)
                 int height: number of rows (y direction)

        returns: list gridpoints: list with points in serpentine scan order
        """
        list_even = [(x, y, 0) for y in range(height) for x in range(width)
                     if y % 2 == 0]
        list_odd = [(x, y, 0) for y in range(height)
                    for x in reversed(range(width)) if y % 2 != 0]
        list_all = list_even + list_odd
        gridpoints = sorted(list_all, key=self.sort_second)
        return gridpoints

    def sort_second(self, val):
        """ helper function for sorting a list of tuples by the second element of each tuple, used for setting up the serpentine grid

        @returns: the second element of value (in the context here, value is a 3dim tuple (x, y, z))
        """
        return val[1]

    # with this overloading it is possible to call it from gui without specifying a roi_distance . the default value is taken
    # but maybe modify to send the selected value with it..
    @QtCore.Slot()
    @QtCore.Slot(float)
    def add_interpolation(
            self,
            roi_distance=50):  # remember to correct the roi_distance parameter
        """ Fills the space between the already defined rois (at least 2) with more rois using a center to center distance roi_distance.
        The grid starts in the minimum x and y coordinates from the already defined rois and covers the maximum x and y coordinates

        @params: roi_distance

        @:returns: None
        """

        if len(self.roi_positions) < 2:
            self.log.warning(
                'Please specify at least 2 ROIs to perform an interpolation')
        else:
            try:
                # find the minimal and maximal x and y coordonates from the current roi_list
                xmin = min(
                    [self.roi_positions[key][0] for key in self.roi_positions])
                xmax = max(
                    [self.roi_positions[key][0] for key in self.roi_positions])
                ymin = min(
                    [self.roi_positions[key][1] for key in self.roi_positions])
                ymax = max(
                    [self.roi_positions[key][1] for key in self.roi_positions])
                # print(xmin, xmax, ymin, ymax)

                # calculate the number of tiles needed
                width = abs(xmax - xmin)
                height = abs(ymax - ymin)
                # print(width, height)
                num_x = ceil(
                    width / roi_distance) + 1  # number of tiles in x direction
                num_y = ceil(
                    height /
                    roi_distance) + 1  # number of tiles in y direction
                # print(num_x, num_y)

                # create a grid of the central points
                grid = self.make_serpentine_grid(
                    int(num_x), int(num_y)
                )  # type conversion necessary because xmin etc are numpy floats
                # type conversion from list to np array for making linear, elementwise operations
                grid_array = np.array(grid)
                # get the current z position of the stage to keep the same level for all rois defined in the interpolation
                # alternative: set it to 0. What should be done in case the different rois are not on the same z level ?
                z = self.stage_position[2]
                # stretch the grid and shift it so that the first center point is in (x_min, y_min)
                roi_centers = grid_array * roi_distance + [xmin, ymin, z]
                # print(roi_centers)

                # list is not reset before adding new rois. we might end up having some overlapping exactly the initial ones.
                # to discuss if the initial ones shall be kept
                # or think of a method how to get rid of the twice defined positions
                for item in roi_centers:
                    self.add_roi(item)

            except Exception:
                self.log.error('Could not create interpolation')

    # functions for the tracking mode of the stage position.
    # using a timer as first approach (did not work because i put the timer into __init__ but it should have gone in on_activate
    # alternatively, use worker thread as for temperature tracking in basic_gui
    # worker thread version
    def start_tracking(self):
        self.tracking = True
        # monitor the current stage position, using a worker thread
        worker = Worker()
        worker.signals.sigFinished.connect(self.tracking_loop)
        self.threadpool.start(worker)

    def stop_tracking(self):
        self.tracking = False
        # get once again the latest position
        position = self.stage_position
        self.sigUpdateStagePosition.emit(position)
        self.sigTrackingModeStopped.emit()

    def tracking_loop(self):
        position = self.stage_position
        self.sigUpdateStagePosition.emit(position)
        if self.tracking:
            # enter in a loop until tracking mode is switched off
            worker = Worker()
            worker.signals.sigFinished.connect(self.tracking_loop)
            self.threadpool.start(worker)

    def set_stage_velocity(self, param_dict):
        self.stage().set_velocity(param_dict)

    def disable_tracking_mode(self):
        """ This method provides a security that tracking mode is not callable from GUI, for example during Tasks. """
        if self.tracking:
            self.stop_tracking()
        self.sigDisableTracking.emit()

    def enable_tracking_mode(self):
        """ This method makes tracking mode again available from GUI, for example when a Task is finishing. """
        self.sigEnableTracking.emit()

    def disable_roi_actions(self):
        self.sigDisableRoiActions.emit()

    def enable_roi_actions(self):
        self.sigEnableRoiActions.emit()
Exemplo n.º 9
0
class AomLogic(GenericLogic):
    """
    This is the logic for controlling AOM diffraction efficiency
    for power control and Psat
    """
    _modclass = 'aomlogic'
    _modtype = 'logic'

    # declare connectors
    voltagescanner = Connector(interface='VoltageScannerInterface')
    laser = Connector(interface='SimpleLaserInterface')
    savelogic = Connector(interface='SaveLogic')
    fitlogic = Connector(interface='FitLogic')

    psat_updated = QtCore.Signal()
    psat_fit_updated = QtCore.Signal()
    psat_saved = QtCore.Signal()
    aom_updated = QtCore.Signal()
    power_available = QtCore.Signal(bool)
    max_power_update = QtCore.Signal()

    # status vars
    _clock_frequency = StatusVar('clock_frequency', 30)

    #_calibration_voltage = ConfigOption('voltage', missing='error')
    #_calibration_efficiency = ConfigOption('efficiency', missing='error')

    # temporary to avoid restarting qudi
    #_calibration_voltage = [0.6, 0.65, .7, .75, .8, .85, .9, .95, 1.0, 1.05, 1.10, 1.15, 1.2, 1.3, 1.4]
    #_calibration_efficiency = [.00141, .00554, .01342, .02467, .03881, .05515, .07320, 0.09160, .11123, .12956,
    #                           .14604, .16094, .17408, .19377, .20777]

    def __init__(self, config, **kwargs):
        super().__init__(config=config, **kwargs)

        self.powers = []
        self.psat_data = []
        self.psat_fit_x = []
        self.psat_fit_y = []
        self.fitted_Isat = 0.0
        self.fitted_Psat = 0.0
        self.fitted_offset = 0.0
        self.psat_fitted = False
        self.psat_collected = False

        #locking for thread safety
        self.threadlock = Mutex()

    def on_activate(self):
        """ Initialisation performed during activation of the module.
        """
        self._voltagescanner = self.get_connector('voltagescanner')
        self._laser = self.get_connector('laser')
        self._save_logic = self.get_connector('savelogic')
        self._fitlogic = self.get_connector('fitlogic')
        config = self.getConfiguration()

        # configure calibration
        self._cal_voltage = config['voltage']
        self._cal_efficiency = config['efficiency']
        self.maximum_efficiency = max(self._cal_efficiency)

        self.set_psat_points()
        self.clear()

        self.psat_updated.connect(self.fit_data)
        self.set_power(1e-4)
        # self.laser.sigPower.connect(self.update_aom)

    def on_deactivate(self):
        self.psat_updated.disconnect(self.fit_data)

    def clear(self):
        self.set_psat_points()
        self.psat_data = np.zeros_like(self.powers)
        self.psat_fit_x = np.linspace(0, max(self.powers), 60)
        self.psat_fit_y = np.zeros_like(self.psat_fit_x)
        self.fitted_Isat = 0.0
        self.fitted_Psat = 0.0
        self.fitted_offset = 0.0
        self.psat_fitted = False
        self.psat_colllected = False
        self.psat_updated.emit()
        self.psat_fit_updated.emit()

    def psat_available(self):
        return self.psat_available

    def psat_fit_available(self):
        return self.psat_fit_available

    def efficiency_for_voltage(self, v):
        return np.interp(v, self._cal_voltage, self._cal_efficiency)

    def voltage_for_efficiency(self, e):
        return np.interp(e, self._cal_efficiency, self._cal_voltage, 0.0,
                         np.inf)

    def voltages_for_powers(self, powers):
        laser_power = self._get_laser_power()
        e = [p / laser_power for p in powers]
        return np.interp(e, self._cal_efficiency, self._cal_voltage, 0.0,
                         np.inf)

    def set_power(self, p):
        laser_power = self._get_laser_power()
        efficiency = p / laser_power
        if efficiency > self.maximum_efficiency:
            self.log.warning("Too much power requested, turn the laser up!")
        else:
            self.power = p
            self.update_aom()

    def source_changed(self):
        self.max_power_update.emit()
        self.update_aom()

    def update_aom(self):
        if self.power > self.current_maximum_power():
            self.power_unavailable.emit()
        else:
            laser_power = self._get_laser_power()
            efficiency = self.power / laser_power
            v = self.voltage_for_efficiency(efficiency)
            self.log.info("Setting AOM voltage {}V efficiency {}".format(
                v, efficiency))
            self._voltagescanner.set_voltage(v)
            self.aom_updated.emit()

    def _get_laser_power(self):
        return self._laser.get_power_setpoint()

    def get_power(self):
        laser_power = self._get_laser_power()
        v = self._voltagescanner.get_voltage()
        efficiency = self.efficiency_for_voltage(v)
        return laser_power * efficiency

    def current_maximum_power(self):
        laser_power = self._get_laser_power()
        return laser_power * self.maximum_efficiency

    def set_psat_points(self, minimum=0.0, maximum=None, points=40):
        if maximum is None:
            maximum = self.current_maximum_power() * .95
        if maximum > self.current_maximum_power():
            self.log.warn(
                "Maximum power is not available without more laser power")

        self.powers = np.linspace(minimum, maximum, points)

    def run_psat(self):
        if max(self.powers) > self.current_maximum_power():
            self.log.warn("Full range not available for current laser power")
        v = self.voltages_for_powers(self.powers)
        self.log.debug("Scanning AOM efficiency with voltages {}".format(v))
        self._voltagescanner.set_up_scanner_clock(
            clock_frequency=self._clock_frequency)
        self._voltagescanner.set_up_scanner()
        o = self._voltagescanner.scan_voltage(v)
        self._voltagescanner.close_scanner()
        self._voltagescanner.close_scanner_clock()
        d = np.append(o, [])
        self.clear()

        self.psat_data = d
        self.psat_voltages = v

        self.psat_collected = True

        self.psat_updated.emit()

        return self.powers, v, self.psat_data

    def fit_data(self):
        model, param = self._fitlogic.make_hyperbolicsaturation_model()
        param['I_sat'].min = 0
        param['I_sat'].max = 1e7
        param['I_sat'].value = max(self.psat_data) * .7
        param['P_sat'].max = 10.0
        param['P_sat'].min = 0.0
        param['P_sat'].value = 0.0001
        param['slope'].min = 0.0
        param['slope'].value = 1e3
        param['offset'].min = 0.0
        fit = self._fitlogic.make_hyperbolicsaturation_fit(
            x_axis=self.powers,
            data=self.psat_data,
            estimator=self._fitlogic.estimate_hyperbolicsaturation,
            add_params=param)
        self.fit = fit
        self.fitted_Psat = fit.best_values['P_sat']
        self.fitted_Isat = fit.best_values['I_sat']
        self.fitted_offset = fit.best_values['offset']
        self.psat_fitted = True
        self.psat_fit_y = model.eval(x=self.psat_fit_x, params=fit.params)

        self.psat_fit_updated.emit()

    def set_to_psat(self):
        if self.fitted_Psat:
            self.set_power(self.fitted_Psat)

    def save_psat(self):
        # File path and name
        filepath = self._save_logic.get_path_for_module(module_name='Psat')

        # We will fill the data OrderedDict to send to savelogic
        data = OrderedDict()

        # Lists for each column of the output file
        power = self.powers
        voltage = self.psat_voltages
        counts = self.psat_data

        data['Power (mW)'] = np.array(power)
        data['Voltage (V)'] = np.array(voltage)
        data['Count rate (/s)'] = np.array(counts)

        self._save_logic.save_data(data,
                                   filepath=filepath,
                                   filelabel='Psat',
                                   fmt=['%.6e', '%.6e', '%.6e'])
        self.log.info('Psat saved to:\n{0}'.format(filepath))

        self.psat_saved.emit()

        return 0

    def on_deactivate(self):
        """ Reverse steps of activation

        @return int: error code (0:OK, -1:error)
        """
        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 = int(clock_frequency)
        #checks if scanner is still running
        if self.module_state() == 'locked':
            return -1
        else:
            return 0
Exemplo n.º 10
0
class ODMRAWGLogic(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='MicrowaveInterface'
    )  # TODO: Why is this connector not MicrowaveInterface?
    pulsegenerator = Connector(interface='PulserInterface')
    savelogic = Connector(interface='SaveLogic')
    taskrunner = Connector(interface='TaskRunner')

    # config option
    # TODO: Take care of the configs later
    mw_scanmode = MicrowaveMode.LIST
    # mw_scanmode = ConfigOption(
    #                 'scanmode',
    #                 'LIST',
    #                 missing='warn',
    #                 converter=lambda x: MicrowaveMode[x.upper()]) # TODO: Do we need this?

    clock_frequency = StatusVar('clock_frequency', 200)
    cw_mw_frequency = StatusVar('cw_mw_frequency', 2870e6)
    cw_mw_power = StatusVar('cw_mw_power', -40)
    sweep_mw_power = StatusVar('sweep_mw_power', -40)
    mw_start = StatusVar('mw_start', 2800e6)
    mw_stop = StatusVar('mw_stop', 2950e6)
    mw_step = StatusVar('mw_step', 2e6)
    freq_list = []
    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)

    # Stuff I added - Dan
    sample_rate = 1.25e9  # Sample set to default - 1.25 GSa/sec
    freq_duration = 1e-6  # Duration of each frequency set to 3 μsec
    samples_per_freq = int(
        sample_rate *
        freq_duration)  # Number of samples required for each frequency
    awg_samples_limit = 5e6
    average_factor = 100000  # How many sweeps will be conducted at each ODMR line scan
    digital_pulse_length = 500e-9  # How long will the digital pulse be
    one_sweep_time = 0.5  # How long will a sweep be
    digital_sync_length = 9e-9  # Synchronize analog and digital channels

    # 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)

    # TODO: Get rid of this, this is just for testing
    # iterable = 1

    def __init__(self, config, **kwargs):
        super().__init__(config=config, **kwargs)
        self.threadlock = Mutex()

    def on_activate(self):
        print('ODMR logic activated')
        """
        Initialisation performed during activation of the module.
        """
        # Get connectors
        self._mw_device = self.microwave1()
        self._awg_device = self.pulsegenerator()
        self._fit_logic = self.fitlogic()
        self._odmr_counter = self.odmrcounter()
        self._save_logic = self.savelogic()
        self._taskrunner = self.taskrunner()

        # self._awg_device.reset()

        # Get hardware constraints
        limits = self.get_hw_constraints()

        # Set/recall microwave source parameters
        # TODO: Take care of the constraints, make sure they are obtained correctly
        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)

        # 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
        # TODO
        self.mw_trigger_pol = TriggerEdge.RISING

        # 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)

        # TODO: Do we need to turn off the AWG?

        # 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)
        print('Deactivated')
        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_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)
            # self._mw_device.set_pulse_mod()
        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)) and isinstance(
                    stop, (int, float)) and isinstance(step, (int, float)):
                self.mw_start = limits.frequency_in_range(start)
                self.mw_stop = limits.frequency_in_range(stop)
                self.mw_step = limits.list_step_in_range(step)
                if self.mw_stop <= self.mw_start:
                    self.mw_stop = self.mw_start + self.mw_step
                if self.mw_stop - self.mw_start > 8e8:
                    self.mw_stop = self.mw_start + np.floor(
                        8e8 / self.mw_step) * self.mw_step
                self.cw_mw_frequency = (self.mw_stop + self.mw_start) / 2
            if isinstance(power, (int, float)):
                self.cw_mw_power = limits.power_in_range(power)
            self.set_cw_parameters(self.cw_mw_frequency, self.cw_mw_power)
        else:
            self.log.warning('set_sweep_parameters failed. Logic is locked.')

        param_dict = {
            'cw_mw_frequency': self.cw_mw_frequency,
            '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.cw_mw_power, self.cw_mw_frequency

    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.rf_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 list_to_waveform(self, freq_list):
        digital_pulse_samples = int(
            np.floor(self.sample_rate * self.digital_pulse_length))
        # analog_samples = {'a_ch0': np.zeros(digital_pulse_samples, dtype=int),'a_ch1': np.zeros(digital_pulse_samples, dtype=int)}
        # digital_samples = {'d_ch0': [], 'd_ch1': []}
        analog_samples = {'a_ch0': [], 'a_ch1': []}
        digital_samples = {'d_ch0': [], 'd_ch2': []}
        print('CW freq is ', self.cw_mw_frequency)
        # TODO: Take care of negative and positive shifts, at the moment we are assuming the
        #  CW frequency is always higher
        self.shifted_freq_list = self.cw_mw_frequency - freq_list
        self.norm_freq_list = self.shifted_freq_list / self.sample_rate

        x = np.linspace(start=0,
                        stop=int(self.samples_per_freq) - 1,
                        num=self.samples_per_freq)
        single_digital_pulse = np.concatenate([
            np.ones(digital_pulse_samples, dtype=int),
            np.zeros(int(self.samples_per_freq) - digital_pulse_samples,
                     dtype=int)
        ])
        digital_sync_samples = int(
            np.floor(self.sample_rate * self.digital_sync_length))
        digital_sync_pulse = np.zeros(int(digital_sync_samples), dtype=int)
        digital_samples['d_ch0'] = np.append(digital_samples['d_ch0'],
                                             digital_sync_pulse)
        digital_samples['d_ch2'] = np.append(digital_samples['d_ch2'],
                                             digital_sync_pulse)
        single_bias_pulse = np.concatenate([
            np.ones(digital_pulse_samples, dtype=int),
            -1 * np.ones(int(self.samples_per_freq) - digital_pulse_samples,
                         dtype=int)
        ])
        # single_bias_pulse = np.concatenate([-1 * np.ones(int(self.samples_per_freq), dtype=int)])

        for norm_freq in self.norm_freq_list:
            analog_samples['a_ch0'] = np.append(
                analog_samples['a_ch0'], np.sin(2 * np.pi * norm_freq * x))
            analog_samples['a_ch1'] = np.append(
                analog_samples['a_ch1'],
                np.sin(2 * np.pi * norm_freq * x - np.pi / 2))
            digital_samples['d_ch0'] = np.append(digital_samples['d_ch0'],
                                                 single_digital_pulse)

        analog_samples['a_ch0'] = np.append(
            analog_samples['a_ch0'],
            np.zeros(2 * digital_pulse_samples + digital_sync_samples))
        analog_samples['a_ch1'] = np.append(
            analog_samples['a_ch1'],
            np.zeros(2 * digital_pulse_samples + digital_sync_samples))

        digital_samples['d_ch0'] = np.append(
            digital_samples['d_ch0'], np.ones(digital_pulse_samples,
                                              dtype=int))
        digital_samples['d_ch0'] = np.append(
            digital_samples['d_ch0'], np.zeros(digital_pulse_samples,
                                               dtype=int))

        digital_samples['d_ch5'] = digital_samples['d_ch0']

        digital_samples['d_ch2'] = np.append(
            digital_samples['d_ch2'],
            np.ones(len(analog_samples['a_ch0']) - len(digital_sync_pulse),
                    dtype=int))

        print('Analog sample array size is', np.size(analog_samples['a_ch0']))
        print('d_ch0 array size is', np.size(digital_samples['d_ch0']))

        return analog_samples, digital_samples

    def sweep_list(self):
        freq_range_size = np.abs(self.mw_stop - self.mw_start)
        if (freq_range_size / self.mw_step
            ) * self.samples_per_freq >= self.awg_samples_limit:
            self.log.warning(
                'Number of frequency steps too large for microwave device. '
                'Lowering resolution to fit the maximum length.')
            self.mw_step = np.floor(
                (freq_range_size /
                 (self.awg_samples_limit / self.samples_per_freq)) *
                10**-3) * 10**3
            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(freq_range_size / self.mw_step))
        end_freq = self.mw_start + num_steps * self.mw_step
        self.freq_list = np.linspace(self.mw_start, end_freq, num_steps + 1)
        return 0

    def test_write_waveform(self):
        self.sweep_list()
        self._awg_device.set_analog_level(amplitude={
            'a_ch0': 500,
            'a_ch1': 500
        })
        analog_samples, digital_samples = self.list_to_waveform(self.freq_list)
        print(digital_samples)
        num_of_samples, waveform_names = self._awg_device.write_waveform(
            name='ODMR',
            analog_samples=analog_samples,
            digital_samples=digital_samples,
            is_first_chunk=True,
            is_last_chunk=True,
            total_number_of_samples=len(analog_samples['a_ch0']))
        return num_of_samples, waveform_names

    def mw_sweep_on(self):
        """
        Switching on the mw source in list/sweep mode.

        @return str, bool: active mode ['cw', 'list', 'sweep'], is_running

        When using the AWG the sweep mode is redundant, so we leave only the list options.
        """
        # TODO: Disable the Sweep option, add an error in case it is enabled.

        self._awg_device.set_analog_level(amplitude={
            'a_ch0': 500,
            'a_ch1': 500
        })
        analog_samples, digital_samples = self.list_to_waveform(self.freq_list)
        print(digital_samples)
        num_of_samples, waveform_names = self._awg_device.write_waveform(
            name='ODMR',
            analog_samples=analog_samples,
            digital_samples=digital_samples,
            is_first_chunk=True,
            is_last_chunk=True,
            total_number_of_samples=len(analog_samples['a_ch0']))
        self._awg_device.load_waveform(load_dict=waveform_names)
        self._awg_device.set_reps(self.average_factor)
        self._mw_device.cw_on()
        # self._mw_device.pulse_mod_on()

        # Following lines update the corrected parameters to the GUI
        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)
        # TODO: Fix this thing here with the mode, where we just define it aggressively as sweep

        # mode, is_running = self._awg_device.get_status()
        self.log.warn("Pretending to know state")
        mode = 'sweep'
        is_running = 1
        self.sigOutputStateUpdated.emit(mode, is_running)
        return mode, is_running

    def reset_sweep(self):
        """
        Resets the AWG, starting the sweep from the beginning.
        """
        self._awg_device.pulser_off()
        time.sleep(0.1)
        self._awg_device.pulser_on()
        return

    def mw_off(self):
        """ Switching off the MW source.

        @return str, bool: active mode ['cw', 'list', 'sweep'], is_running
        """
        error_code_pulsar = self._awg_device.pulser_off()
        if error_code_pulsar < 0:
            self.log.error('Switching off pulsar source failed.')

        error_code_mw = self._mw_device.off()
        if error_code_mw < 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)
        """
        self._odmr_counter.set_up_odmr()
        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.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)

            # Defining the frequency list and sending the length to the ODMR counter
            self.sweep_list()
            # Calculate the average factor - number of sweeps in a single line acquisition
            # based on a single sweep time
            # of 1/2 sec (defined above)
            self.average_factor = int(
                self.one_sweep_time /
                (self.freq_duration * len(self.freq_list)))
            # Just to make sure we're not averaging on a very low number (or zero...)
            if self.average_factor < 100:
                self.average_factor = 100

            sweep_bin_num = self.average_factor * len(self.freq_list) - 1
            self._odmr_counter.set_odmr_length(length=sweep_bin_num)
            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 / (
                sweep_bin_num * self.freq_duration * 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.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()
            self._odmr_counter.clear_odmr()
            # Acquire count data
            time.sleep(self.one_sweep_time + 0.05)
            err, new_counts = self._odmr_counter.count_odmr(
                length=self.odmr_plot_x.size, pulsed=True)

            if err:
                self.stopRequested = True
                self.sigNextLine.emit()
                return

            # Average the sweeps, turning the long array into a short one (whose length is the number of frequencies)
            new_counts = np.reshape(a=new_counts,
                                    newshape=(self.average_factor,
                                              len(self.freq_list)))
            new_counts = np.mean(new_counts, axis=0)

            # save_count_data(self.iterable, data=new_counts)
            # self.iterable += 1
            # 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,
                                 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 {}

        # set all relevant parameter:
        self.set_power(power)
        self.set_sweep_frequencies(freq_start, freq_stop, freq_step)
        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)
            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
Exemplo n.º 11
0
class FlipMirror(Base, SwitchInterface):
    """ This class is implements communication with the Radiant Dyes flip mirror driver using pyVISA

    Example config for copy-paste:

    flipmirror_switch:
        module.Class: 'switches.flipmirror.FlipMirror'
        interface: 'ASRL1::INSTR'
        name: 'Flipmirror Switch'  # optional
        switch_time: 2  # optional
        remember_states: False  # optional
        switch_name: 'Detection'  # optional
        switch_states: ['Spectrometer', 'APD']  # optional
    """

    # ConfigOptions to give the single switch and its states custom names
    _switch_name = ConfigOption(name='switch_name',
                                default='1',
                                missing='nothing')
    _switch_states = ConfigOption(name='switch_states',
                                  default=['Down', 'Up'],
                                  missing='nothing')
    # optional name of the hardware
    _hardware_name = ConfigOption(name='name',
                                  default='Flipmirror Switch',
                                  missing='nothing')
    # if remember_states is True the last state will be restored at reloading of the module
    _remember_states = ConfigOption(name='remember_states',
                                    default=False,
                                    missing='nothing')
    # switch_time to wait after setting the states for the solenoids to react
    _switch_time = ConfigOption(name='switch_time',
                                default=2.0,
                                missing='nothing')
    # name of the serial interface where the hardware is connected.
    # Use e.g. the Keysight IO connections expert to find the device.
    serial_interface = ConfigOption('interface',
                                    'ASRL1::INSTR',
                                    missing='error')

    # StatusVariable for remembering the last state of the hardware
    _states = StatusVar(name='states', default=None)

    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self.lock = RecursiveMutex()
        self._resource_manager = None
        self._instrument = None
        self._switches = dict()

    def on_activate(self):
        """ Prepare module, connect to hardware.
        """
        assert isinstance(self._switch_name,
                          str), 'ConfigOption "switch_name" must be str type'
        assert len(
            self._switch_states
        ) == 2, 'ConfigOption "switch_states" must be len 2 iterable'
        self._switches = self._chk_refine_available_switches(
            {self._switch_name: self._switch_states})

        self._resource_manager = visa.ResourceManager()
        self._instrument = self._resource_manager.open_resource(
            self.serial_interface,
            baud_rate=115200,
            write_termination='\r\n',
            read_termination='\r\n',
            timeout=10,
            send_end=True)

        # reset states if requested, otherwise use the saved states
        if self._remember_states and isinstance(self._states, dict) and \
                set(self._states) == set(self._switches):
            self._states = {
                switch: self._states[switch]
                for switch in self._switches
            }
            self.states = self._states
        else:
            self._states = dict()
            self.states = {
                switch: states[0]
                for switch, states in self._switches.items()
            }

    def on_deactivate(self):
        """ Disconnect from hardware on deactivation.
        """
        self._instrument.close()
        self._resource_manager.close()

    @property
    def name(self):
        """ Name of the hardware as string.

        @return str: The name of the hardware
        """
        return self._hardware_name

    @property
    def available_states(self):
        """ Names of the states as a dict of tuples.

        The keys contain the names for each of the switches. The values are tuples of strings
        representing the ordered names of available states for each switch.

        @return dict: Available states per switch in the form {"switch": ("state1", "state2")}
        """
        return self._switches.copy()

    @property
    def states(self):
        """ The current states the hardware is in.

        The states of the system as a dict consisting of switch names as keys and state names as values.

        @return dict: All the current states of the switches in a state dict of the form {"switch": "state"}
        """
        with self.lock:
            response = self._instrument.query('GP1').strip().upper()
            assert response in {
                'H1', 'V1'
            }, f'Unexpected hardware return value: "{response}"'
            switch, avail_states = next(iter(self.available_states.items()))
            self._states = {switch: avail_states[int(response == 'V1')]}
            return self._states.copy()

    @states.setter
    def states(self, state_dict):
        """ The setter for the states of the hardware.

        The states of the system can be set by specifying a dict that has the switch names as keys
        and the names of the states as values.

        @param dict state_dict: state dict of the form {"switch": "state"}
        """
        assert isinstance(state_dict, dict), \
            f'Property "state" must be dict type. Received: {type(state_dict)}'
        assert all(switch in self.available_states for switch in state_dict), \
            f'Invalid switch name(s) encountered: {tuple(state_dict)}'
        assert all(isinstance(state, str) for state in state_dict.values()), \
            f'Invalid switch state(s) encountered: {tuple(state_dict.values())}'

        if state_dict:
            with self.lock:
                switch, state = next(iter(state_dict.items()))
                down = self.available_states[switch][0] == state
                answer = self._instrument.query('SH1' if down else 'SV1',
                                                delay=self._switch_time)
                assert answer == 'OK1', \
                    f'setting of state "{state}" in switch "{switch}" failed with return value "{answer}"'
                self._states = {switch: state}
                self.log.debug('{0}-{1}: {2}'.format(self.name, switch, state))

    def get_state(self, switch):
        """ Query state of single switch by name

        @param str switch: name of the switch to query the state for
        @return str: The current switch state
        """
        assert switch in self.available_states, f'Invalid switch name: "{switch}"'
        return self.states[switch]

    def set_state(self, switch, state):
        """ Query state of single switch by name

        @param str switch: name of the switch to change
        @param str state: name of the state to set
        """
        self.states = {switch: state}
Exemplo n.º 12
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 module not 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(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(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(get_main_dir(), 'config')
        filename = QtWidgets.QFileDialog.getSaveFileName(
            self._mw, 'Save Configration', defaultconfigpath,
            'Configuration files (*.cfg)')[0]
        if filename != '':
            self.sigSaveConfig.emit(filename)
Exemplo n.º 13
0
class SnvmLogic(GenericLogic):

    doublescanner = Connector(interface='SnvmScannerInterface')
    odmrscanner = Connector(interface='MicrowaveInterface')

    slow_motion_clock_rate = StatusVar('slow_motion_clock_rate', 10)
    backward_speed = StatusVar('slow_motion_speed', 1)

    signal_moved_to_point = QtCore.Signal()
    # signals
    signal_start_snvm = QtCore.Signal()
    signal_continue_snvm = QtCore.Signal()
    signal_start_confocal = QtCore.Signal()
    signal_continue_confocal = QtCore.Signal()
    signal_stop_scan = QtCore.Signal()

    signal_snvm_image_updated = QtCore.Signal()
    signal_xy_image_updated = QtCore.Signal()
    signal_freq_px_acquired = QtCore.Signal(
        int
    )  #Emits the row of the temporary data matrix to store the odmr data
    signal_xy_px_acquired = QtCore.Signal()
    signal_scan_finished = QtCore.Signal(
        bool)  #Emits True if the scan was snvm, False otherwise

    signal_snvm_initialized = QtCore.Signal()
    signal_confocal_initialized = QtCore.Signal()

    def __init__(self, config, **kwargs):
        super().__init__(config=config, **kwargs)

        self.threadlock = Mutex()
        self.stopRequested = False

    def on_activate(self):
        self._scanning_device = self.doublescanner()
        self._odmrscanner = self.odmrscanner()

        self.set_slowmotion_clockrate(self.slow_motion_clock_rate)
        self.set_motion_speed(self.backward_speed)

        #TODO: figure out a smart way of storing all these data in another class
        #####
        # Setting up the scanning initial parameters, and get the two stack names.
        #####
        self.sampleStackName, self.tipStackName = self._scanning_device.get_stack_names(
        )

        self._active_stack = self.sampleStackName  #Default stack is sample

        #Get the maximum scanning ranges, and the position to voltage conversion factors, and put them in a dictionary
        self.x_maxrange = dict()
        self.y_maxrange = dict()
        self.position_to_voltage_arrays = dict()

        for stack in [self.sampleStackName, self.tipStackName]:
            x_range, y_range = self._scanning_device.get_position_range(
                stack=stack)
            x_volt_range, y_volt_range = self._scanning_device.get_voltage_range(
                stack=stack)
            self.x_maxrange[stack] = x_range
            self.y_maxrange[stack] = y_range

            self.position_to_voltage_arrays[stack] = [
                (x_volt_range[1] - x_volt_range[0]) /
                (x_range[1] - x_range[0]),
                (y_volt_range[1] - y_volt_range[0]) / (y_range[1] - y_range[0])
            ]

        #These are the scanning ranges that will be used for the scanning
        self.scanning_x_range = 0  #Initalize to zero
        self.scanning_y_range = 0

        self.scanning_x_resolution = 0
        self.scanning_y_resolution = 0

        #Integration time per pixel
        self.px_time = 0
        self._photon_samples = 0
        self.backward_pixels = 0  #The number is determined by the clock frequency and the bw_speed

        #These are the indices which will be used to scan through the arrays of the frequencies and position pairs
        self._x_scanning_index = 0
        self._y_scanning_index = 0
        self._x_index_step = 1  #This coefficient is decided to decide the direction of the x scanning
        self._freq_scanning_index = 0
        self._odmr_rep_index = 0  #To keep track of the averages
        self._is_retracing = False
        self.stopRequested = False

        self.store_retrace = False

        self.invalid = np.nan  #Number corresponding to invalid data points.

        ####
        # Set up the ODMR scanner parameters
        ####
        self.start_freq = 0
        self.stop_freq = 0
        self.freq_resolution = 0
        self.mw_power = -100
        self.odmr_averages = 0

        #Initalize the attributes that will be the scan data containers
        self._x_scanning_axis = None
        self._y_scanning_axis = None
        self.xy_scan_matrix = None
        self.snvm_matrix = None
        self._temp_afm_matrix = None  #Matrix used to store the AFM values while scanning the ODMR.
        self.temp_freq_matrix = None  #This matrix is used to store the ODMR traces to be averaged.
        self.xy_scan_matrix_retrace = None
        self.snvm_matrix_retrace = None
        self.freq_axis = None
        self._xy_matrix_width = None  # This variable is used only to keep the code clean and reduce the calls to np.shape
        self._freq_axis_length = None  #Same here
        self.average_odmr_trace = None

        self._snvm_active = False

        #Now connect all the signals
        self.signal_continue_snvm.connect(self.continue_snvm_scanning,
                                          QtCore.Qt.QueuedConnection)
        self.signal_stop_scan.connect(self.stop_scanning,
                                      QtCore.Qt.QueuedConnection)
        self.signal_xy_px_acquired.connect(self.move_to_xy_pixel,
                                           QtCore.Qt.QueuedConnection)
        self.signal_freq_px_acquired.connect(self.move_to_freq_pixel,
                                             QtCore.Qt.QueuedConnection)
        self.signal_continue_confocal.connect(self.continue_confocal_scanning,
                                              QtCore.Qt.QueuedConnection)

    def on_deactivate(self):
        pass

    def _prepare_data_matrices(self):

        #Clip the ranges if they are out of bound
        self.check_xy_ranges()

        #Generate axes and matrices to store the data
        x_axis = np.linspace(self.scanning_x_range[0],
                             self.scanning_x_range[1],
                             self.scanning_x_resolution)
        y_axis = np.linspace(self.scanning_y_range[0],
                             self.scanning_y_range[1],
                             self.scanning_y_resolution)

        #Now generate the matrices to store the data
        xy_scan_matrix = np.zeros((len(x_axis), len(y_axis)), dtype=np.float64)
        # FIXME: for now the stack scanner is the one that's assumed to have the ESR sequence. Maybe consider a flexible
        #  way of doing this
        if self._snvm_active:
            step_number = 1 + round(
                (self.stop_freq - self.start_freq) / self.freq_resolution)
            freq_axis = np.linspace(self.start_freq, self.stop_freq,
                                    step_number)
            snvm_matrix = np.zeros(
                (xy_scan_matrix.shape[0], xy_scan_matrix.shape[1],
                 len(freq_axis), self.odmr_averages),
                dtype=np.float64)
            temp_freq_matrix = np.full((self.odmr_averages, len(freq_axis)),
                                       self.invalid)
            temp_afm_matrix = np.copy(temp_freq_matrix)
            average_odmr_trace = np.zeros((temp_freq_matrix.shape[1], ))
        else:
            snvm_matrix = np.copy(xy_scan_matrix[:, :, np.newaxis])
            freq_axis = None
            temp_freq_matrix = None
            average_odmr_trace = None
            temp_afm_matrix = None

        if self.store_retrace:
            xy_scan_matrix_retrace = np.copy(xy_scan_matrix)
            snvm_matrix_retrace = np.copy(snvm_matrix)
        else:
            xy_scan_matrix_retrace = None
            snvm_matrix_retrace = None

        self._x_scanning_axis = x_axis
        self._y_scanning_axis = y_axis
        self.xy_scan_matrix = xy_scan_matrix
        self.snvm_matrix = snvm_matrix
        self.temp_freq_matrix = temp_freq_matrix
        self._temp_afm_matrix = temp_afm_matrix
        self.average_odmr_trace = average_odmr_trace
        self.xy_scan_matrix_retrace = xy_scan_matrix_retrace
        self.snvm_matrix_retrace = snvm_matrix_retrace
        self.freq_axis = freq_axis
        self._xy_matrix_width = self.xy_scan_matrix.shape[0]
        self._freq_axis_length = len(self.freq_axis) if isinstance(
            self.freq_axis, np.ndarray) else None

    def check_xy_ranges(self):
        """
        Check that the requested scanning ranges are within the maximum scanning ranges set by the hardware.
        If they are out of bounds, clip the values.
        """
        curr_x_minrange, curr_x_maxrange = self.x_maxrange[self._active_stack]
        curr_y_minrange, curr_y_maxrange = self.y_maxrange[self._active_stack]
        # TODO: emit a signal when the clipping happens, and update the GUI limits accordingly
        if not (
            (curr_x_minrange <= self.scanning_x_range[0] <= curr_x_maxrange)
                or not (curr_x_minrange <= self.scanning_x_range[1] <=
                        curr_x_maxrange)):
            self.scanning_x_range = np.clip(self.scanning_x_range,
                                            curr_x_minrange, curr_x_maxrange)
            self.log.warning(
                "x scanning range limits are out of bounds, clipped back to the maximum values."
            )

        if not (
            (curr_y_minrange <= self.scanning_y_range[0] <= curr_y_maxrange)
                or not (curr_y_minrange <= self.scanning_y_range[1] <=
                        curr_y_maxrange)):
            self.scanning_y_range = np.clip(self.scanning_y_range,
                                            curr_y_minrange, curr_y_maxrange)
            self.log.warning(
                "y scanning range limits are out of bounds, clipped back to the maximum values."
            )
        return 0

    def prepare_devices(self):
        """
        Initalize the scanning device, prior to starting the scan.
        """

        self._scanning_device.module_state.lock()
        self._photon_samples = self.pxtime_to_samples()
        if self._snvm_active:
            analog_channels = self._scanning_device.get_ai_counter_channels(
                stack_name=self._active_stack)
        else:
            analog_channels = None
        self._scanning_device.prepare_counters(
            samples_to_acquire=self._photon_samples,
            counter_ai_channels=analog_channels)

        self._scanning_device.create_ao_task(self._active_stack)
        if self.store_retrace is False:
            self._scanning_device.prepare_motion_clock()
            clk_freq = self._scanning_device.get_motion_clock_frequency()
            speed = self._scanning_device.get_motion_speed()
            self.backward_pixels = int(
                ((self._x_scanning_axis.max() - self._x_scanning_axis.min()) /
                 speed) * clk_freq)
            if self.backward_pixels < 2:
                self.backward_pixels = 2

        if self._snvm_active:
            self._odmrscanner.module_state.lock()
            try:
                pass
                # FIXME: look into how to operate the srs in list mode
                #self._odmrscanner.set_list(frequency=self.freq_axis, power=self.mw_power)
            except:
                self.log.error(
                    "Failed loading the frequency axis into the ODMR scanner. Aborted execution."
                )
                #self._scanning_device.module_state.unlock()
                #self._odmrscanner.module_state.unlock()

    def start_snvm_scanning(self):

        self.stopRequested = False

        self.module_state.lock()

        self._active_stack = self.sampleStackName
        self._snvm_active = True
        self._prepare_data_matrices()

        self._initialize_scanning_statuses()
        self.prepare_devices()

        self.signal_snvm_image_updated.emit()
        self.signal_snvm_initialized.emit()

        #self._scanning_device.scanner_set_position([self._x_scanning_axis[self._x_scanning_index],
        #                                           self._y_scanning_axis[self._y_scanning_index]],
        #                                           stack=self._active_stack)

        self._scanning_device.scanner_slow_motion([
            self._x_scanning_axis[self._x_scanning_index],
            self._y_scanning_axis[self._y_scanning_index]
        ],
                                                  stack=self._active_stack,
                                                  clear_ao_whenfinished=False)
        #FIXME: look into how to operate the srs in list mode
        self._odmrscanner.set_frequency(
            self.freq_axis[self._freq_scanning_index])
        self._odmrscanner.on()

        self.signal_continue_snvm.emit()

    def start_confocal_scanning(self):
        self.module_state.lock()

        self._active_stack = self.tipStackName
        self._snvm_active = False
        self._prepare_data_matrices()

        self._initialize_scanning_statuses()
        self.prepare_devices()

        self.signal_xy_image_updated.emit()
        self.signal_confocal_initialized.emit()

        #self._scanning_device.scanner_set_position([self._x_scanning_axis[self._x_scanning_index],
        #                                            self._y_scanning_axis[self._y_scanning_index]],
        #                                           stack=self._active_stack)
        self._scanning_device.scanner_slow_motion([
            self._x_scanning_axis[self._x_scanning_index],
            self._y_scanning_axis[self._y_scanning_index]
        ],
                                                  stack=self._active_stack,
                                                  clear_ao_whenfinished=False)
        self.signal_continue_confocal.emit()

    def continue_snvm_scanning(self):
        acquire_data = False if (self.store_retrace is False) and (
            self._is_retracing is True) else True
        if acquire_data:
            #If the index of the ODMR is less than the averages, keep acquiring
            if self._odmr_rep_index < self.odmr_averages:
                counts, ainput = self.acquire_pixel()
                self.temp_freq_matrix[self._odmr_rep_index,
                                      self._freq_scanning_index] = counts
                self._temp_afm_matrix[
                    self._odmr_rep_index,
                    self._freq_scanning_index] = ainput.mean()

                if self._odmr_rep_index > 0:
                    self.average_odmr_trace = np.nanmean(self.temp_freq_matrix,
                                                         axis=0)

                #TODO: in the GUI, when the user changes between trace and retrace, or changes the frequency slice,
                # the refresh plotting takes the average, along the last axis of the snvm_matrix. This means that it will
                # display an extra pixel with lower value, until the move_to_next_pixel is called.
                if self._is_retracing and self.store_retrace:
                    self.snvm_matrix_retrace[self._y_scanning_index,
                                             self._x_scanning_index,
                                             self._freq_scanning_index,
                                             self._odmr_rep_index] = counts
                else:
                    self.snvm_matrix[self._y_scanning_index,
                                     self._x_scanning_index,
                                     self._freq_scanning_index,
                                     self._odmr_rep_index] = counts
                self.signal_freq_px_acquired.emit(self._odmr_rep_index)

            #Else, the OMDR acquisition for the pixel has finished. Store the data and ask for the next pixel. If also the
            #Scanning is done, tell that the scanning has finished.
            else:
                if self._is_retracing and self.store_retrace:
                    # FIXME: I am not acquiring and storing properly the analog input, find a way after basic debugging done
                    self.xy_scan_matrix_retrace[
                        self._y_scanning_index,
                        self._x_scanning_index] = self._temp_afm_matrix.mean()
                else:
                    self.xy_scan_matrix[
                        self._y_scanning_index,
                        self._x_scanning_index] = self._temp_afm_matrix.mean()

                #The ODMR sequence has finished. Update the indices accordingly
                self._x_scanning_index += self._x_index_step
                self._odmr_rep_index = 0
                self.temp_freq_matrix[:] = self.invalid
                self.average_odmr_trace[:] = self.invalid
                self.signal_snvm_image_updated.emit()
                self.signal_xy_px_acquired.emit()

    def continue_confocal_scanning(self):
        acquire_data = False if (self.store_retrace is False) and (
            self._is_retracing is True) else True
        if acquire_data:
            counts, _ = self.acquire_pixel()
            if self._is_retracing and self.store_retrace:
                self.xy_scan_matrix_retrace[self._y_scanning_index,
                                            self._x_scanning_index] = counts
            else:
                self.xy_scan_matrix[self._y_scanning_index,
                                    self._x_scanning_index] = counts
        self.signal_xy_image_updated.emit()
        self._x_scanning_index += self._x_index_step
        self.signal_xy_px_acquired.emit()

    def pxtime_to_samples(self):
        return round(self.px_time *
                     self._scanning_device.get_counter_clock_frequency())

    def acquire_pixel(self):
        data = self._scanning_device.read_pixel(self._photon_samples)
        return data

    def move_to_xy_pixel(self):
        if not self._is_retracing and self._x_scanning_index == len(
                self._x_scanning_axis):
            self._is_retracing = True
            self._x_index_step = -1
            self._x_scanning_index += self._x_index_step
            if (self._y_scanning_index == len(self._y_scanning_axis) -
                    1) and not self.store_retrace:
                self.stopRequested = True
        elif self._is_retracing and self._x_scanning_index < 0:
            self._is_retracing = False
            self._x_index_step = 1
            self._x_scanning_index += self._x_index_step
            self._y_scanning_index += 1
            if self._y_scanning_index == len(self._y_scanning_axis):
                self.stopRequested = True

        if not self.stopRequested:
            if self._is_retracing and not self.store_retrace:
                retrace_line = np.linspace(self._x_scanning_axis.max(),
                                           self._x_scanning_axis.min(),
                                           self.backward_pixels)
                retrace_line = np.vstack(
                    (retrace_line,
                     np.full(retrace_line.shape,
                             self._y_scanning_axis[self._y_scanning_index])))
                self._scanning_device.move_along_line(
                    position_array=retrace_line, stack=self._active_stack)
                self._x_scanning_index = 0
                self._y_scanning_index += 1
                self._is_retracing = False
                self._x_index_step = 1

            new_x_pos = self._x_scanning_axis[self._x_scanning_index]
            new_y_pos = self._y_scanning_axis[self._y_scanning_index]
            self._scanning_device.scanner_set_position(
                [new_x_pos, new_y_pos], stack=self._active_stack)

            if self._snvm_active:
                self.signal_continue_snvm.emit()
            else:
                self.signal_continue_confocal.emit()
        else:
            self.signal_stop_scan.emit()

    def move_to_freq_pixel(self):
        if not self.stopRequested:
            self._freq_scanning_index += 1
            if self._freq_scanning_index == self._freq_axis_length:
                self._odmr_rep_index += 1
                self._freq_scanning_index = 0
            self._odmrscanner.set_frequency(
                self.freq_axis[self._freq_scanning_index])
            self.signal_continue_snvm.emit()
        else:
            self.signal_stop_scan.emit()

    def stop_scanning(self):
        if self.stopRequested:
            with self.threadlock:
                self._scanning_device.close_counters()
                self.stopRequested = False
                self.stop_xy_scanner()
                if self._snvm_active:
                    self.stop_freq_scanner()
                if not self.store_retrace:
                    self._scanning_device.clear_motion_clock()
                self.module_state.unlock()
            self.signal_scan_finished.emit(self._snvm_active)

    def stop_xy_scanner(self):
        """Closing the scanner device.

        @return int: error code (0:OK, -1:error)
        """
        try:
            self._scanning_device.close_counters()
        except Exception as e:
            self.log.exception('Could not close the scanning tasks.')
        try:
            self._scanning_device.clear_ao_task(self._active_stack)
        except:
            self.log.exception("Could not clear the ao task.")
        try:
            self._scanning_device.module_state.unlock()
        except Exception as e:
            self.log.exception('Could not unlock scanning device.')

        return 0

    def stop_freq_scanner(self):
        self._odmrscanner.off()
        try:
            self._odmrscanner.module_state.unlock()
        except Exception as e:
            self.log.exception('Could not unlock scanning device.')

        return 0

    def go_to_point(self, xy_coord, stack=None):
        self._scanning_device.scanner_slow_motion(xy_coord, stack=stack)
        self.signal_moved_to_point.emit()

    def get_xy_image_range(self, multiplier=1):
        """
        Multiplier is an optional parameter to convert the range to the desired units
        """
        return [[
            self._x_scanning_axis[0] * multiplier,
            self._x_scanning_axis[-1] * multiplier
        ],
                [
                    self._y_scanning_axis[0] * multiplier,
                    self._y_scanning_axis[-1] * multiplier
                ]]

    def get_xy_step_size(self, multiplier=1):
        return [
            (self._x_scanning_axis[1] - self._x_scanning_axis[0]) * multiplier,
            (self._y_scanning_axis[1] - self._y_scanning_axis[0]) * multiplier
        ]

    def get_stack_names(self):
        return self._scanning_device.get_stack_names()

    def get_slowmotion_clockrate(self):
        return self._scanning_device.get_motion_clock_frequency()

    def set_slowmotion_clockrate(self, clockrate):
        # FIXME: dirty trick to keep the clock rate as a status variable which sets the clock rate when reloading Qudi.
        self.slow_motion_clock_rate = clockrate
        self._scanning_device.set_motion_clock_frequency(clockrate)

    def get_motion_speed(self):
        return self._scanning_device.get_motion_speed()

    def set_motion_speed(self, speed):
        #FIXME: dirty trick to keep the motion_speed as a status variable which sets the speed when reloading Qudi.
        self.backward_speed = speed
        self._scanning_device.set_motion_speed(speed * 1e-6)

    def _initialize_scanning_statuses(self):
        self._x_scanning_index = 0
        self._y_scanning_index = 0
        self._x_index_step = 1
        self._freq_scanning_index = 0
        self._odmr_rep_index = 0
        self._is_retracing = False
        self.stopRequested = False
Exemplo n.º 14
0
class QDPlotLogic(GenericLogic):
    """ This logic module helps display user data in plots, and makes it easy to save.
    
    There are phythonic setters and getters for each of the parameter and data. 
    They can be called by "plot_<plot_number>_parameter". plot_number ranges from 1 to 3.
    Parameters are: x_limits, y_limits, x_label, y_label, x_unit, y_unit, x_data, y_data, clear_old_data
    
    All parameters and data can also be interacted with by calling get_ and set_ functions.

    Example config for copy-paste:

    qdplotlogic:
        module.Class: 'qdplot_logic.QDPlotLogic'
        connect:
            save_logic: 'savelogic'
            fit_logic: 'fitlogic'
        default_plot_number: 3
    """
    sigPlotDataUpdated = QtCore.Signal(int, list, list, bool)
    sigPlotParamsUpdated = QtCore.Signal(int, dict)
    sigPlotNumberChanged = QtCore.Signal(int)
    sigFitUpdated = QtCore.Signal(int, np.ndarray, str, str)

    # declare connectors
    save_logic = Connector(interface='SaveLogic')
    fit_logic = Connector(interface='FitLogic')

    _default_plot_number = ConfigOption(name='default_plot_number', default=3)

    fit_container = StatusVar(name='fit_container', default=None)

    def __init__(self, *args, **kwargs):
        """ Create QDPlotLogic object with connectors.

        @param dict args: optional parameters
        @param dict kwargs: optional keyword parameters
        """
        super().__init__(*args, **kwargs)

        self._save_logic = None
        self._fit_logic = None

        # locking for thread safety
        self.threadlock = RecursiveMutex()

        self._clear_old = list()
        self._x_limits = list()
        self._y_limits = list()
        self._x_label = list()
        self._y_label = list()
        self._x_unit = list()
        self._y_unit = list()
        self._x_data = list()
        self._y_data = list()
        self._fit_data = list()
        self._fit_results = list()
        self._fit_method = list()

    def on_activate(self):
        """ Initialisation performed during activation of the module. """
        # Sanity-check ConfigOptions
        if not isinstance(self._default_plot_number,
                          int) or self._default_plot_number < 1:
            self.log.warning(
                'Invalid number of plots encountered in config. Falling back to 1.'
            )
            self._default_plot_number = 1

        self._save_logic = self.save_logic()
        self._fit_logic = self.fit_logic()

        self._clear_old = list()
        self._x_limits = list()
        self._y_limits = list()
        self._x_label = list()
        self._y_label = list()
        self._x_unit = list()
        self._y_unit = list()
        self._x_data = list()
        self._y_data = list()
        self._fit_data = list()
        self._fit_results = list()
        self._fit_method = list()

        self.set_number_of_plots(self._default_plot_number)

    def on_deactivate(self):
        """ De-initialisation performed during deactivation of the module. """
        for i in reversed(range(self.number_of_plots)):
            self.remove_plot(i)
        self._save_logic = None
        self._fit_logic = None

    @fit_container.constructor
    def sv_set_fit(self, val):
        """ Set up fit container """
        fc = self.fit_logic().make_fit_container('Plot QDPlotterLogic', '1d')
        fc.set_units(['', 'a.u.'])
        if not (isinstance(val, dict) and len(val) > 0):
            val = dict()
        fc.load_from_dict(val)
        return fc

    @fit_container.representer
    def sv_get_fit(self, val):
        """ Save configured fits """
        if len(val.fit_list) > 0:
            return val.save_to_dict()
        else:
            return None

    @property
    def number_of_plots(self):
        with self.threadlock:
            return len(self._clear_old)

    @QtCore.Slot()
    def add_plot(self):
        with self.threadlock:
            self._clear_old.append(True)
            self._x_limits.append([-0.5, 0.5])
            self._y_limits.append([-0.5, 0.5])
            self._x_label.append('X')
            self._y_label.append('Y')
            self._x_unit.append('a.u.')
            self._y_unit.append('a.u.')
            self._x_data.append([np.zeros(1)])
            self._y_data.append([np.zeros(1)])
            self._fit_data.append(None)
            self._fit_results.append(None)
            self._fit_method.append('No Fit')
            plot_index = self.number_of_plots - 1

            self.sigPlotNumberChanged.emit(self.number_of_plots)
            self.sigPlotDataUpdated.emit(plot_index, self._x_data[plot_index],
                                         self._y_data[plot_index],
                                         self._clear_old[plot_index])
            if self._fit_method[plot_index] != 'No Fit':
                self.sigFitUpdated.emit(plot_index, self._fit_data[plot_index],
                                        self._fit_results[plot_index],
                                        self._fit_method[plot_index])
            params = {
                'x_label': self._x_label[plot_index],
                'y_label': self._y_label[plot_index],
                'x_unit': self._x_unit[plot_index],
                'y_unit': self._y_unit[plot_index],
                'x_limits': self._x_limits[plot_index],
                'y_limits': self._y_limits[plot_index]
            }
            self.sigPlotParamsUpdated.emit(plot_index, params)

    @QtCore.Slot()
    @QtCore.Slot(int)
    def remove_plot(self, plot_index=None):
        with self.threadlock:
            if plot_index is None or plot_index == -1:
                plot_index = -1
            elif not (0 <= plot_index < self.number_of_plots):
                raise IndexError(
                    'Plot index {0:d} out of bounds.'.format(plot_index))

            del self._clear_old[plot_index]
            del self._x_limits[plot_index]
            del self._y_limits[plot_index]
            del self._x_label[plot_index]
            del self._y_label[plot_index]
            del self._x_unit[plot_index]
            del self._y_unit[plot_index]
            del self._x_data[plot_index]
            del self._y_data[plot_index]
            del self._fit_data[plot_index]
            del self._fit_results[plot_index]
            del self._fit_method[plot_index]
            self.sigPlotNumberChanged.emit(self.number_of_plots)

            update_range = (-1, ) if plot_index == -1 else range(
                plot_index, self.number_of_plots)
            for i in update_range:
                self.sigPlotDataUpdated.emit(i, self._x_data[i],
                                             self._y_data[i],
                                             self._clear_old[i])
                self.sigFitUpdated.emit(i, self._fit_data[i],
                                        self._fit_results[i],
                                        self._fit_method[i])
                params = {
                    'x_label': self._x_label[i],
                    'y_label': self._y_label[i],
                    'x_unit': self._x_unit[i],
                    'y_unit': self._y_unit[i],
                    'x_limits': self._x_limits[i],
                    'y_limits': self._y_limits[i]
                }
                self.sigPlotParamsUpdated.emit(i, params)

    @QtCore.Slot(int)
    def set_number_of_plots(self, plt_count):
        with self.threadlock:
            if not isinstance(plt_count, int):
                raise TypeError
            if plt_count < 1:
                self.log.error('number of plots must be integer >= 1.')
                return
            while self.number_of_plots < plt_count:
                self.add_plot()
            while self.number_of_plots > plt_count:
                self.remove_plot()

    def get_x_data(self, plot_index=0):
        """ Get the data of the x-axis being plotted.
        
        @param int plot_index: index of the plot in the range from 0 to number_of_plots-1
        @return np.ndarray or list of np.ndarrays x: data of the x-axis
        """
        with self.threadlock:
            if 0 <= plot_index < self.number_of_plots:
                return self._x_data[plot_index]
            self.log.error(
                'Error while retrieving plot x_data. Plot index {0:d} out of bounds.'
                ''.format(plot_index))
            return [np.zeros(0)]

    def get_y_data(self, plot_index=0):
        """ Get the data of the y-axis being plotted.
        
        @param int plot_index: index of the plot in the range from 0 to number_of_plots-1
        @return np.ndarray or list of np.ndarrays y: data of the y-axis
        """
        with self.threadlock:
            if 0 <= plot_index < self.number_of_plots:
                return self._y_data[plot_index]
            self.log.error(
                'Error while retrieving plot y_data. Plot index {0:d} out of bounds.'
                ''.format(plot_index))
            return [np.zeros(0)]

    def set_data(self,
                 x=None,
                 y=None,
                 clear_old=True,
                 plot_index=0,
                 adjust_scale=True):
        """ Set the data to plot

        @param np.ndarray or list of np.ndarrays x: data of independents variable(s)
        @param np.ndarray or list of np.ndarrays y: data of dependent variable(s)
        @param bool clear_old: clear old plots in GUI if True
        @param int plot_index: index of the plot in the range from 0 to 2
        @param bool adjust_scale: Whether auto-scale should be performed after adding data or not.
        """
        with self.threadlock:
            if x is None:
                self.log.error('No x-values provided. Cannot set plot data.')
                return -1
            if y is None:
                self.log.error('No y-values provided. Cannot set plot data.')
                return -1
            if not (0 <= plot_index < self.number_of_plots):
                self.log.error(
                    'Plot index {0:d} out of bounds. To add a new plot, call set_number_of_plots(int) '
                    'or add_plot() first.'.format(plot_index))
                return -1

            self._clear_old[plot_index] = clear_old
            # check if input is only an array (single plot) or a list of arrays (one or several plots)
            if isinstance(
                    x[0],
                    np.ndarray):  # if x is an array, type(x[0]) is a np.float
                self._x_data[plot_index] = list(x)
                self._y_data[plot_index] = list(y)
            else:
                self._x_data[plot_index] = [x]
                self._y_data[plot_index] = [y]

            # reset fit for this plot
            self._fit_data[plot_index] = None
            self._fit_results[plot_index] = None
            self._fit_method[plot_index] = None

            # automatically set the correct range
            self.set_x_limits(plot_index=plot_index)
            self.set_y_limits(plot_index=plot_index)

            self.sigPlotDataUpdated.emit(plot_index, self._x_data[plot_index],
                                         self._y_data[plot_index], clear_old)
            self.sigPlotParamsUpdated.emit(
                plot_index, {
                    'x_limits': self._x_limits[plot_index],
                    'y_limits': self._y_limits[plot_index]
                })
            if adjust_scale:
                self.update_auto_range(plot_index, True, True)
            return 0

    @QtCore.Slot(str, int)
    def do_fit(self, fit_method, plot_index=0):
        """ Get the data of the x-axis being plotted.
        
        @param str fit_method: name of the fit_method, this needs to match the methods in
                               fit_container.
        @param int plot_index: index of the plot in the range from 0 to 2
        @return int plot_index, 3D np.ndarray fit_data, str result, str fit_method: result of fit
        """
        with self.threadlock:
            if not (0 <= plot_index < self.number_of_plots):
                raise IndexError(
                    'Plot index {0:d} out of bounds. Unable to perform data fit.'
                    .format(plot_index))
            # check that the fit_method is correct
            if fit_method is None or isinstance(fit_method, str):
                if fit_method not in self.fit_container.fit_list:
                    if fit_method is not None and fit_method != 'No Fit':
                        self.log.warning(
                            'Fit function "{0}" not available in fit container. Configure '
                            'available fits first.'.format(fit_method))
                    fit_method = 'No Fit'
            else:
                raise TypeError(
                    'Parameter fit_method must be str or None type.')

            result = ''
            fit_data = list()

            # do one fit for each data set in the plot
            for data_set in range(len(self._x_data[plot_index])):
                x_data = self._x_data[plot_index][data_set]
                y_data = self._y_data[plot_index][data_set]

                self.fit_container.set_current_fit(fit_method)

                # only fit if the is enough data to actually do the fit
                if len(x_data) < 2 or len(y_data) < 2 or min(x_data) == max(
                        x_data):
                    self.log.warning(
                        'The data you are trying to fit does not contain enough points for a fit.'
                    )
                    return (plot_index,
                            np.zeros(shape=(len(self._x_data[plot_index]), 2,
                                            10)), 'results',
                            self.fit_container.current_fit)

                # actually do the fit
                fit_x, fit_y, result_set = self.fit_container.do_fit(
                    np.array(x_data), np.array(y_data))
                fit_data_set = np.array([fit_x, fit_y])
                fit_data.append(fit_data_set)

                # Get formatted result string and concatenate the results of the data sets
                if fit_method == 'No Fit':
                    formatted_fitresult = 'No Fit'
                else:
                    try:
                        formatted_fitresult = units.create_formatted_output(
                            result_set.result_str_dict)
                    except:
                        formatted_fitresult = 'This fit does not return formatted results'
                tabbed_result = '\n  '.join(
                    formatted_fitresult.split('\n')[:-1])
                result += 'data_set {0}:\n  {1}\n'.format(
                    data_set, tabbed_result)

            # convert list to np.ndarray to make handling it much more efficient
            fit_data = np.array(fit_data)

            # save the fit results internally
            self._fit_data[plot_index] = fit_data
            self._fit_results[plot_index] = result
            self._fit_method[plot_index] = fit_method

            self.sigFitUpdated.emit(plot_index, fit_data, result,
                                    self.fit_container.current_fit)
            return plot_index, fit_data, result, self.fit_container.current_fit

    def get_fit_data(self, plot_index):
        with self.threadlock:
            if not (0 <= plot_index < self.number_of_plots):
                raise IndexError(
                    'Plot index {0:d} out of bounds.'.format(plot_index))
            return (self._fit_data[plot_index], self._fit_results[plot_index],
                    self._fit_method[plot_index])

    def save_data(self, postfix='', plot_index=0):
        """ Save the data to a file.

        @param str postfix: an additional tag, which will be added to the filename upon save
        @param int plot_index: index of the plot in the range for 0 to 2
        """
        with self.threadlock:
            if not (0 <= plot_index < self.number_of_plots):
                raise IndexError(
                    'Plot index {0:d} out of bounds. Unable to save data.'.
                    format(plot_index))

            # Set the parameters:
            parameters = OrderedDict()
            parameters['user-selected x-limits'] = self._x_limits[plot_index]
            parameters['user-selected y-limits'] = self._y_limits[plot_index]
            parameters['user-selected x-label'] = self._x_label[plot_index]
            parameters['user-selected y-label'] = self._y_label[plot_index]
            parameters['user-selected x-unit'] = self._x_unit[plot_index]
            parameters['user-selected y-unit'] = self._y_unit[plot_index]

            # If there is a postfix then add separating underscore
            if postfix == '':
                file_label = 'qdplot'
            else:
                file_label = postfix

            file_label += '_plot_{0:d}'.format(int(plot_index) + 1)

            # Data labels
            x_label = self._x_label[plot_index] + ' (' + self._x_unit[
                plot_index] + ')'
            y_label = self._y_label[plot_index] + ' (' + self._y_unit[
                plot_index] + ')'

            # prepare the data in a dict or in an OrderedDict:
            data = OrderedDict()
            for data_set in range(len(self._x_data[plot_index])):
                data['{0} set {1:d}'.format(
                    x_label,
                    data_set + 1)] = self._x_data[plot_index][data_set]
                data['{0} set {1:d}'.format(
                    y_label,
                    data_set + 1)] = self._y_data[plot_index][data_set]

            # Prepare the figure to save as a "data thumbnail"
            plt.style.use(self._save_logic.mpl_qd_style)

            fig, ax1 = plt.subplots()

            for data_set in range(len(self._x_data[plot_index])):
                ax1.plot(self._x_data[plot_index][data_set],
                         self._y_data[plot_index][data_set],
                         linestyle=':',
                         linewidth=1)

                if self._fit_data[plot_index] is not None:
                    ax1.plot(self._fit_data[plot_index][data_set][0],
                             self._fit_data[plot_index][data_set][1],
                             color='r',
                             marker='None',
                             linewidth=1.5,
                             label='fit')

            # Do not include fit parameter if there is no fit calculated.
            if self._fit_data[plot_index] is not None:
                # Parameters for the text plot:
                # The position of the text annotation is controlled with the
                # relative offset in x direction and the relative length factor
                # rel_len_fac of the longest entry in one column
                rel_offset = 0.02
                rel_len_fac = 0.011
                entries_per_col = 24

                # do reverse processing to get each entry in a list
                entry_list = self._fit_results[plot_index].split('\n')
                # slice the entry_list in entries_per_col
                chunks = [
                    entry_list[x:x + entries_per_col]
                    for x in range(0, len(entry_list), entries_per_col)
                ]

                is_first_column = True  # first entry should contain header or \n

                for column in chunks:
                    max_length = max(column, key=len)  # get the longest entry
                    column_text = ''

                    for entry in column:
                        column_text += entry.rstrip() + '\n'

                    column_text = column_text[:-1]  # remove the last new line

                    heading = 'Fit results for method: {}'.format(
                        self._fit_method[plot_index]
                    ) if is_first_column else ''
                    column_text = heading + '\n' + column_text

                    ax1.text(1.00 + rel_offset,
                             0.99,
                             column_text,
                             verticalalignment='top',
                             horizontalalignment='left',
                             transform=ax1.transAxes,
                             fontsize=12)

                    # the rel_offset in position of the text is a linear function
                    # which depends on the longest entry in the column
                    rel_offset += rel_len_fac * len(max_length)

                    is_first_column = False

            # set labels, units and limits
            ax1.set_xlabel(x_label)
            ax1.set_ylabel(y_label)

            ax1.set_xlim(self._x_limits[plot_index])
            ax1.set_ylim(self._y_limits[plot_index])

            fig.tight_layout()

            # Call save logic to write everything to file
            file_path = self._save_logic.get_path_for_module(
                module_name='qdplot')
            self._save_logic.save_data(data,
                                       filepath=file_path,
                                       parameters=parameters,
                                       filelabel=file_label,
                                       plotfig=fig,
                                       delimiter='\t')
            plt.close(fig)
            self.log.debug('Data saved to:\n{0}'.format(file_path))

    def get_limits(self, plot_index=0):
        with self.threadlock:
            return self.get_x_limits(plot_index), self.get_y_limits(plot_index)

    def set_limits(self, limits=None, plot_index=0):
        with self.threadlock:
            if limits is None:
                limits = (None, None)
            self.set_x_limits(limits[0], plot_index)
            self.set_y_limits(limits[1], plot_index)

    def get_x_limits(self, plot_index=0):
        """ Get the limits of the x-axis being plotted.

        @param int plot_index: index of the plot in the range from 0 to 2
        @return 2-element list: limits of the x-axis e.g. as [0, 1]
        """
        with self.threadlock:
            if not (0 <= plot_index < self.number_of_plots):
                raise IndexError(
                    'Plot index {0:d} out of bounds.'.format(plot_index))
            return self._x_limits[plot_index]

    def set_x_limits(self, limits=None, plot_index=0):
        """Set the x_limits, to match the data (default) or to a specified new range

        @param float limits: 2-element list containing min and max x-values
        @param int plot_index: index of the plot in the range for 0 to 2
        """
        with self.threadlock:
            if not (0 <= plot_index < self.number_of_plots):
                raise IndexError(
                    'Plot index {0:d} out of bounds.'.format(plot_index))
            if limits is not None:
                if isinstance(limits,
                              (list, tuple, np.ndarray)) and len(limits) > 1:
                    self._x_limits[plot_index] = limits
                else:
                    self.log.error(
                        'limits need to be a list of at least 2 elements but is {}.'
                        ''.format(limits))
                    return
            else:
                range_min = np.min(
                    [np.min(values) for values in self._x_data[plot_index]])
                range_max = np.max(
                    [np.max(values) for values in self._x_data[plot_index]])
                range_range = range_max - range_min
                self._x_limits[plot_index] = [
                    range_min - 0.02 * range_range,
                    range_max + 0.02 * range_range
                ]
            self.sigPlotParamsUpdated.emit(
                plot_index, {'x_limits': self._x_limits[plot_index]})

    def get_y_limits(self, plot_index):
        """ Get the limits of the y-axis being plotted.

        @param int plot_index: index of the plot in the range from 0 to 2
        @return 2-element list: limits of the y-axis e.g. as [0, 1]
        """
        with self.threadlock:
            if not (0 <= plot_index < self.number_of_plots):
                raise IndexError(
                    'Plot index {0:d} out of bounds.'.format(plot_index))
            return self._y_limits[plot_index]

    def set_y_limits(self, limits=None, plot_index=0):
        """Set the y_limits, to match the data (default) or to a specified new range

        @param float limits: 2-element list containing min and max y-values
        @param int plot_index: index of the plot in the range for 0 to 2
        """
        with self.threadlock:
            if not (0 <= plot_index < self.number_of_plots):
                raise IndexError(
                    'Plot index {0:d} out of bounds.'.format(plot_index))
            if limits is not None:
                if isinstance(limits,
                              (list, tuple, np.ndarray)) and len(limits) > 1:
                    self._y_limits[plot_index] = limits
                else:
                    self.log.error(
                        'limits need to be a list of at least 2 elements but is {}.'
                        ''.format(limits))
            else:
                range_min = np.min(
                    [np.min(values) for values in self._y_data[plot_index]])
                range_max = np.max(
                    [np.max(values) for values in self._y_data[plot_index]])
                range_range = range_max - range_min
                self._y_limits[plot_index] = [
                    range_min - 0.02 * range_range,
                    range_max + 0.02 * range_range
                ]
            self.sigPlotParamsUpdated.emit(
                plot_index, {'y_limits': self._y_limits[plot_index]})

    def get_labels(self, plot_index=0):
        with self.threadlock:
            return self.get_x_label(plot_index), self.get_y_label(plot_index)

    def set_labels(self, labels, plot_index=0):
        with self.threadlock:
            self.set_x_label(labels[0], plot_index)
            self.set_y_label(labels[1], plot_index)

    def get_x_label(self, plot_index=0):
        """ Get the label of the x-axis being plotted.

        @param int plot_index: index of the plot in the range from 0 to 2
        @return str: current label of the x-axis
        """
        with self.threadlock:
            if not (0 <= plot_index < self.number_of_plots):
                raise IndexError(
                    'Plot index {0:d} out of bounds.'.format(plot_index))
            return self._x_label[plot_index]

    def set_x_label(self, value, plot_index=0):
        """ Set the label of the x-axis being plotted.

        @param str value: label to be set
        @param int plot_index: index of the plot in the range for 0 to 2
        """
        with self.threadlock:
            if not (0 <= plot_index < self.number_of_plots):
                raise IndexError(
                    'Plot index {0:d} out of bounds.'.format(plot_index))
            self._x_label[plot_index] = str(value)
            self.sigPlotParamsUpdated.emit(
                plot_index, {'x_label': self._x_label[plot_index]})

    def get_y_label(self, plot_index=0):
        """ Get the label of the y-axis being plotted.

        @param int plot_index: index of the plot in the range from 0 to 2
        @return str: current label of the y-axis
        """
        with self.threadlock:
            if not (0 <= plot_index < self.number_of_plots):
                raise IndexError(
                    'Plot index {0:d} out of bounds.'.format(plot_index))
            return self._y_label[plot_index]

    def set_y_label(self, value, plot_index=0):
        """ Set the label of the y-axis being plotted.

        @param str value: label to be set
        @param int plot_index: index of the plot in the range for 0 to 2
        """
        with self.threadlock:
            if not (0 <= plot_index < self.number_of_plots):
                raise IndexError(
                    'Plot index {0:d} out of bounds.'.format(plot_index))
            self._y_label[plot_index] = str(value)
            self.sigPlotParamsUpdated.emit(
                plot_index, {'y_label': self._y_label[plot_index]})

    def get_units(self, plot_index=0):
        with self.threadlock:
            return self.get_x_unit(plot_index), self.get_y_unit(plot_index)

    def set_units(self, units, plot_index=0):
        with self.threadlock:
            self.set_x_unit(units[0], plot_index)
            self.set_y_unit(units[1], plot_index)

    def get_x_unit(self, plot_index=0):
        """ Get the unit of the x-axis being plotted.

        @param int plot_index: index of the plot in the range from 0 to 2
        @return str: current unit of the x-axis
        """
        with self.threadlock:
            if not (0 <= plot_index < self.number_of_plots):
                raise IndexError(
                    'Plot index {0:d} out of bounds.'.format(plot_index))
            return self._x_unit[plot_index]

    def set_x_unit(self, value, plot_index=0):
        """ Set the unit of the x-axis being plotted.

        @param str value: label to be set
        @param int plot_index: index of the plot in the range for 0 to 2
        """
        with self.threadlock:
            if not (0 <= plot_index < self.number_of_plots):
                raise IndexError(
                    'Plot index {0:d} out of bounds.'.format(plot_index))
            self._x_unit[plot_index] = str(value)
            self.sigPlotParamsUpdated.emit(
                plot_index, {'x_unit': self._x_unit[plot_index]})

    def get_y_unit(self, plot_index=0):
        """ Get the unit of the y-axis being plotted.

        @param int plot_index: index of the plot in the range from 0 to 2
        @return str: current unit of the y-axis
        """
        with self.threadlock:
            if not (0 <= plot_index < self.number_of_plots):
                raise IndexError(
                    'Plot index {0:d} out of bounds.'.format(plot_index))
            return self._y_unit[plot_index]

    def set_y_unit(self, value, plot_index=0):
        """ Set the unit of the y-axis being plotted.

        @param str value: label to be set
        @param int plot_index: index of the plot in the range for 0 to 2
        """
        with self.threadlock:
            if not (0 <= plot_index < self.number_of_plots):
                raise IndexError(
                    'Plot index {0:d} out of bounds.'.format(plot_index))
            self._y_unit[plot_index] = str(value)
            self.sigPlotParamsUpdated.emit(
                plot_index, {'y_unit': self._y_unit[plot_index]})

    def clear_old_data(self, plot_index=0):
        """ Get the information, if the previous plots in the windows are kept or not

        @param int plot_index: index of the plot in the range from 0 to 2
        @return bool: are the plots currently in the GUI kept or not
        """
        with self.threadlock:
            if not (0 <= plot_index < self.number_of_plots):
                raise IndexError(
                    'Plot index {0:d} out of bounds.'.format(plot_index))
            return self._clear_old[plot_index]

    @QtCore.Slot(int, dict)
    def update_plot_parameters(self, plot_index, params):
        with self.threadlock:
            if 0 <= plot_index < len(self._x_data):
                if 'x_label' in params:
                    self.set_x_label(params['x_label'], plot_index)
                if 'x_unit' in params:
                    self.set_x_unit(params['x_unit'], plot_index)
                if 'y_label' in params:
                    self.set_y_label(params['y_label'], plot_index)
                if 'y_unit' in params:
                    self.set_y_unit(params['y_unit'], plot_index)
                if 'x_limits' in params:
                    self.set_x_limits(params['x_limits'], plot_index)
                if 'y_limits' in params:
                    self.set_y_limits(params['y_limits'], plot_index)

    @QtCore.Slot(int, bool, bool)
    def update_auto_range(self, plot_index, auto_x, auto_y):
        with self.threadlock:
            if 0 <= plot_index < len(self._x_data):
                if auto_x:
                    self.set_x_limits(plot_index=plot_index)
                if auto_y:
                    self.set_y_limits(plot_index=plot_index)
Exemplo n.º 15
0
class TimeSeriesReaderLogic(GenericLogic):
    """
    This logic module gathers data from a hardware streaming device.

    Example config for copy-paste:

    time_series_reader_logic:
        module.Class: 'time_series_reader_logic.TimeSeriesReaderLogic'
        max_frame_rate: 10  # optional (10Hz by default)
        calc_digital_freq: True  # optional (True by default)
        connect:
            _streamer_con: <streamer_name>
            _savelogic_con: <save_logic_name>
    """
    # declare signals
    sigDataChanged = QtCore.Signal(object, object, object, object)
    sigStatusChanged = QtCore.Signal(bool, bool)
    sigSettingsChanged = QtCore.Signal(dict)
    _sigNextDataFrame = QtCore.Signal()  # internal signal

    # declare connectors
    _streamer_con = Connector(interface='DataInStreamInterface')
    _savelogic_con = Connector(interface='SaveLogic')

    # config options
    _max_frame_rate = ConfigOption('max_frame_rate',
                                   default=10,
                                   missing='warn')
    _calc_digital_freq = ConfigOption('calc_digital_freq',
                                      default=True,
                                      missing='warn')

    # status vars
    _trace_window_size = StatusVar('trace_window_size', default=6)
    _moving_average_width = StatusVar('moving_average_width', default=9)
    _oversampling_factor = StatusVar('oversampling_factor', default=1)
    _data_rate = StatusVar('data_rate', default=50)
    _active_channels = StatusVar('active_channels', default=None)
    _averaged_channels = StatusVar('averaged_channels', default=None)

    def __init__(self, *args, **kwargs):
        """
        """
        super().__init__(*args, **kwargs)

        self._streamer = None
        self._savelogic = None

        # locking for thread safety
        self.threadlock = Mutex()
        self._samples_per_frame = None
        self._stop_requested = True

        # Data arrays
        self._trace_data = None
        self._trace_times = None
        self._trace_data_averaged = None
        self.__moving_filter = None

        # for data recording
        self._recorded_data = None
        self._data_recording_active = False
        self._record_start_time = None
        return

    def on_activate(self):
        """ Initialisation performed during activation of the module.
        """
        # Store references to connected modules
        self._streamer = self._streamer_con()
        self._savelogic = self._savelogic_con()

        debugpy.debug_this_thread()

        # Flag to stop the loop and process variables
        self._stop_requested = True
        self._data_recording_active = False
        self._record_start_time = None

        # Check valid StatusVar
        # active channels
        avail_channels = tuple(ch.name
                               for ch in self._streamer.available_channels)
        if self._active_channels is None:
            if self._streamer.active_channels:
                self._active_channels = tuple(
                    ch.name for ch in self._streamer.active_channels)
            else:
                self._active_channels = avail_channels
        elif any(ch not in avail_channels for ch in self._active_channels):
            self.log.warning(
                'Invalid active channels found in StatusVar. StatusVar ignored.'
            )
            if self._streamer.active_channels:
                self._active_channels = tuple(
                    ch.name for ch in self._streamer.active_channels)
            else:
                self._active_channels = avail_channels

        # averaged channels
        if self._averaged_channels is None:
            self._averaged_channels = self._active_channels
        else:
            self._averaged_channels = tuple(ch
                                            for ch in self._averaged_channels
                                            if ch in self._active_channels)

        # Check for odd moving averaging window
        if self._moving_average_width % 2 == 0:
            self.log.warning(
                'Moving average width ConfigOption must be odd integer number. '
                'Changing value from {0:d} to {1:d}.'
                ''.format(self._moving_average_width,
                          self._moving_average_width + 1))
            self._moving_average_width += 1

        # set settings in streamer hardware
        settings = self.all_settings
        settings['active_channels'] = self._active_channels
        settings['data_rate'] = self._data_rate
        self.configure_settings(**settings)

        # set up internal frame loop connection
        self._sigNextDataFrame.connect(self.acquire_data_block,
                                       QtCore.Qt.QueuedConnection)
        return

    def on_deactivate(self):
        """ De-initialisation performed during deactivation of the module.
        """
        # Stop measurement
        if self.module_state() == 'locked':
            self._stop_reader_wait()

        self._sigNextDataFrame.disconnect()

        # Save status vars
        self._active_channels = self.active_channel_names
        self._data_rate = self.data_rate
        return

    def _init_data_arrays(self):
        window_size = self.trace_window_size_samples
        self._trace_data = np.zeros([
            self.number_of_active_channels,
            window_size + self._moving_average_width // 2
        ])
        self._trace_data_averaged = np.zeros([
            len(self._averaged_channels),
            window_size - self._moving_average_width // 2
        ])
        self._trace_times = np.arange(window_size) / self.data_rate
        self._recorded_data = list()
        return

    @property
    def trace_window_size_samples(self):
        return int(round(self._trace_window_size * self.data_rate))

    @property
    def streamer_constraints(self):
        """
        Retrieve the hardware constrains from the counter device.

        @return SlowCounterConstraints: object with constraints for the counter
        """
        return self._streamer.get_constraints()

    @property
    def data_rate(self):
        return self.sampling_rate / self.oversampling_factor

    @data_rate.setter
    def data_rate(self, val):
        self.configure_settings(data_rate=val)
        return

    @property
    def trace_window_size(self):
        return self._trace_window_size

    @trace_window_size.setter
    def trace_window_size(self, val):
        self.configure_settings(trace_window_size=val)
        return

    @property
    def moving_average_width(self):
        return self._moving_average_width

    @moving_average_width.setter
    def moving_average_width(self, val):
        self.configure_settings(moving_average_width=val)
        return

    @property
    def data_recording_active(self):
        return self._data_recording_active

    @property
    def oversampling_factor(self):
        """

        @return int: Oversampling factor (always >= 1). Value of 1 means no oversampling.
        """
        return self._oversampling_factor

    @oversampling_factor.setter
    def oversampling_factor(self, val):
        """

        @param int val: The oversampling factor to set. Must be >= 1.
        """
        self.configure_settings(oversampling_factor=val)
        return

    @property
    def sampling_rate(self):
        return self._streamer.sample_rate

    @property
    def available_channels(self):
        return self._streamer.available_channels

    @property
    def active_channels(self):
        return self._streamer.active_channels

    @property
    def active_channel_names(self):
        return tuple(ch.name for ch in self._streamer.active_channels)

    @property
    def active_channel_units(self):
        unit_dict = dict()
        for ch in self._streamer.active_channels:
            if self._calc_digital_freq and ch.type == StreamChannelType.DIGITAL:
                unit_dict[ch.name] = 'Hz'
            else:
                unit_dict[ch.name] = ch.unit
        return unit_dict

    @property
    def active_channel_types(self):
        return {ch.name: ch.type for ch in self._streamer.active_channels}

    @property
    def has_active_analog_channels(self):
        return any(ch.type == StreamChannelType.ANALOG
                   for ch in self._streamer.active_channels)

    @property
    def has_active_digital_channels(self):
        return any(ch.type == StreamChannelType.DIGITAL
                   for ch in self._streamer.active_channels)

    @property
    def averaged_channel_names(self):
        return self._averaged_channels

    @property
    def number_of_active_channels(self):
        return self._streamer.number_of_channels

    @property
    def trace_data(self):
        data_offset = self._trace_data.shape[
            1] - self._moving_average_width // 2
        data = {
            ch: self._trace_data[i, :data_offset]
            for i, ch in enumerate(self.active_channel_names)
        }
        return self._trace_times, data

    @property
    def averaged_trace_data(self):
        if not self.averaged_channel_names or self.moving_average_width <= 1:
            return None, None
        data = {
            ch: self._trace_data_averaged[i]
            for i, ch in enumerate(self.averaged_channel_names)
        }
        return self._trace_times[-self._trace_data_averaged.shape[1]:], data

    @property
    def all_settings(self):
        return {
            'oversampling_factor': self.oversampling_factor,
            'active_channels': self.active_channels,
            'averaged_channels': self.averaged_channel_names,
            'moving_average_width': self.moving_average_width,
            'trace_window_size': self.trace_window_size,
            'data_rate': self.data_rate
        }

    @QtCore.Slot(dict)
    def configure_settings(self, settings_dict=None, **kwargs):
        """
        Sets the number of samples to average per data point, i.e. the oversampling factor.
        The counter is stopped first and restarted afterwards.

        @param dict settings_dict: optional, dict containing all parameters to set. Entries will
                                   be overwritten by conflicting kwargs.

        @return dict: The currently configured settings
        """
        if self.data_recording_active:
            self.log.warning(
                'Unable to configure settings while data is being recorded.')
            return self.all_settings

        if settings_dict is None:
            settings_dict = kwargs
        else:
            settings_dict.update(kwargs)

        if not settings_dict:
            return self.all_settings

        # Flag indicating if the stream should be restarted
        restart = self.module_state() == 'locked'
        if restart:
            self._stop_reader_wait()

        with self.threadlock:
            constraints = self.streamer_constraints
            all_ch = tuple(ch.name for ch in self._streamer.available_channels)
            data_rate = self.data_rate
            active_ch = self.active_channel_names

            if 'oversampling_factor' in settings_dict:
                new_val = int(settings_dict['oversampling_factor'])
                if new_val < 1:
                    self.log.error(
                        'Oversampling factor must be integer value >= 1 '
                        '(received: {0:d}).'.format(new_val))
                else:
                    if self.has_active_analog_channels and self.has_active_digital_channels:
                        min_val = constraints.combined_sample_rate.min
                        max_val = constraints.combined_sample_rate.max
                    elif self.has_active_analog_channels:
                        min_val = constraints.analog_sample_rate.min
                        max_val = constraints.analog_sample_rate.max
                    else:
                        min_val = constraints.digital_sample_rate.min
                        max_val = constraints.digital_sample_rate.max
                    if not (min_val <= (new_val * data_rate) <= max_val):
                        if 'data_rate' in settings_dict:
                            self._oversampling_factor = new_val
                        else:
                            self.log.error(
                                'Oversampling factor to set ({0:d}) would cause '
                                'sampling rate outside allowed value range. '
                                'Setting not changed.'.format(new_val))
                    else:
                        self._oversampling_factor = new_val

            if 'moving_average_width' in settings_dict:
                new_val = int(settings_dict['moving_average_width'])
                if new_val < 1:
                    self.log.error(
                        'Moving average width must be integer value >= 1 '
                        '(received: {0:d}).'.format(new_val))
                elif new_val % 2 == 0:
                    new_val += 1
                    self.log.warning(
                        'Moving average window must be odd integer number in order to '
                        'ensure perfect data alignment. Will increase value to {0:d}.'
                        ''.format(new_val))
                if new_val / data_rate > self.trace_window_size:
                    if 'data_rate' in settings_dict or 'trace_window_size' in settings_dict:
                        self._moving_average_width = new_val
                        self.__moving_filter = np.full(
                            shape=self.moving_average_width,
                            fill_value=1.0 / self.moving_average_width)
                    else:
                        self.log.warning(
                            'Moving average width to set ({0:d}) is smaller than the '
                            'trace window size. Will adjust trace window size to '
                            'match.'.format(new_val))
                        self._trace_window_size = float(new_val / data_rate)
                else:
                    self._moving_average_width = new_val
                    self.__moving_filter = np.full(
                        shape=self.moving_average_width,
                        fill_value=1.0 / self.moving_average_width)

            if 'data_rate' in settings_dict:
                new_val = float(settings_dict['data_rate'])
                if new_val < 0:
                    self.log.error('Data rate must be float value > 0.')
                else:
                    if self.has_active_analog_channels and self.has_active_digital_channels:
                        min_val = constraints.combined_sample_rate.min
                        max_val = constraints.combined_sample_rate.max
                    elif self.has_active_analog_channels:
                        min_val = constraints.analog_sample_rate.min
                        max_val = constraints.analog_sample_rate.max
                    else:
                        min_val = constraints.digital_sample_rate.min
                        max_val = constraints.digital_sample_rate.max
                    sample_rate = new_val * self.oversampling_factor
                    if not (min_val <= sample_rate <= max_val):
                        self.log.warning(
                            'Data rate to set ({0:.3e}Hz) would cause sampling rate '
                            'outside allowed value range. Will clip data rate to '
                            'boundaries.'.format(new_val))
                        if sample_rate > max_val:
                            new_val = max_val / self.oversampling_factor
                        elif sample_rate < min_val:
                            new_val = min_val / self.oversampling_factor

                    data_rate = new_val
                    if self.moving_average_width / data_rate > self.trace_window_size:
                        if 'trace_window_size' not in settings_dict:
                            self.log.warning(
                                'Data rate to set ({0:.3e}Hz) would cause too few '
                                'data points within the trace window. Adjusting window'
                                ' size.'.format(new_val))
                            self._trace_window_size = self.moving_average_width / data_rate

            if 'trace_window_size' in settings_dict:
                new_val = float(settings_dict['trace_window_size'])
                if new_val < 0:
                    self.log.error(
                        'Trace window size must be float value > 0.')
                else:
                    # Round window to match data rate
                    data_points = int(round(new_val * data_rate))
                    new_val = data_points / data_rate
                    # Check if enough points are present
                    if data_points < self.moving_average_width:
                        self.log.warning(
                            'Requested trace_window_size ({0:.3e}s) would have too '
                            'few points for moving average. Adjusting window size.'
                            ''.format(new_val))
                        new_val = self.moving_average_width / data_rate
                    self._trace_window_size = new_val

            if 'active_channels' in settings_dict:
                new_val = tuple(settings_dict['active_channels'])
                if any(ch not in all_ch for ch in new_val):
                    self.log.error('Invalid channel found to set active.')
                else:
                    active_ch = new_val

            if 'averaged_channels' in settings_dict:
                new_val = tuple(ch for ch in settings_dict['averaged_channels']
                                if ch in active_ch)
                if any(ch not in all_ch for ch in new_val):
                    self.log.error(
                        'Invalid channel found to set activate moving average for.'
                    )
                else:
                    self._averaged_channels = new_val

            # Apply settings to hardware if needed
            self._streamer.configure(sample_rate=data_rate *
                                     self.oversampling_factor,
                                     streaming_mode=StreamingMode.CONTINUOUS,
                                     active_channels=active_ch,
                                     buffer_size=10000000,
                                     use_circular_buffer=True)

            # update actually set values
            self._averaged_channels = tuple(ch
                                            for ch in self._averaged_channels
                                            if ch in self.active_channel_names)

            self._samples_per_frame = int(
                round(self.data_rate / self._max_frame_rate))
            self._init_data_arrays()
            settings = self.all_settings
            self.sigSettingsChanged.emit(settings)
            if not restart:
                self.sigDataChanged.emit(*self.trace_data,
                                         *self.averaged_trace_data)
        if restart:
            self.start_reading()
        return settings

    @QtCore.Slot()
    def start_reading(self):
        """
        Start data acquisition loop.

        @return error: 0 is OK, -1 is error
        """
        with self.threadlock:
            # Lock module
            if self.module_state() == 'locked':
                self.log.warning(
                    'Data acquisition already running. "start_reading" call ignored.'
                )
                self.sigStatusChanged.emit(True, self._data_recording_active)
                return 0

            self.module_state.lock()
            self._stop_requested = False

            self.sigStatusChanged.emit(True, self._data_recording_active)

            # # Configure streaming device
            # curr_settings = self._streamer.configure(sample_rate=self.sampling_rate,
            #                                          streaming_mode=StreamingMode.CONTINUOUS,
            #                                          active_channels=self._active_channels,
            #                                          buffer_size=10000000,
            #                                          use_circular_buffer=True)
            # # update actually set values
            # self._active_channels = tuple(ch.name for ch in curr_settings['active_channels'])
            # self._averaged_channels = tuple(
            #     ch for ch in self._averaged_channels if ch in self._active_channels)
            # self._data_rate = curr_settings['sample_rate'] / self._oversampling_factor
            #
            # self._samples_per_frame = int(round(self._data_rate / self._max_frame_rate))
            # self._init_data_arrays()
            # settings = self.all_settings
            # self.sigSettingsChanged.emit(settings)

            if self._data_recording_active:
                self._record_start_time = dt.datetime.now()
                self._recorded_data = list()

            if self._streamer.start_stream() < 0:
                self.log.error(
                    'Error while starting streaming device data acquisition.')
                self._stop_requested = True
                self._sigNextDataFrame.emit()
                return -1

            self._sigNextDataFrame.emit()
        return 0

    @QtCore.Slot()
    def stop_reading(self):
        """
        Send a request to stop counting.

        @return int: error code (0: OK, -1: error)
        """
        with self.threadlock:
            if self.module_state() == 'locked':
                self._stop_requested = True
        return 0

    @QtCore.Slot()
    def acquire_data_block(self):
        """
        This method gets the available data from the hardware.

        It runs repeatedly by being connected to a QTimer timeout signal.
        """
        with self.threadlock:
            if self.module_state() == 'locked':
                # check for break condition
                if self._stop_requested:
                    # terminate the hardware streaming
                    if self._streamer.stop_stream() < 0:
                        self.log.error(
                            'Error while trying to stop streaming device data acquisition.'
                        )
                    if self._data_recording_active:
                        self._save_recorded_data(to_file=True,
                                                 save_figure=True)
                        self._recorded_data = list()
                    self._data_recording_active = False
                    self.module_state.unlock()
                    self.sigStatusChanged.emit(False, False)
                    return

                samples_to_read = max(
                    (self._streamer.available_samples //
                     self._oversampling_factor) * self._oversampling_factor,
                    self._samples_per_frame * self._oversampling_factor)
                if samples_to_read < 1:
                    self._sigNextDataFrame.emit()
                    return

                # read the current counter values
                data = self._streamer.read_data(
                    number_of_samples=samples_to_read)
                if data.shape[1] != samples_to_read:
                    self.log.error('Reading data from streamer went wrong; '
                                   'killing the stream with next data frame.')
                    self._stop_requested = True
                    self._sigNextDataFrame.emit()
                    return

                # Process data
                self._process_trace_data(data)

                # Emit update signal
                self.sigDataChanged.emit(*self.trace_data,
                                         *self.averaged_trace_data)
                self._sigNextDataFrame.emit()
        return

    def _process_trace_data(self, data):
        """
        Processes raw data from the streaming device
        """
        # Down-sample and average according to oversampling factor
        if self.oversampling_factor > 1:
            if data.shape[1] % self.oversampling_factor != 0:
                self.log.error(
                    'Number of samples per channel not an integer multiple of the '
                    'oversampling factor.')
                return -1
            tmp = data.reshape(
                (data.shape[0], data.shape[1] // self.oversampling_factor,
                 self.oversampling_factor))
            data = np.mean(tmp, axis=2)

        digital_channels = [
            c for c, typ in self.active_channel_types.items()
            if typ == StreamChannelType.DIGITAL
        ]
        # Convert digital event count numbers into frequencies according to ConfigOption
        if self._calc_digital_freq and digital_channels:
            data[:len(digital_channels)] *= self.sampling_rate

        # Append data to save if necessary
        if self._data_recording_active:
            self._recorded_data.append(data.copy())

        data = data[:, -self._trace_data.shape[1]:]
        new_samples = data.shape[1]

        # Roll data array to have a continuously running time trace
        self._trace_data = np.roll(self._trace_data, -new_samples, axis=1)
        # Insert new data
        self._trace_data[:, -new_samples:] = data

        # Calculate moving average by using numpy.convolve with a normalized uniform filter
        if self.moving_average_width > 1 and self.averaged_channel_names:
            # Only convolve the new data and roll the previously calculated moving average
            self._trace_data_averaged = np.roll(self._trace_data_averaged,
                                                -new_samples,
                                                axis=1)
            offset = new_samples + len(self.__moving_filter) - 1
            for i, ch in enumerate(self.averaged_channel_names):
                data_index = self.active_channel_names.index(ch)
                self._trace_data_averaged[i, -new_samples:] = np.convolve(
                    self._trace_data[data_index, -offset:],
                    self.__moving_filter,
                    mode='valid')
        return

    @QtCore.Slot()
    def start_recording(self):
        """
        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 int: Error code (0: OK, -1: Error)
        """
        with self.threadlock:
            if self._data_recording_active:
                self.sigStatusChanged.emit(self.module_state() == 'locked',
                                           True)
                return -1

            self._data_recording_active = True
            if self.module_state() == 'locked':
                self._recorded_data = list()
                self._record_start_time = dt.datetime.now()
                self.sigStatusChanged.emit(True, True)
            else:
                self.start_reading()
        return 0

    @QtCore.Slot()
    def stop_recording(self):
        """
        Stop the accumulative data recording and save data to file. Will not stop the data stream.
        Ignored if stream reading is inactive (module is in idle state).

        @return int: Error code (0: OK, -1: Error)
        """
        with self.threadlock:
            if not self._data_recording_active:
                self.sigStatusChanged.emit(self.module_state() == 'locked',
                                           False)
                return 0

            self._data_recording_active = False
            if self.module_state() == 'locked':
                self._save_recorded_data(to_file=True, save_figure=True)
                self._recorded_data = list()
                self.sigStatusChanged.emit(True, False)
        return 0

    def _save_recorded_data(self, to_file=True, name_tag='', save_figure=True):
        """ 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 name_tag: an additional tag, which will be added to the filename upon save
        @param bool save_figure: select whether png and pdf should be saved

        @return dict parameters: Dictionary which contains the saving parameters
        """
        if not self._recorded_data:
            self.log.error('No data has been recorded. Save to file failed.')
            return np.empty(0), dict()

        data_arr = np.concatenate(self._recorded_data, axis=1)
        if data_arr.size == 0:
            self.log.error('No data has been recorded. Save to file failed.')
            return np.empty(0), dict()

        saving_stop_time = self._record_start_time + dt.timedelta(
            seconds=data_arr.shape[1] / self.data_rate)

        # write the parameters:
        parameters = dict()
        parameters['Start recoding time'] = self._record_start_time.strftime(
            '%d.%m.%Y, %H:%M:%S.%f')
        parameters['Stop recoding time'] = saving_stop_time.strftime(
            '%d.%m.%Y, %H:%M:%S.%f')
        parameters['Data rate (Hz)'] = self.data_rate
        parameters['Oversampling factor (samples)'] = self.oversampling_factor
        parameters['Sampling rate (Hz)'] = self.sampling_rate

        if to_file:
            # If there is a postfix then add separating underscore
            filelabel = 'data_trace_{0}'.format(
                name_tag) if name_tag else 'data_trace'

            # prepare the data in a dict:
            header = ', '.join(
                '{0} ({1})'.format(ch, unit)
                for ch, unit in self.active_channel_units.items())

            data = {header: data_arr.transpose()}
            filepath = self._savelogic.get_path_for_module(
                module_name='TimeSeriesReader')
            set_of_units = set(self.active_channel_units.values())
            unit_list = tuple(self.active_channel_units)
            y_unit = 'arb.u.'
            occurrences = 0
            for unit in set_of_units:
                count = unit_list.count(unit)
                if count > occurrences:
                    occurrences = count
                    y_unit = unit

            fig = self._draw_figure(data_arr, self.data_rate,
                                    y_unit) if save_figure else None

            self._savelogic.save_data(data=data,
                                      filepath=filepath,
                                      parameters=parameters,
                                      filelabel=filelabel,
                                      plotfig=fig,
                                      delimiter='\t',
                                      timestamp=saving_stop_time)
            self.log.info('Time series saved to: {0}'.format(filepath))
        return data_arr, parameters

    def _draw_figure(self, data, timebase, y_unit):
        """ 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.
        """
        # Use qudi style
        plt.style.use(self._savelogic.mpl_qd_style)

        # Create figure and scale data
        max_abs_value = ScaledFloat(max(data.max(), np.abs(data.min())))
        time_data = np.arange(data.shape[1]) / timebase
        fig, ax = plt.subplots()
        if max_abs_value.scale:
            ax.plot(time_data,
                    data.transpose() / max_abs_value.scale_val,
                    linestyle=':',
                    linewidth=0.5)
        else:
            ax.plot(time_data, data.transpose(), linestyle=':', linewidth=0.5)
        ax.set_xlabel('Time (s)')
        ax.set_ylabel('Signal ({0}{1})'.format(max_abs_value.scale, y_unit))
        return fig

    @QtCore.Slot()
    def save_trace_snapshot(self, to_file=True, name_tag='', save_figure=True):
        """
        The currently displayed data trace will be saved.

        @param bool to_file: optional, whether data should be saved to a text file
        @param str name_tag: optional, additional description that will be appended to the file name
        @param bool save_figure: optional, whether a data thumbnail figure should be saved

        @return dict, dict: Data which was saved, Experiment parameters

        This method saves the already displayed counts to file and does not accumulate them.
        """
        with self.threadlock:
            timestamp = dt.datetime.now()

            # write the parameters:
            parameters = dict()
            parameters['Time stamp'] = timestamp.strftime(
                '%d.%m.%Y, %H:%M:%S.%f')
            parameters['Data rate (Hz)'] = self.data_rate
            parameters[
                'Oversampling factor (samples)'] = self.oversampling_factor
            parameters['Sampling rate (Hz)'] = self.sampling_rate

            header = ', '.join(
                '{0} ({1})'.format(ch, unit)
                for ch, unit in self.active_channel_units.items())
            data_offset = self._trace_data.shape[
                1] - self.moving_average_width // 2
            data = {header: self._trace_data[:, :data_offset].transpose()}

            if to_file:
                filepath = self._savelogic.get_path_for_module(
                    module_name='TimeSeriesReader')
                filelabel = 'data_trace_snapshot_{0}'.format(
                    name_tag) if name_tag else 'data_trace_snapshot'
                self._savelogic.save_data(data=data,
                                          filepath=filepath,
                                          parameters=parameters,
                                          filelabel=filelabel,
                                          timestamp=timestamp,
                                          delimiter='\t')
                self.log.info(
                    'Time series snapshot saved to: {0}'.format(filepath))
        return data, parameters

    def _stop_reader_wait(self):
        """
        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
        """
        with self.threadlock:
            self._stop_requested = True
            # terminate the hardware streaming
            if self._streamer.stop_stream() < 0:
                self.log.error(
                    'Error while trying to stop streaming device data acquisition.'
                )
            if self._data_recording_active:
                self._save_recorded_data(to_file=True, save_figure=True)
                self._recorded_data = list()
            self._data_recording_active = False
            self.module_state.unlock()
            self.sigStatusChanged.emit(False, False)
        return 0
Exemplo n.º 16
0
class OptimizerLogic(GenericLogic):
    """This is the Logic class for optimizing scanner position on bright features.
    """

    # declare connectors
    confocalscanner1 = Connector(interface='ConfocalScannerInterface')
    fitlogic = Connector(interface='FitLogic')

    # Config options
    # STEPZ optimisation strategy
    stepz_stepsize = ConfigOption('stepz_stepsize',
                                  default=5e-8)  # Step size (m)
    stepz_maxiter = ConfigOption('stepz_maxiter',
                                 default=20)  # Max iterations (#)
    stepz_oversampling = ConfigOption('stepz_oversampling',
                                      default=5)  # Oversampling per point (#)
    stepz_tolerance = ConfigOption(
        'stepz_tolerance', default=1e-3)  # Tolerance (fraction of counts)

    # 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)

    # 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)

        self.stop_requested = threading.Event()
        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.confocalscanner1()
        self._fit_logic = self.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

        self.stop_requested.clear()

        ###########################
        # 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()
        }

        self.sigRefocusStarted.connect(self._run_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)
        """
        self.stop_requested.set()
        self.sigRefocusStarted.disconnect()
        return 0

    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.module_state() == '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:
            raise ValueError('initial_pos must be an array-like or None')

        # 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.stop_requested.clear()

        # Call _run_refocus() in the optimizer logic thread.
        self.sigRefocusStarted.emit(caller_tag)

    def stop_refocus(self):
        """Stops refocus at soonest available time"""
        self.stop_requested.set()

    def _run_refocus(self, tag):
        """Run the refocus. Slot for sigRefocusStarted.
        """
        self.module_state.lock()
        try:
            self.start_scanner()
            for step in self.optimization_sequence:
                # Run each step in the optimization sequence
                if self.stop_requested.is_set():
                    return

                if step == 'XY':
                    # Do XY refocus scan
                    self._initialize_xy_refocus_image()
                    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]
                    ])
                    for line in range(len(self._Y_values)):
                        # Scan lines
                        if self.stop_requested.is_set():
                            return

                        self._refocus_xy_line(line)

                    # Fit and set positions
                    self._set_optimized_xy_from_fit()

                elif step == 'Z':
                    # Do Z refocus scan
                    self._initialize_z_refocus_image()
                    self.do_z_optimization()

                elif step == 'STEPZ':
                    # Do stepwise Z refocus (better for open-loop scanning)
                    self.do_stepwise_z()

                else:
                    self.log.error(
                        'Unsupported optimization step {}'.format(step))

                # Yield to other threads
                time.sleep(0)

        finally:
            self.finish_refocus()
            self.module_state.unlock()

    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):
            raise OptimizerLogicError(
                'Error moving to starting position of optimizer')

        time.sleep(self.hw_settle_time)

    def _refocus_xy_line(self, line_num):
        """Scanning a line of the xy optimization image.
        @param line_num: Line number (0->max_Y)
        """
        n_ch = len(self._scanning_device.get_scanner_axes())

        lsx = self.xy_refocus_image[line_num, :, 0]
        lsy = self.xy_refocus_image[line_num, :, 1]
        lsz = self.xy_refocus_image[line_num, :, 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):
            raise OptimizerLogicError('XY scan failed during optimization')

        lsx = self._return_X_values
        lsy = self.xy_refocus_image[line_num, 0, 1] * np.ones(lsx.shape)
        lsz = self.xy_refocus_image[line_num, 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):
            raise OptimizerLogicError('XY scan failed during optimization')

        s_ch = len(self.get_scanner_count_channels())
        self.xy_refocus_image[line_num, :, 3:3 + s_ch] = line_counts
        self.sigImageUpdated.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 + self.opt_channel].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)

        if result_2D_gaus.success is False:
            self.log.error('XY optimisation failed: could not fit Gaussian')
            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.
        else:
            optim_x = result_2D_gaus.best_values['center_x']
            optim_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']

            # Clip to optimizer range
            self.optim_pos_x = np.clip(optim_x, np.min(self._X_values),
                                       np.max(self._X_values))
            self.optim_pos_y = np.clip(optim_y, np.min(self._Y_values),
                                       np.max(self._Y_values))

        # emit image updated signal so crosshair can be updated from this fit
        self.sigImageUpdated.emit()

    def do_z_optimization(self):
        """ Do the z axis optimization."""
        # z scaning
        self._scan_z_line_refocus()

        # 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 = {
                '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('Z optimisation failed: could not fit Gaussian')
            self.optim_pos_z = self._initial_pos_z
            self.optim_sigma_z = 0.
        else:
            # Clip to optimizer range
            optim_z = result.best_values['center']
            self.optim_pos_z = np.clip(optim_z, np.min(self._zimage_Z_values),
                                       np.max(self._zimage_Z_values))
            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)

        self.sigImageUpdated.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 '
                      '({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_refocus(self):
        """Scans the z line for Gaussian refocus."""

        # Moves to the start value of the z-scan
        self._move_to_start_pos(
            [self.optim_pos_x, self.optim_pos_y, self._zimage_Z_values[0]])

        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):
            raise OptimizerLogicError(
                'Z scan went wrong, killing the scanner.')

        # 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
            self._move_to_start_pos([
                self.optim_pos_x + self.surface_subtr_scan_offset,
                self.optim_pos_y, self._zimage_Z_values[0]
            ])

            # 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):
                raise OptimizerLogicError(
                    'The scan went wrong, killing the scanner.')

            # 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.
        """
        clock_status = self._scanning_device.set_up_scanner_clock(
            clock_frequency=self._clock_frequency)
        if clock_status < 0:
            raise OptimizerLogicError('Error setting up scan clock')

        scanner_status = self._scanning_device.set_up_scanner()
        if scanner_status < 0:
            raise OptimizerLogicError('Error setting up scan clock')

    def kill_scanner(self):
        """Closing the scanner device.

        @return int: error code (0:OK, -1:error)
        """
        rv = self._scanning_device.close_scanner()
        rv2 = self._scanning_device.close_scanner_clock()
        return rv + rv2

    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)

    def do_stepwise_z(self):
        """Run stepwise Z optimisation"""
        stepsize = self.stepz_stepsize
        max_steps = self.stepz_maxiter
        oversampling = self.stepz_oversampling
        tolerance = self.stepz_tolerance

        for i in range(max_steps):
            z_values = np.concatenate((np.full(oversampling, self.optim_pos_z),
                                       np.full(oversampling,
                                               self.optim_pos_z + stepsize)))
            z_counts = self._scan_z_line(z_values)[:, self.opt_channel]
            z_counts_1 = np.median(z_counts[:oversampling])
            z_counts_2 = np.median(z_counts[oversampling:])

            if np.abs(z_counts_1 - z_counts_2) < (tolerance * z_counts_1):
                self.optim_pos_z = np.mean(
                    (self.optim_pos_z, self.optim_pos_z + stepsize))
                self.log.debug(
                    'STEPZ hit target after {} iterations'.format(i))
                break

            if z_counts_1 < z_counts_2:
                self.optim_pos_z = self.optim_pos_z + stepsize
            else:
                self.optim_pos_z = self.optim_pos_z - stepsize

        self.log.debug(
            'STEPZ finished with {} counts residual'.format(z_counts_1 -
                                                            z_counts_2))

    def _scan_z_line(self, z_values):
        """ Scan in Z along values provided in z_values

        @param z_values: 1D Numpy array of Z values
        @return np.array: 1D array of count values
        """
        n_ch = len(self._scanning_device.get_scanner_axes())

        # defining trace of positions for z-refocus
        scan_z_line = z_values
        scan_x_line = self.optim_pos_x * np.ones(z_values.shape)
        scan_y_line = self.optim_pos_y * np.ones(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):
            raise OptimizerLogicError(
                'Z scan went wrong, killing the scanner.')

        return line_counts
Exemplo n.º 17
0
class HBridge(Base, SwitchInterface):
    """ Methods to control slow laser switching devices.

    Example config for copy-paste:

    h_bridge_switch:
        module.Class: 'switches.hbridge.HBridge'
        interface: 'ASRL1::INSTR'
        name: 'HBridge Switch'  # optional
        switch_time: 0.5  # optional
        remember_states: False  # optional
        switches:               # optional
            One: ['Spectrometer', 'APD']
            Two: ['Spectrometer', 'APD']
            Three: ['Spectrometer', 'APD']
            Four: ['Spectrometer', 'APD']
    """

    # ConfigOptions

    # customize all 4 available switches in config. Each switch needs a tuple of 2 state names.
    _switches = ConfigOption(
        name='switches',
        default={str(ii): ('Off', 'On')
                 for ii in range(1, 5)},
        missing='nothing')
    # optional name of the hardware
    _hardware_name = ConfigOption(name='name',
                                  default='HBridge Switch',
                                  missing='nothing')
    # if remember_states is True the last state will be restored at reloading of the module
    _remember_states = ConfigOption(name='remember_states',
                                    default=False,
                                    missing='nothing')
    # switch_time to wait after setting the states for the solenoids to react
    _switch_time = ConfigOption(name='switch_time',
                                default=0.5,
                                missing='nothing')
    # name of the serial interface where the hardware is connected.
    # Use e.g. the Keysight IO connections expert to find the device.
    serial_interface = ConfigOption('interface',
                                    'ASRL1::INSTR',
                                    missing='error')

    # StatusVariable for remembering the last state of the hardware
    _states = StatusVar(name='states', default=None)

    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self.lock = RecursiveMutex()
        self._resource_manager = None
        self._instrument = None

    def on_activate(self):
        """ Prepare module, connect to hardware.
        """
        self._switches = self._chk_refine_available_switches(self._switches)

        self._resource_manager = visa.ResourceManager()
        self._instrument = self._resource_manager.open_resource(
            self.serial_interface,
            baud_rate=9600,
            write_termination='\r\n',
            read_termination='\r\n',
            timeout=10,
            send_end=True)

        # reset states if requested, otherwise use the saved states
        if self._remember_states and isinstance(self._states, dict) and \
                set(self._states) == set(self._switches):
            self._states = {
                switch: self._states[switch]
                for switch in self._switches
            }
            self.states = self._states
        else:
            self._states = dict()
            self.states = {
                switch: states[0]
                for switch, states in self._switches.items()
            }

    def on_deactivate(self):
        """ Disconnect from hardware on deactivation.
        """
        self._instrument.close()
        self._resource_manager.close()

    @property
    def name(self):
        """ Name of the hardware as string.

        @return str: The name of the hardware
        """
        return self._hardware_name

    @property
    def available_states(self):
        """ Names of the states as a dict of tuples.

        The keys contain the names for each of the switches. The values are tuples of strings
        representing the ordered names of available states for each switch.

        @return dict: Available states per switch in the form {"switch": ("state1", "state2")}
        """
        return self._switches.copy()

    @property
    def states(self):
        """ The current states the hardware is in as state dictionary with switch names as keys and
        state names as values.

        @return dict: All the current states of the switches in the form {"switch": "state"}
        """
        with self.lock:
            binary = tuple(
                int(pos == '1')
                for pos in self.inst.ask('STATUS').strip().split())
            avail_states = self.available_states
            self._states = {
                switch: states[binary[index]]
                for index, (switch, states) in enumerate(avail_states.items())
            }
        return self._states.copy()

    @states.setter
    def states(self, state_dict):
        """ The setter for the states of the hardware.

        The states of the system can be set by specifying a dict that has the switch names as keys
        and the names of the states as values.

        @param dict state_dict: state dict of the form {"switch": "state"}
        """
        assert isinstance(state_dict, dict), \
            f'Property "state" must be dict type. Received: {type(state_dict)}'

        if state_dict:
            with self.lock:
                for switch, state in state_dict.items():
                    self.set_state(switch, state)

    def get_state(self, switch):
        """ Query state of single switch by name

        @param str switch: name of the switch to query the state for
        @return str: The current switch state
        """
        assert switch in self.available_states, 'Invalid switch name "{0}"'.format(
            switch)
        return self.states[switch]

    def set_state(self, switch, state):
        """ Query state of single switch by name

        @param str switch: name of the switch to change
        @param str state: name of the state to set
        """
        avail_states = self.available_states
        assert switch in avail_states, f'Invalid switch name: "{switch}"'
        assert state in avail_states[
            switch], f'Invalid state name "{state}" for switch "{switch}"'

        with self.lock:
            switch_index = self.switch_names.index(switch) + 1
            state_index = avail_states[switch].index(state) + 1
            cmd = 'P{0:d}={1:d}'.format(switch_index, state_index)
            answer = self._instrument.ask(cmd)
            assert answer == cmd, \
                f'setting of state "{state}" in switch "{switch}" failed with return value "{answer}"'
            time.sleep(self._switch_time)

    @staticmethod
    def _chk_refine_available_switches(switch_dict):
        """ See SwitchInterface class for details

        @param dict switch_dict:
        @return dict:
        """
        refined = super()._chk_refine_available_switches(switch_dict)
        assert len(
            refined
        ) == 4, 'Exactly 4 switches or None must be specified in config'
        assert all(len(s) == 2 for s in
                   refined.values()), 'Switches can only take exactly 2 states'
        return refined
Exemplo n.º 18
0
class DigitalSwitchNI(Base, SwitchInterface):
    """ This class enables to control a switch via the NI card.
    Control external hardware by the output of the digital channels of a NI card.

    Example config for copy-paste:

    digital_switch_ni:
        module.Class: 'switches.digital_switch_ni.DigitalSwitchNI'
        channel: '/Dev1/port0/line30:31'  # optional
        name: 'My Switch Hardware Name'  # optional
        switch_time: 0.1
        remember_states: True
        switches:                       # optional
            One: ['Low', 'High']
            Two: ['Off', 'On']
    """
    # Channels of the NI Card to be used for switching.
    # Can either be a single channel or multiple lines.
    _channel = ConfigOption(name='channel', default='/Dev1/port0/line31', missing='warn')
    # switch_time to wait after setting the states for the connected hardware to react
    _switch_time = ConfigOption(name='switch_time', default=0.1, missing='nothing')
    # optionally customize all switches in config. Each switch needs a tuple of 2 state names.
    # If used, you must specify as many switches as you have specified channels
    _switches = ConfigOption(name='switches', default=None, missing='nothing')
    # optional name of the hardware
    _hardware_name = ConfigOption(name='name', default=None, missing='nothing')
    # if remember_states is True the last state will be restored at reloading of the module
    _remember_states = ConfigOption(name='remember_states', default=True, missing='nothing')
    # if inverted is True, first entry in switches is "high" and second is "low"
    _inverted_states = ConfigOption(name='inverted_states', default=False, missing='nothing')

    _states = StatusVar(name='states', default=None)

    def __init__(self, *args, **kwargs):
        """ Create the digital switch output control module
        """
        super().__init__(*args, **kwargs)
        self.lock = RecursiveMutex()

        self._channels = tuple()

    def on_activate(self):
        """ Prepare module, connect to hardware.
        The number of switches is automatically determined from the ConfigOption channel:
            /Dev1/port0/line31 lead to 1 switch
            /Dev1/port0/line29:31 leads to 3 switches
        """
        # Determine DO lines to use. This defines the number of switches for this module.
        assert isinstance(self._channel, str), 'ConfigOption "channel" must be str type'
        match = re.match(r'(.*?dev\d/port\d/line)(\d+)(?::(\d+))?', self._channel, re.IGNORECASE)
        match_pfi = re.match(r'(.*?dev\d/pfi)(\d+)(?::(\d+))?', self._channel, re.IGNORECASE)
        assert match is not None or match_pfi is not None, \
            'channel string invalid. Valid examples: "/Dev1/port0/line29:31", "/Dev1/PFI10:13"'
        if match is not None and match.groups()[2] is None:
            self._channels = (match.group(),)
        elif match_pfi is not None and match_pfi.groups()[2] is None:
            self._channels = (match_pfi.group(),)
        elif match is not None:
            first, last = sorted(int(ch) for ch in match.groups()[1:])
            prefix = match.groups()[0]
            self._channels = tuple('{0}{1:d}'.format(prefix, ii) for ii in range(first, last + 1))
        elif match_pfi is not None:
            first, last = sorted(int(ch) for ch in match_pfi.groups()[1:])
            prefix = match_pfi.groups()[0]
            self._channels = tuple('{0}{1:d}'.format(prefix, ii) for ii in range(first, last + 1))
        else:
            self.log.error(f'channel does not have the required format: "{self._channel}"')

        # Determine available switches and states
        if self._switches is None:
            self._switches = {str(ii): ('Off', 'On') for ii in range(1, len(self._channels) + 1)}
        self._switches = self._chk_refine_available_switches(self._switches)

        if self._hardware_name is None:
            self._hardware_name = 'NICard' + str(self._channel).replace('/', ' ')

        # reset states if requested, otherwise use the saved states
        if self._remember_states and isinstance(self._states, dict) and \
                set(self._states) == set(self._switches):
            self._states = {switch: self._states[switch] for switch in self._switches}
            self.states = self._states
        else:
            self._states = dict()
            self.states = {switch: states[0] for switch, states in self._switches.items()}

    def on_deactivate(self):
        """ Disconnect from hardware on deactivation.
        """
        pass

    @property
    def name(self):
        """ Name of the hardware as string.

        The name can either be defined as ConfigOption (name) or it defaults to the name of the hardware module.

        @return str: The name of the hardware
        """
        return self._hardware_name

    @property
    def available_states(self):
        """ Names of the states as a dict of tuples.

        The keys contain the names for each of the switches. The values are tuples of strings
        representing the ordered names of available states for each switch.

        @return dict: Available states per switch in the form {"switch": ("state1", "state2")}
        """
        return self._switches.copy()

    @property
    def states(self):
        """ The current states the hardware is in as state dictionary with switch names as keys and
        state names as values.

        @return dict: All the current states of the switches in the form {"switch": "state"}
        """
        return self._states.copy()

    @states.setter
    def states(self, state_dict):
        """ The setter for the states of the hardware.

        The states of the system can be set by specifying a dict that has the switch names as keys
        and the names of the states as values.

        @param dict state_dict: state dict of the form {"switch": "state"}
        """
        avail_states = self.available_states
        assert isinstance(state_dict,
                          dict), f'Property "state" must be dict type. Received: {type(state_dict)}'
        assert all(switch in avail_states for switch in
                   state_dict), f'Invalid switch name(s) encountered: {tuple(state_dict)}'
        assert all(isinstance(state, str) for state in
                   state_dict.values()), f'Invalid switch state(s) encountered: {tuple(state_dict.values())}'

        if state_dict:
            with self.lock:
                new_states = self._states.copy()
                new_states.update(state_dict)
                with nidaqmx.Task('NISwitchTask' + self.name.replace(':', ' ')) as switch_task:
                    binary = list()
                    for channel_index, (switch, state) in enumerate(new_states.items()):
                        switch_task.do_channels.add_do_chan(self._channels[channel_index])
                        if self._inverted_states:
                            binary.append(avail_states[switch][0] == state)
                        else:
                            binary.append(avail_states[switch][0] != state)
                    switch_task.write(binary, auto_start=True)
                    time.sleep(self._switch_time)
                    self._states = new_states

    def get_state(self, switch):
        """ Query state of single switch by name

        @param str switch: name of the switch to query the state for
        @return str: The current switch state
        """
        assert switch in self._states, f'Invalid switch name: "{switch}"'
        return self._states[switch]

    def set_state(self, switch, state):
        """ Query state of single switch by name

        @param str switch: name of the switch to change
        @param str state: name of the state to set
        """
        self.states = {switch: state}

    def _chk_refine_available_switches(self, switch_dict):
        """ See SwitchInterface class for details

        @param dict switch_dict:
        @return dict:
        """
        refined = super()._chk_refine_available_switches(switch_dict)
        num = len(self._channels)
        assert len(refined) == num, f'Exactly {num} switches or None must be specified in config'
        assert all(len(s) == 2 for s in refined.values()), 'Switches can only take exactly 2 states'
        return refined
Exemplo n.º 19
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()

    # 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.confocalscanner1()
        self._save_logic = self.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.module_state() == 'locked'
                or self._scanning_device.module_state() == '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.module_state() == '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.module_state.lock()
        self._scanning_device.module_state.lock()

        returnvalue = self._scanning_device.set_up_scanner_clock(
            clock_frequency=self._clock_frequency)
        if returnvalue < 0:
            self._scanning_device.module_state.unlock()
            self.module_state.unlock()
            self.set_position('scanner')
            return -1

        returnvalue = self._scanning_device.set_up_scanner()
        if returnvalue < 0:
            self._scanning_device.module_state.unlock()
            self.module_state.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.module_state() == '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.module_state.can('unlock'):
                self.module_state.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.module_state.can('unlock'):
                self._scanning_device.module_state.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
Exemplo n.º 20
0
class TemperatureMonitorLogic(GenericLogic):
    """ Logic module agreggating multiple hardware switches.
    """

    # waiting time between queries im milliseconds
    tm = Connector(interface='ProcessInterface')
    savelogic = Connector(interface='SaveLogic')

    queryInterval = ConfigOption('query_interval', 1000)
    queryIntervalLowerLim = ConfigOption('query_interval_lower_lim', 100)
    queryIntervalUpperLim = ConfigOption('query_interval_upper_lim', 60000)

    sigUpdate = QtCore.Signal()
    sigSavingStatusChanged = QtCore.Signal(bool)

    _saving = StatusVar('saving', False)

    def __init__(self, config, **kwargs):
        """ Create TemperatureMonitorLogic 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]))

        self._saving = False
        self.header_string = None
        return

    def on_activate(self):
        """ Prepare logic module for work.
        """
        self._tm = self.tm()
        self._save_logic = self.savelogic()

        self.stopRequest = False
        self.data = {}
        self._data_to_save = []
        self._saving = False

        self.queryTimer = QtCore.QTimer()
        self.queryTimer.setInterval(self.queryInterval)
        self.queryTimer.setSingleShot(True)
        self.queryTimer.timeout.connect(self.check_temperature_loop,
                                        QtCore.Qt.QueuedConnection)

        # get laser capabilities
        self.tm_unit = self._tm.get_process_unit()

        self.init_data_logging()
        self.start_query_loop()

    def on_deactivate(self):
        """ Deactivate module.
        """
        self.stop_query_loop()
        if self._saving:
            self.stop_saving()
        self.clear_buffer()

    @QtCore.Slot(int)
    def change_qtimer_interval(self, interval):
        """ Change query interval time. """
        if self.queryIntervalLowerLim <= interval <= self.queryIntervalUpperLim:
            self.queryTimer.setInterval(interval)
            self.queryInterval = interval
            self.log.info(f"Query interval changed to {self.queryInterval}")
        else:
            self.log.warn(
                f"Query interval limits are {self.queryIntervalLowerLim} to {self.queryIntervalUpperLim}. "
                f"Query interval is {self.queryInterval}")

    def get_saving_state(self):
        """ Returns if the data is saved in the moment.

        @return bool: saving state
        """
        return self._saving

    def get_channels(self):
        """ Shortcut for hardware get_counter_channels.

            @return list(str): return list of active counter channel names
        """
        return self._tm.get_channels()

    @QtCore.Slot()
    def check_temperature_loop(self):
        """ Get temperatures from monitor. """
        if self.stopRequest:
            if self.module_state.can('stop'):
                self.module_state.stop()
            self.stopRequest = False
            return
        qi = self.queryInterval
        try:
            for channel in self.get_channels():
                self.data[channel].append(
                    self._tm.get_process_value(channel=channel))

            self.data['time'].append(time.time())
        except:
            qi = 3000
            self.log.exception(
                "Exception in TM status loop, throttling refresh rate.")

        # save the data if necessary
        if self._saving:
            newdata = np.empty((len(self.get_channels()) + 1), )
            newdata[0] = time.time() - self._saving_start_time
            for i, channel in enumerate(self.get_channels()):
                newdata[i + 1] = self.data[channel][-1]
            self._save_logic.write_data([newdata],
                                        header=self.header_string,
                                        filepath=self.filepath,
                                        filename=self.filename)
            self._data_to_save.append(newdata)

        self.queryTimer.start(qi)
        self.sigUpdate.emit()

    @QtCore.Slot()
    def start_query_loop(self):
        """ Start the readout loop. """
        self.module_state.run()
        self.queryTimer.start(self.queryInterval)

    @QtCore.Slot()
    def stop_query_loop(self):
        """ Stop the readout loop. """
        self.stopRequest = True
        self.queryTimer.stop()

    def init_data_logging(self):
        """
        Initialize data logging into a continuously expanding dictionary.
        Note that this can potentially cause problems with extremely long measurements on systems with less memory.
        """
        self.data['time'] = []
        for ch in self.get_channels():
            self.data[ch] = []

    def clear_buffer(self):
        """ Flush all data currently stored in memory. """
        if self.data:
            # Only clear data if it is not empty
            self.data.clear()
        self.init_data_logging()

    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
        self.save_data_header()
        self.sigSavingStatusChanged.emit(self._saving)
        return self._saving

    def stop_saving(self):
        """
        Stop the saving of the file, and set the QtSignal accordingly.

        @return bool: saving state
        """
        if not self._data_to_save:
            # Check if list is empty
            self.log.warn("No data to save!")
        else:
            # Only save figure if list is not empty to prevent IndexError
            fig = self.draw_figure(data=np.array(self._data_to_save))
            self._save_logic.save_figure(plotfig=fig,
                                         filepath=self.filepath,
                                         filename=self.filename)

        self._saving = False
        self.sigSavingStatusChanged.emit(self._saving)
        self.header = None
        self._data_to_save.clear()
        return self._saving

    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.
        """
        time_data = data[:, 0]

        # Use qudi style
        plt.style.use(self._save_logic.mpl_qudihira_style)

        # Create figure
        fig, ax = plt.subplots(nrows=len(self.get_channels()),
                               ncols=1,
                               sharex=True)

        if len(self.get_channels()) == 1:
            ax = [ax]

        for i, channel in enumerate(self.get_channels()):
            ax[i].plot(time_data, data[:, i + 1], '.-')
            ax[i].set_xlabel('Time (s)')
            ax[i].set_ylabel(channel.title() + ' T (K)')

        plt.tight_layout()

        return fig

    def save_data_header(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
        @param bool save_figure: select whether png and pdf should be saved

        @return dict parameters: Dictionary which contains the saving parameters
        """
        # write the parameters:
        parameters = OrderedDict()
        parameters['Start counting time'] = time.strftime(
            '%d.%m.%Y %Hh:%Mmin:%Ss', time.localtime(self._saving_start_time))

        if to_file:
            # If there is a postfix then add separating underscore
            if postfix == '':
                filelabel = 'temperature'
            else:
                filelabel = 'temperature_' + postfix

            # prepare the data in a dict or in an OrderedDict:
            self.header_string = 'Time (s)'

            for i, channel in enumerate(self.get_channels()):
                self.header_string += ',{}_temp (K)'.format(channel)

            header_array = self.header_string.split(",")
            self.filepath = self._save_logic.get_path_for_module(
                module_name='Temperature')
            self.filename = self._save_logic.create_file_and_header(
                header_array,
                filepath=self.filepath,
                parameters=parameters,
                filelabel=filelabel,
                delimiter='\t')

        return [], parameters
Exemplo n.º 21
0
class PIDLogic(GenericLogic):
    """
    Control a process via software PID.
    """

    # 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.controller()
        self._save_logic = self.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]
Exemplo n.º 22
0
class NVCalculatorLogic(GenericLogic):
    """This is the Logic class for Calculator."""
    _modclass = 'calculatorlogic'
    _modtype = 'logic'

    # declare connectors
    odmr = Connector(interface='ODMRLogic', optional=True)
    pulsed = Connector(interface='PulsedMeasurementLogic', optional=True)

    data_source = 0  # choose the data and fitting source, either from cw-odmr, or pulsedmeasurement 0: "no data_source", 1: "CW_ODMR", 2: "pulsed"
    zero_field_D = StatusVar('ZFS', 2870e6)
    diamond_strain = StatusVar('strain', 0)
    freq1 = StatusVar('freq1', 2800e6)
    freq2 = StatusVar('freq2', 2900e6)
    lac = StatusVar('level_anti_crossing', default=False)
    manual_nmr = StatusVar('manual_NMR', default=False)

    auto_field = 0.0
    manual_field = 0.0

    # Update signals, e.g. for GUI module
    sigFieldaCalUpdated = QtCore.Signal(str, str)
    sigFieldmCalUpdated = QtCore.Signal(str, str)
    sigFieldParamsUpdated = QtCore.Signal(dict)
    sigDataSourceUpdated = QtCore.Signal(int)
    sigManualFieldUpdated = QtCore.Signal(float)
    sigNMRUpdated = QtCore.Signal(list, list)

    def __init__(self, config, **kwargs):
        super().__init__(config=config, **kwargs)
        self.threadlock = Mutex()

    def on_activate(self):
        # Get connectors
        self.set_data_source(self.data_source)
        return

    def on_deactivate(self):
        """ """
        pass

    def set_data_source(self, data_source):
        self.data_source = data_source
        if data_source == 0:
            self.sigDataSourceUpdated.emit(0)
        elif data_source == 1:
            self.fit = self.odmr()
            self.sigDataSourceUpdated.emit(1)
        elif data_source == 2:
            self.fit = self.pulsed()
            self.sigDataSourceUpdated.emit(2)
        return

    def set_field_params(self, zfs, e, lac):
        self.zero_field_D = zfs
        self.diamond_strain = e
        self.lac = lac
        param_dict = {
            'ZFS': self.zero_field_D,
            'strain': self.diamond_strain,
            'level_anti_crossing': self.lac
        }
        self.sigFieldParamsUpdated.emit(param_dict)
        return self.zero_field_D, self.diamond_strain, self.lac

    def set_manual_dip_values(self, freq1, freq2):
        self.freq1 = freq1
        self.freq2 = freq2
        param_dict = {'freq1': self.freq1, 'freq2': self.freq2}
        self.sigFieldParamsUpdated.emit(param_dict)
        return self.freq1, self.freq2

    def set_manual_field(self, field):
        self.manual_field = field
        self.sigManualFieldUpdated.emit(field)
        return self.manual_field

    def cal_alignment(self, freq1, freq2):  # from Balasubramanian2008 paper
        '''calculates the alignment theta and the magn. field out of the two
        transition frequencies freq1 and freq2

        Attention: If the field is higher than 1000 Gauss the -1 transition frequency
        has to be inserted as a negative value'''
        D_zerofield = self.zero_field_D / 1e6
        zeroField_E = self.diamond_strain / 1e6

        if self.lac:
            freq1 = -freq1  # In case of level anti-crossing, freq1 transforms to be negative value

        delta = (
            (7 * D_zerofield**3 + 2 * (freq1 + freq2) *
             (2 *
              (freq1**2 + freq2**2) - 5 * freq1 * freq2 - 9 * zeroField_E**2) -
             3 * D_zerofield *
             (freq1**2 + freq2**2 - freq1 * freq2 + 9 * zeroField_E**2)) /
            (9 * (freq1**2 + freq2**2 - freq1 * freq2 - D_zerofield**2 -
                  3 * zeroField_E**2)))
        angle_factor = delta / D_zerofield - 1e-9
        field_factor = (freq1**2 + freq2**2 - freq1 * freq2 -
                        D_zerofield**2) / 3 - zeroField_E**2
        if -1 < angle_factor < 1:
            angle = np.arccos(angle_factor) / 2 / (np.pi) * 180
        else:
            angle = np.nan
            self.log.error(
                "Angle calculation failed, probably because of incorrect input ODMR frequencies or "
                "incorrect zero-field splitting, or strain value has to be considered!"
            )
        if field_factor >= 0:
            beta = np.sqrt(field_factor)
            b_field = beta / physical_constants['Bohr magneton in Hz/T'][0] / \
                      (-1 * physical_constants['electron g factor'][0]) * 1e10
        else:
            b_field = np.nan
            self.log.error(
                "Field calculation failed, probably because of incorrect input ODMR frequencies or "
                "incorrect zero-field splitting, or strain value has to be considered!"
            )
        return b_field, angle  # in Gauss and degrees

    def manual_dips(self):
        b_field, angle = self.cal_alignment(self.freq1 / 1e6, self.freq2 / 1e6)
        self.sigFieldmCalUpdated.emit('%.3f' % b_field, '%.3f' % angle)
        self.auto_field = b_field
        return

    def auto_dips(self):
        if self.data_source == 0:
            self.log.error(
                "You have not select data source. Select a data source, or try manual Freqs."
            )
            return
        try:
            if 'g0_center' in self.fit.fc.current_fit_param:
                freq1 = self.fit.fc.current_fit_param['g0_center'].value / 1e6
                freq2 = self.fit.fc.current_fit_param['g1_center'].value / 1e6
            else:
                freq1 = self.fit.fc.current_fit_param['l0_center'].value / 1e6
                freq2 = self.fit.fc.current_fit_param['l1_center'].value / 1e6
            b_field, angle = self.cal_alignment(freq1, freq2)
            self.sigFieldaCalUpdated.emit('%.3f' % b_field, '%.3f' % angle)
            self.auto_field = b_field
        except:
            self.log.error(
                "The NV calculator seems unable to get ODMR dips!"
                " Only double dip Lorentzian or double dip Gaussian fitting can be recognized by the "
                "program. Please inspect your fitting method.")

        return

    def set_m_f(self):
        self.manual_nmr = False
        return

    def set_m_t(self):
        self.manual_nmr = True
        self.auto_field = int(self.auto_field)
        return

    def calculate_nmr(self):
        """
        NMR frequency in Hz, XY8 in s
        """
        if self.manual_nmr:
            field = self.manual_field
        else:
            field = self.auto_field

        h1_freq = 42.58 * field * 1e2
        c13_freq = 10.705 * field * 1e2
        n14_freq = 3.077 * field * 1e2
        n15_freq = -4.316 * field * 1e2

        freqs = [h1_freq, c13_freq, n14_freq, n15_freq]

        h1_xy8 = 1 / 2 / h1_freq
        c13_xy8 = 1 / 2 / c13_freq
        n14_xy8 = 1 / 2 / n14_freq
        n15_xy8 = 0.5 / n15_freq

        xy8 = [h1_xy8, c13_xy8, n14_xy8, n15_xy8]
        self.sigNMRUpdated.emit(freqs, xy8)
        return freqs, xy8

    def single_freq(self, freq):
        if self.lac:
            b_field = np.abs(freq / 1e6 + self.zero_field_D / 1e6) / 2.8
        else:
            b_field = np.abs(freq / 1e6 - self.zero_field_D / 1e6) / 2.8
        return b_field
Exemplo n.º 23
0
class HardwareSwitchFpga(Base, SwitchInterface):
    """
    This is the hardware class for the Spartan-6 (Opal Kelly XEM6310) FPGA based hardware switch.
    The command reference for communicating via the OpalKelly Frontend can be looked up here:

        https://library.opalkelly.com/library/FrontPanelAPI/index.html

    The Frontpanel is basically a C++ interface, where a wrapper was used (SWIG) to access the
    dll library. Be aware that the wrapper is specified for a specific version of python
    (here python 3.4), and it is not guaranteed to be working with other versions.

    Example config for copy-paste:

    fpga_switch:
        module.Class: 'switches.ok_fpga.ok_s6_switch.HardwareSwitchFpga'
        fpga_serial: '143400058N'
        fpga_type: 'XEM6310_LX45'  # optional
        path_to_bitfile: <file path>  # optional
        name: 'OpalKelly FPGA Switch'  # optional
        remember_states: True  # optional
        switches:               # optional
            B14: ['Off', 'On']
            B16: ['Off', 'On']
            B12: ['Off', 'On']
             C7: ['Off', 'On']
            D15: ['Off', 'On']
            D10: ['Off', 'On']
             D9: ['Off', 'On']
            D11: ['Off', 'On']
    """

    # config options
    # serial number of the FPGA
    _serial = ConfigOption('fpga_serial', missing='error')
    # Type of the FGPA, possible type options: XEM6310_LX150, XEM6310_LX45
    _fpga_type = ConfigOption('fpga_type', default='XEM6310_LX45', missing='warn')
    # specify the path to the bitfile, if it is not in qudi_main_dir/thirdparty/qo_fpga
    _path_to_bitfile = ConfigOption('path_to_bitfile', default=None, missing='nothing')
    # customize available switches in config. Each switch needs a tuple of 2 state names.
    _switches = ConfigOption(
        name='switches',
        default={s: ('Off', 'On') for s in ('B14', 'B16', 'B12', 'C7', 'D15', 'D10', 'D9', 'D11')},
        missing='nothing'
    )
    # optional name of the hardware
    _hardware_name = ConfigOption(name='name', default='OpalKelly FPGA Switch', missing='nothing')
    # if remember_states is True the last state will be restored at reloading of the module
    _remember_states = ConfigOption(name='remember_states', default=False, missing='nothing')

    # StatusVariable for remembering the last state of the hardware
    _states = StatusVar(name='states', default=None)

    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)

        self._fpga = None
        self._lock = RecursiveMutex()
        self._connected = False

    def on_activate(self):
        """ Connect and configure the access to the FPGA.
        """
        self._switches = self._chk_refine_available_switches(self._switches)

        # Create an instance of the Opal Kelly FrontPanel
        self._fpga = ok.FrontPanel()
        # Sanity check for fpga_type ConfigOption
        self._fpga_type = self._fpga_type.upper()
        if self._fpga_type not in ('XEM6310_LX45', 'XEM6310_LX150'):
            raise NameError('Unsupported FPGA type "{0}" specified in config. Valid options are '
                            '"XEM6310_LX45" and "XEM6310_LX150".\nAborting module activation.'
                            ''.format(self._fpga_type))
        # connect to the FPGA module
        self._connect()

        # reset states if requested, otherwise use the saved states
        if self._remember_states and isinstance(self._states, dict) and \
                set(self._states) == set(self._switches):
            self._states = {switch: self._states[switch] for switch in self._switches}
            self.states = self._states
        else:
            self._states = dict()
            self.states = {switch: states[0] for switch, states in self._switches.items()}

    def on_deactivate(self):
        """ Deactivate the FPGA.
        """
        del self._fpga
        self._connected = False

    def _connect(self):
        """ Connect host PC to FPGA module with the specified serial number.
        The serial number is defined by the mandatory ConfigOption fpga_serial.
        """
        # check if a FPGA is connected to this host PC. That method is used to
        # determine also how many devices are available.
        if not self._fpga.GetDeviceCount():
            self.log.error('No FPGA connected to host PC or FrontPanel.exe is running.')
            return -1

        # open a connection to the FPGA with the specified serial number
        self._fpga.OpenBySerial(self._serial)

        if not self._path_to_bitfile:
            # upload the proper hardware switch configuration bitfile to the FPGA
            if self._fpga_type == 'XEM6310_LX45':
                bitfile_name = 'switch_8chnl_withcopy_LX45.bit'
            elif self._fpga_type == 'XEM6310_LX150':
                bitfile_name = 'switch_8chnl_withcopy_LX150.bit'
            else:
                self.log.error('Unsupported FPGA type "{0}" specified in config. Valid options are '
                               '"XEM6310_LX45" and "XEM6310_LX150".\nConnection to FPGA module failed.'
                               ''.format(self._fpga_type))
                return -1
            self._path_to_bitfile = os.path.join(get_main_dir(), 'thirdparty', 'qo_fpga', bitfile_name)

        # Load on the FPGA a configuration file (bit file).
        self.log.debug(f'Using bitfile: {self._path_to_bitfile}')
        self._fpga.ConfigureFPGA(self._path_to_bitfile)

        # Check if the upload was successful and the Opal Kelly FrontPanel is enabled on the FPGA
        if not self._fpga.IsFrontPanelEnabled():
            self.log.error('Opal Kelly FrontPanel is not enabled in FPGA')
            return -1

        self._connected = True
        return 0

    @property
    def name(self):
        """ Name of the hardware as string.

        @return str: The name of the hardware
        """
        return self._hardware_name

    @property
    def available_states(self):
        """ Names of the states as a dict of tuples.

        The keys contain the names for each of the switches. The values are tuples of strings
        representing the ordered names of available states for each switch.

        @return dict: Available states per switch in the form {"switch": ("state1", "state2")}
        """
        return self._switches.copy()

    @property
    def states(self):
        """ The current states the hardware is in as state dictionary with switch names as keys and
        state names as values.

        @return dict: All the current states of the switches in the form {"switch": "state"}
        """
        with self._lock:
            self._fpga.UpdateWireOuts()
            new_state = int(self._fpga.GetWireOutValue(0x20))
            self._states = dict()
            for channel_index, (switch, valid_states) in enumerate(self.available_states):
                if new_state & (1 << channel_index):
                    self._states[switch] = valid_states[1]
                else:
                    self._states[switch] = valid_states[0]
            return self._states.copy()

    @states.setter
    def states(self, state_dict):
        """ The setter for the states of the hardware.

        The states of the system can be set by specifying a dict that has the switch names as keys
        and the names of the states as values.

        @param dict state_dict: state dict of the form {"switch": "state"}
        """
        assert isinstance(state_dict, dict), \
            f'Property "state" must be dict type. Received: {type(state_dict)}'
        assert all(switch in self.available_states for switch in state_dict), \
            f'Invalid switch name(s) encountered: {tuple(state_dict)}'
        assert all(isinstance(state, str) for state in state_dict.values()), \
            f'Invalid switch state(s) encountered: {tuple(state_dict.values())}'

        with self._lock:
            # determine desired state of ALL switches
            new_states = self._states.copy()
            new_states.update(state_dict)
            # encode states into a single int
            new_channel_state = 0
            for channel_index, (switch, state) in enumerate(new_states.items()):
                if state == self.available_states[switch][1]:
                    new_channel_state |= 1 << channel_index

            # apply changes in hardware
            self._fpga.SetWireInValue(0x00, new_channel_state)
            self._fpga.UpdateWireIns()
            # Check for success
            assert self.states == new_states, 'Setting of channel states failed'

    def get_state(self, switch):
        """ Query state of single switch by name

        @param str switch: name of the switch to query the state for
        @return str: The current switch state
        """
        assert switch in self.available_states, 'Invalid switch name "{0}"'.format(switch)
        return self.states[switch]

    def set_state(self, switch, state):
        """ Query state of single switch by name

        @param str switch: name of the switch to change
        @param str state: name of the state to set
        """
        self.states = {switch: state}

    @staticmethod
    def _chk_refine_available_switches(switch_dict):
        """ See SwitchInterface class for details

        @param dict switch_dict:
        @return dict:
        """
        refined = super()._chk_refine_available_switches(switch_dict)
        assert len(refined) == 8, 'Exactly 8 switches or None must be specified in config'
        assert all(len(s) == 2 for s in refined.values()), 'Switches can only take exactly 2 states'
        return refined
Exemplo n.º 24
0
class SwitchDummy(Base, SwitchInterface):
    """ Methods to control slow switching devices.

    Example config for copy-paste:

    switch_dummy:
        module.Class: 'switches.switch_dummy.SwitchDummy'
        name: 'First'  # optional
        remember_states: True  # optional
        switches:
            one: ['down', 'up']
            two: ['down', 'up']
            three: ['low', 'middle', 'high']
    """

    # ConfigOptions
    # customize available switches in config. Each switch needs a tuple of at least 2 state names.
    _switches = ConfigOption(name='switches', missing='error')
    # optional name of the hardware
    _hardware_name = ConfigOption(name='name', default=None, missing='nothing')
    # if remember_states is True the last state will be restored at reloading of the module
    _remember_states = ConfigOption(name='remember_states',
                                    default=True,
                                    missing='nothing')

    # StatusVariable for remembering the last state of the hardware
    _states = StatusVar(name='states', default=None)

    def on_activate(self):
        """ Activate the module and fill status variables.
        """
        self._switches = self._chk_refine_available_switches(self._switches)

        # Choose config name for this module if no name is given in ConfigOptions
        if self._hardware_name is None:
            self._hardware_name = self._name

        # reset states if requested, otherwise use the saved states
        if self._remember_states and isinstance(self._states, dict) and \
                set(self._states) == set(self._switches):
            self._states = {
                switch: self._states[switch]
                for switch in self._switches
            }
        else:
            self._states = {
                switch: states[0]
                for switch, states in self._switches.items()
            }

    def on_deactivate(self):
        """ Deactivate the module and clean up.
        """
        pass

    @property
    def name(self):
        """ Name of the hardware as string.

        @return str: The name of the hardware
        """
        return self._hardware_name

    @property
    def available_states(self):
        """ Names of the states as a dict of tuples.

        The keys contain the names for each of the switches. The values are tuples of strings
        representing the ordered names of available states for each switch.

        @return dict: Available states per switch in the form {"switch": ("state1", "state2")}
        """
        return self._switches.copy()

    def get_state(self, switch):
        """ Query state of single switch by name

        @param str switch: name of the switch to query the state for
        @return str: The current switch state
        """
        assert switch in self.available_states, f'Invalid switch name: "{switch}"'
        return self._states[switch]

    def set_state(self, switch, state):
        """ Query state of single switch by name

        @param str switch: name of the switch to change
        @param str state: name of the state to set
        """
        avail_states = self.available_states
        assert switch in avail_states, f'Invalid switch name: "{switch}"'
        assert state in avail_states[
            switch], f'Invalid state name "{state}" for switch "{switch}"'
        self._states[switch] = state
Exemplo n.º 25
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'

    """

    activation_config = StatusVar(default=None)
    force_sequence_option = ConfigOption('force_sequence_option', default=False)

    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['config5']
        elif self.activation_config not in self.get_constraints().activation_config.values():
            self.activation_config = self.get_constraints().activation_config['config5']

        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.FORCED if self.force_sequence_option else 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))
        # print(number_of_samples, waveforms)
        return number_of_samples, waveforms

    def write_sequence(self, name, sequence_parameter_list):
        print(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]

        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')
        """

        print('load_dict:')
        print(load_dict)

        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
Exemplo n.º 26
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
    """

    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()
Exemplo n.º 27
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
    """

    hardware = Connector(interface='ProcessControlInterface')

    _calibration = StatusVar(default=None)
    _calibration_file = ConfigOption('calibration_file', None)
    _force_calibration_from_file = ConfigOption('force_calibration_from_file', False)
    _interpolated_function = None
    _interpolated_function_reversed = None

    _new_unit = ConfigOption('new_unit', None)

    _last_control_value = None

    def on_activate(self):
        """ Activate module.
        """
        self._hardware = self.hardware()

        if self._force_calibration_from_file and self._calibration_file is None:
            self.log.error('Loading from calibration is enforced but no calibration file has been'
                           'given.')
        if self._force_calibration_from_file or (self._calibration is None and self._calibration_file is not None):
            self.log.info('Loading from calibration file.')
            calibration = np.loadtxt(self._calibration_file)
            self.update_calibration(calibration)
        else:
            self.update_calibration()

    def on_deactivate(self):
        """ Deactivate module.
        """
        pass

    def update_calibration(self, calibration=None):
        """ Construct the interpolated function from the calibration data

        calibration (optional) 2d array : A new calibration to set

        """
        if calibration is not None:
            self._calibration = calibration
        if self._calibration is None:
            self._interpolated_function = lambda x: x
            self._interpolated_function_reversed = lambda x: x
        else:
            self._interpolated_function = interp1d(self._calibration[:, 0], self._calibration[:, 1])
            self._interpolated_function_reversed = interp1d(self._calibration[:, 1], self._calibration[:, 0])
        if self._last_control_value is not None:
            self.set_control_value(self._last_control_value)

    def reset_to_identity(self):
        """ Reset the calibration data to use identity """
        self._calibration = None
        self.update_calibration()

    def get_control_value(self):
        """ Return the original control value
        """
        if self._interpolated_function_reversed is not None:
            return self._interpolated_function_reversed(self._hardware.get_control_value())
        else:
            self.log.error('No calibration was found, please set the control value modifier data first.')

    def set_control_value(self, value):
        """ Set the control value modified
        """
        if self._interpolated_function is not None:
            self._hardware.set_control_value(self._interpolated_function(value))
        else:
            self.log.error('No calibration was found, please set the control value modifier data first.')

    def get_control_unit(self):
        """ Return the process unit
        """
        if self._new_unit is not None:
            return self._new_unit
        else:
            return self._hardware.get_control_unit()

    def get_control_limit(self):
        """ Return limits within which the controlled value can be set as a tuple of (low limit, high limit)
        """
        mini, maxi = self._hardware.get_control_limit()
        mini = float(self._interpolated_function_reversed(mini))
        maxi = float(self._interpolated_function_reversed(maxi))
        return mini, maxi
class StagecontrolLogic(GenericLogic):
    """ Logic module for moving stage hardware with GUI and gamepad.
    """

    stagehardware = Connector(interface='PositionerInterface')
    xboxlogic = Connector(interface='XboxLogic')

    # Signals to trigger GUI updates
    sigPositionUpdated = QtCore.Signal(dict)
    sigHitTarget = QtCore.Signal()
    sigVelocityUpdated = QtCore.Signal(dict)

    # Signals to trigger stage moves
    sigStartJog = QtCore.Signal(tuple)
    sigStartStep = QtCore.Signal(tuple)
    sigStopAxis = QtCore.Signal(str)

    # Config option to invert axes for jog operations
    invert_axes = ConfigOption('jog_invert_axes', [])

    # Set polling interval for stage position (ms)
    poll_interval = ConfigOption('poll_interval', 500)

    preset_velocities = StatusVar(
        default={
            'slow': {
                'x': 0.01,
                'y': 0.01,
                'z': 0.005
            },
            'medium': {
                'x': 0.05,
                'y': 0.05,
                'z': 0.005
            },
            'fast': {
                'x': 0.5,
                'y': 0.5,
                'z': 0.5
            }
        })

    def __init__(self, config, **kwargs):
        """ Create logic object

          @param dict config: configuration in a dict
          @param dict kwargs: additional parameters as a dict
        """
        super().__init__(config=config, **kwargs)

        self.threadlock = Mutex()

    def on_activate(self):
        """ Prepare logic module for work.
        """
        self.stage_hw = self.stagehardware()
        self.xbox_logic = self.xboxlogic()

        self.xbox_logic.sigButtonPress.connect(self.xbox_button_press)
        self.xbox_logic.sigJoystickMoved.connect(self.xbox_joystick_move)

        self.on_target = False

        # Connect stage move signals
        self.sigStartJog.connect(self._do_jog)
        self.sigStartStep.connect(self._do_step)
        self.sigStopAxis.connect(self._stop_axis)

        # Variable to keep track of joystick state (avoid excessive number of
        # commands to cube) - this is 0 for no motion, or +1 or -1 depending on
        # direction.
        self.x_joystick_jog_running = 0
        self.y_joystick_jog_running = 0
        self.z_joystick_jog_running = 0

        self.start_poll()

    def on_deactivate(self):
        """ Deactivate module.
        """
        pass

    #######################
    # Stage control methods
    #######################

    def get_hw_manufacturer(self):
        """ Gets hardware info from stage hardware """
        return self.stage_hw.hw_info()

    def move_abs(self, move_dict):
        """ Moves stage to an absolute position.

        @param move_dict: dict of positions, with axis names as keys and 
            axis target positions as items. If an axis is not specified, it
            remains at its current position.
        """
        self.on_target = False
        if 'x' in move_dict.keys():
            self.stage_hw.set_position('x', move_dict['x'])
        if 'y' in move_dict.keys():
            self.stage_hw.set_position('y', move_dict['y'])
        if 'z' in move_dict.keys():
            self.stage_hw.set_position('z', move_dict['z'])

    def move_rel(self, move_dict):
        """ Moves stage to a relative position.

        @param move_dict: dict of positions, with axis names as keys and 
            axis move distances as items. If an axis is not specified, it
            remains at its current position.
        """
        self.on_target = False
        if 'x' in move_dict.keys():
            self.stage_hw.set_position('x', move_dict['x'], True)
        if 'y' in move_dict.keys():
            self.stage_hw.set_position('y', move_dict['y'], True)
        if 'z' in move_dict.keys():
            self.stage_hw.set_position('z', move_dict['z'], True)

    def is_moving(self):
        """ Returns True if any axis is currently moving.
        """
        if (self.stage_hw.get_axis_status('x', 'on_target')
                and self.stage_hw.get_axis_status('y', 'on_target')
                and self.stage_hw.get_axis_status('z', 'on_target')):
            return False
        else:
            return True

    def start_jog(self, axis, direction):
        """ Start stage movement on axis in specified direction.

        @param axis: str, move along this axis
        @param direction: bool, move in forward direction if True, backward if false.
        """
        self.on_target = False
        # pylint: disable=unsupported-membership-test
        if axis in self.invert_axes:
            direction = not direction

        self.sigStartJog.emit((axis, direction))

    def step(self, axis, steps):
        """ Steps stage in specified direction.
        
        @param axis str: axis to move.
        """
        self.on_target = False
        # pylint: disable=unsupported-membership-test
        if axis in self.invert_axes:
            steps = -steps
        self.sigStartStep.emit((axis, steps))

    def stop_axis(self, axis):
        """ Stops specified axis.
        
        @param axis str: axis to stop.
        """
        self.sigStopAxis.emit(axis)

    def stop(self):
        """ Stops all axes immediatey. """
        self.stage_hw.stop_all()

    def set_axis_config(self, axis, **config_options):
        """ Sets axis config options
        
        Accepts config options as kwargs to pass through to the hardware.
        """
        self.stage_hw.set_axis_config(axis, **config_options)

    def get_axis_config(self, axis, option=None):
        """ Gets axis parameters.
        
        @param axis str: Axis to get parameters from
        @param option str: (optional) Parameter to retrieve. If not specified, 
            return dict of all available config options.
        """
        return self.stage_hw.get_axis_config(axis, option)

    def home_axis(self, axis=None):
        """ Homes stage
        
        @param axis str: If specified, home this axis only. Otherwise home
            all axes. """
        self.stage_hw.reference_axis(axis)

    ##################
    # Velocity presets
    ##################

    def set_velocity_to_preset(self, preset):
        """ Sets velocities to the preset values.

        @param preset str: 'fast', 'medium' or 'slow'.
        """
        allowed_values = ('fast', 'medium', 'slow')
        if preset not in allowed_values:
            raise ValueError("Preset must be one of {}".format(allowed_values))

        # pylint: disable=unsubscriptable-object
        for axis, velocity in self.preset_velocities[preset].items():
            self.stage_hw.set_axis_config(axis, velocity=velocity)

        self.sigVelocityUpdated.emit(self.preset_velocities[preset])

    def set_preset_values(self, slow=None, medium=None, fast=None):
        """ Sets values for velocity presets.

        Any missing kwargs are left unchanged.

        @param slow tuple(float): Set slow velocities for (x, y, z) axes.
        @param medium tuple(float): Set medium velocities for (x, y, z) axes.
        @param fast tuple(float): Set fast velocities for (x, y, z) axes.
        """
        axes = ('x', 'y', 'z')

        # pylint: disable=unsupported-assignment-operation
        if slow is not None:
            self.preset_velocities['slow'] = dict(zip(axes, slow))

        if medium is not None:
            self.preset_velocities['medium'] = dict(zip(axes, medium))

        if fast is not None:
            self.preset_velocities['fast'] = dict(zip(axes, fast))

    ##############################
    # Internal stage control slots
    ##############################

    @QtCore.Slot(dict)
    def _do_jog(self, param_list):
        """ Internal method to start jog. Slot for sigStartJog"""
        self.stage_hw.start_continuous_motion(*param_list)

    @QtCore.Slot(dict)
    def _do_step(self, param_list):
        """ Internal method to start jog. Slot for sigStartStep"""
        self.stage_hw.move_steps(*param_list)

    @QtCore.Slot(str)
    def _stop_axis(self, axis):
        """ Internal method to stop axis. Slot for sigStopAxis"""
        self.stage_hw.stop_axis(axis)
        self.stage_hw.set_axis_config(axis, offset_voltage=0)

    ##################
    # Position polling
    ##################

    def start_poll(self):
        """ Start polling the stage position """
        self.poll = True
        QtCore.QTimer.singleShot(self.poll_interval, self._poll_position)

    def _poll_position(self):
        """ Poll the stage for its current position.

        Designed to be triggered by timer; emits sigPositionUpdated every time
        the position is polled, and sigHitTarget the first time the stage
        reports that no axes are moving after a move was started.
        """
        if not self.poll:
            return

        pos_dict = {}

        try:
            pos_dict['x'] = self.stage_hw.get_position('x')
            pos_dict['y'] = self.stage_hw.get_position('y')
            pos_dict['z'] = self.stage_hw.get_position('z')

            if not self.is_moving():
                if not self.on_target:
                    self.sigHitTarget.emit()
                    self.on_target = True
            else:
                if self.on_target:
                    self.on_target = False

        except PositionerError:
            # Ignore hardware errors.
            pass

        self.sigPositionUpdated.emit(pos_dict)
        QtCore.QTimer.singleShot(self.poll_interval, self._poll_position)

    ###################
    # Gamepad interface
    ###################

    @QtCore.Slot(str)
    def xbox_button_press(self, button):
        """ Moves stage according to inputs from the Xbox controller buttons.

        Slot for sigButtonPressed from xboxlogic module."""
        self.on_target = False
        # D-pad: click x and y
        if button == 'left_down':
            # D-pad down
            self.step('y', -1)

        elif button == 'left_up':
            # D-pad up
            self.step('y', 1)

        elif button == 'left_left':
            # D-pad left
            self.step('x', -1)

        elif button == 'left_right':
            # D-pad right
            self.step('x', 1)

        # Shoulder buttons: left - z down, right - z up
        elif button == 'left_shoulder':
            # Left shoulder
            self.step('z', -1)

        elif button == 'right_shoulder':
            # Right shoulder
            self.step('z', 1)

        # A, B, X, Y
        elif button == 'right_down':
            # A button
            self.set_velocity_to_preset('slow')
        elif button == 'right_up':
            # Y button
            self.set_velocity_to_preset('fast')
        elif button == 'right_left':
            # X button
            self.set_velocity_to_preset('medium')
        elif button == 'right_right':
            # B button
            self.stop()

    @QtCore.Slot(dict)
    def xbox_joystick_move(self, joystick_state):
        """ Moves stage according to inputs from the Xbox controller joysticks.

        Slot for sigJoystickMoved from xboxlogic module."""

        self.on_target = False
        # Z-control on y-axis of right-hand joystick
        z = joystick_state['y_right']

        if z == 0 and self.z_joystick_jog_running != 0:
            # If joystick zeroed and cube is currently moving, stop.
            self.stop_axis('z')
            self.z_joystick_jog_running = 0

        elif np.sign(z) != np.sign(self.z_joystick_jog_running):
            # Otherwise, move in appropriate direction if needed.
            if z > 0:
                self.start_jog('z', False)
                self.z_joystick_jog_running = 1
            elif z < 0:
                self.start_jog('z', True)
                self.z_joystick_jog_running = -1

        # x,y control on left-hand joystick
        # Use sectors defined by lines with y = 2x and x = 2y for pure y or x
        # motion, otherwise do diagonal movement.
        x = joystick_state['x_left']
        y = joystick_state['y_left']

        required_x = 0
        required_y = 0

        if np.sqrt(x**2 + y**2) < 0.1:
            # Circular dead-zone
            pass

        elif abs(y) > abs(2 * x):
            # If in the exclusive y motion sector, just move in y
            required_y = np.sign(y)

        elif abs(x) > abs(2 * y):
            # If in the exclusive x motion sector, just move in x
            required_x = np.sign(x)

        else:
            # If somewhere else, move if the axis is non-zero.
            if x != 0:
                required_x = np.sign(x)
            if y != 0:
                required_y = np.sign(y)

        # Do required movements, checking flags to minimise commands sent to
        # stage controller.

        if required_x == 0 and self.x_joystick_jog_running != 0:
            # Stop x
            self.stop_axis('x')
            self.x_joystick_jog_running = 0

        if required_y == 0 and self.y_joystick_jog_running != 0:
            # Stop y
            self.stop_axis('y')
            self.y_joystick_jog_running = 0

        if (required_y != 0 and
            (np.sign(self.y_joystick_jog_running) != np.sign(required_y)
             or self.y_joystick_jog_running == 0)):
            # Move y
            if y > 0:
                self.start_jog('y', True)
                self.y_joystick_jog_running = 1
            elif y < 0:
                self.start_jog('y', False)
                self.y_joystick_jog_running = -1

        if (required_x != 0 and
            (np.sign(self.x_joystick_jog_running) != np.sign(required_x)
             or self.x_joystick_jog_running == 0)):
            # Move x
            if x > 0:
                self.start_jog('x', True)
                self.x_joystick_jog_running = 1
            elif x < 0:
                self.start_jog('x', False)
                self.x_joystick_jog_running = -1
Exemplo n.º 29
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')
    _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
Exemplo n.º 30
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

    """

    # declare connectors
    # TODO: Use rather the task runner instead directly the module!

    sequencegenerationlogic = Connector(interface='SequenceGeneratorLogic')
    traceanalysislogic = Connector(interface='TraceAnalysisLogic')
    gatedcounterlogic = Connector(interface='CounterLogic')
    odmrlogic = Connector(interface='ODMRLogic')
    optimizerlogic = Connector(interface='OptimizerLogic')
    scannerlogic = Connector(interface='ConfocalLogic')
    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.savelogic()

        #FIXME: THAT IS JUST A TEMPORARY SOLUTION! Implement the access on the
        #       needed methods via the TaskRunner!
        self._seq_gen_logic = self.sequencegenerationlogic()
        self._trace_ana_logic = self.traceanalysislogic()
        self._gc_logic = self.gatedcounterlogic()
        self._odmr_logic = self.odmrlogic()
        self._optimizer_logic = self.optimizerlogic()
        self._confocal_logic = self.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.module_state.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.module_state.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.module_state(
        ) != '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.module_state() == '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.module_state(
        ) != '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.module_state(
        ) != '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))