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()
class OceanOptics(Base, SpectrometerInterface): """ Hardware module for reading spectra from the Ocean Optics spectrometer software. Example config for copy-paste: myspectrometer: module.Class: 'spectrometer.oceanoptics_spectrometer.OceanOptics' spectrometer_serial: 'QEP01583' #insert here the right serial number. """ _serial = ConfigOption('spectrometer_serial', missing='warn') _integration_time = StatusVar('integration_time', default=10000) def on_activate(self): """ Activate module. """ self.spec = sb.Spectrometer.from_serial_number(self._serial) self.log.info(''.format(self.spec.model, self.spec.serial_number)) self.spec.integration_time_micros(self._integration_time) self.log.info('Exposure set to {} microseconds'.format( self._integration_time)) def on_deactivate(self): """ Deactivate module. """ self.spec.close() def recordSpectrum(self): """ Record spectrum from Ocean Optics spectrometer. @return []: spectrum data """ wavelengths = self.spec.wavelengths() specdata = np.empty((2, len(wavelengths)), dtype=np.double) specdata[0] = wavelengths / 1e9 specdata[1] = self.spec.intensities() return specdata def getExposure(self): """ Get exposure. @return float: exposure Not implemented. """ return self._integration_time def setExposure(self, exposureTime): """ Set exposure. @param float exposureTime: exposure time in microseconds """ self._integration_time = exposureTime self.spec.integration_time_micros(self._integration_time)
class 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
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
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)
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()
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
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()
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
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
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}
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)
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
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)
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
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
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
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
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
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
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]
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
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
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
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
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()
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
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
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))