class LaserExtCtrlLogic(GenericLogic): laser = Connector(interface='ExtCtrlLaserInterface') option_placeholder = ConfigOption('option_placeholder', 0) 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): self._laser = self.laser() def on_deactivate(self): pass def get_power_atsource(self): return self._laser.get_power() def set_ext_ctrl_power(self, power_percentage): self._laser.set_power_extctrl(power_percentage) def get_power_atpd(self): pass
class InfluxDataClient(Base, ProcessInterface): """ Retrieve live data from InfluxDB as if the measurement device was connected directly. Example config for copy-paste: influx_data_client: module.Class: 'influx_data_client.InfluxDataClient' user: '******' password: '******' dbname: 'db_name' host: 'localhost' port: 8086 dataseries: 'data_series_name' field: 'field_name' criterion: 'criterion_name' """ user = ConfigOption('user', missing='error') pw = ConfigOption('password', missing='error') dbname = ConfigOption('dbname', missing='error') host = ConfigOption('host', missing='error') port = ConfigOption('port', default=8086) series = ConfigOption('dataseries', missing='error') field = ConfigOption('field', missing='error') cr = ConfigOption('criterion', missing='error') def on_activate(self): """ Activate module. """ self.connect_db() def on_deactivate(self): """ Deactivate module. """ del self.conn def connect_db(self): """ Connect to Influx database """ self.conn = InfluxDBClient(self.host, self.port, self.user, self.pw, self.dbname) def get_process_value(self): """ Return a measured value """ q = 'SELECT last({0}) FROM {1} WHERE (time > now() - 10m AND {2})'.format( self.field, self.series, self.cr) res = self.conn.query(q) return list(res[('{0}'.format(self.series), None)])[0]['last'] def get_process_unit(self): """ Return the unit that the value is measured in @return (str, str): a tuple of ('abreviation', 'full unit name') """ return '°C', ' degrees Celsius'
class MimicGui(GUIBase): xseries = Connector(interface='NationalInstrumentsXSeries') channel = ConfigOption('channel', '/Dev1/PCIe-6363', missing='warn') def __init__(self, config, **kwargs): super().__init__(config=config, **kwargs) def on_activate(self): """ Definition and initialisation of the GUI. """ self.nicard = self.xseries() # Create main window instance self._mw = MimicMainWindow() this_dir = os.path.dirname(__file__) self.fibre_sw_on_pixmap = QtGui.QPixmap( os.path.join(this_dir, "fibre_switch_on.png")) self.fibre_sw_off_pixmap = QtGui.QPixmap( os.path.join(this_dir, "fibre_switch_off.png")) # Initialise module with fibre switch off self.fibre_switch_off() ################### # Connect UI events ################### self._mw.fibre_sw_off_btn.clicked.connect(self.fibre_switch_off) self._mw.fibre_sw_on_btn.clicked.connect(self.fibre_switch_on) def show(self): """Make window visible and put it above all other windows. """ QtWidgets.QMainWindow.show(self._mw) self._mw.activateWindow() self._mw.raise_() def on_deactivate(self): # FIXME: ! """ Deactivate the module """ self._mw.close() def fibre_switch_off(self): self._mw.fibre_sw_mimic.setPixmap(self.fibre_sw_off_pixmap) self.nicard.digital_channel_switch(self.channel, False) def fibre_switch_on(self): self._mw.fibre_sw_mimic.setPixmap(self.fibre_sw_on_pixmap) self.nicard.digital_channel_switch(self.channel, True)
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 SimpleAcq(Base, SimpleDataInterface): """ Read human readable numbers from serial port. Example config for copy-paste: simple_data_acq: module.Class: 'simple_data_acq.SimpleAcq' interface: 'ASRL1::INSTR' baudrate: 115200 """ resource = ConfigOption('interface', 'ASRL1::INSTR', missing='warn') baudrate = ConfigOption('baudrate', 115200, missing='warn') def on_activate(self): """ Activate module. """ self.rm = visa.ResourceManager() self.log.debug('Resources: {0}'.format(self.rm.list_resources())) self.my_instrument = self.rm.open_resource(self.resource, baud_rate=self.baudrate) def on_deactivate(self): """ Deactivate module. """ self.my_instrument.close() self.rm.close() def getData(self): """ Read one value from serial port. @return int: vaue form serial port """ try: return int(self.my_instrument.read_raw().decode('utf-8').rstrip().split()[1]) except: return 0 def getChannels(self): """ Number of channels. @return int: number of channels """ return 1
class ArduinoUnoR3(Base, ArduinoInterface): """ Example config: voltage_generator: module.Class: 'power_supply.power_supply_dummy.PowerSupplyDummy' voltage_min: 0 voltage_max_1: 30 voltage_max_2: 30 voltage_max_3: 5 current_max: 3 """ _usb_address = ConfigOption('usb_address', missing='error') _baud_rate = ConfigOption('baud_rate', missing='error') def on_activate(self): """ Initialisation performed during activation of the module. """ self.rm = visa.ResourceManager() try: self._serialcomm = serial.Serial('COM5', 115200) self._serialcomm.timeout = 0.05 except: self.log.error('Could not connect to port "{}". Check ' 'whether address exists and reload ' 'module!'.format(self._usb_address)) raise def on_deactivate(self): """ Deinitialisation performed during deactivation of the module. """ self._serialcomm.close() def write_and_read(self, text): """ pass string to arduino and receive return """ self._serialcomm.write(text.encode()) time.sleep(0.1) output = self._serialcomm.readline().decode('ascii') return output
class OptimizerLogic(GenericLogic): """Helper module to simplify running survey workflow scripts in the background. Works in cooperatively with Jupyter""" # Connect to everything this module has helper methods for # (intended as a local module although in some form this could be made configurable) aom = Connector(interface='aomlogic') hbt = Connector(interface='hbtlogic') poimanager = Connector(interface='poimanagerlogic') pulsedmaster = Connector(interface='pulsedmasterlogic') optimizer = Connector(interface='optimizerlogic') working_directory = ConfigOption('working_directory') _sigInternal = QtCore.Signal() def __init__(self, config, **kwargs): super().__init__(config=config, **kwargs) self._running = False # Is a workflow running self._subdir = None def on_activate(self): """ Initialisation performed during activation of the module. @return int: error code (0:OK, -1:error) """ return 0 def on_deactivate(self): """ Reverse steps of activation @return int: error code (0:OK, -1:error) """ return 0 def run_in_background(self, code): # set up a namespace for the sub thread def wlog(self, msg): # log the workflow progress to a separate logfile out = _time_str() + ' ' + msg with open(self._log_file, 'a') as f: f.write(out + '\n') self.log.debug(msg) def set_laser_power(self, power): self.wlog("Setting power via AOM to {} mW".format(power)) self.aom().set_power(power * 1e-3)
class SlackNotifierLogic(GenericLogic): """Notifier logic for Slack. Provides the `send_message` method that can be called in scripts or other modules. Config for copy-paste: slacknotifier: module.Class: 'slack_notifier_logic.SlackNotifierLogic' api_key: '<Slack bot API OAuth key>' channel: '#channel-to-message' """ # Config options _api_key = ConfigOption('api_key', missing='error') _channel = ConfigOption('channel', missing='error') def on_activate(self): pass def on_deactivate(self): pass def send_message(self, message): """Send message to Slack @param str message: Message to post. """ try: requests.post('https://slack.com/api/chat.postMessage', { 'token':self._api_key, 'channel':self._channel, 'text':message }).json() self.log.info('{} posted to Slack channel {}'.format(message, self._channel)) except Exception as err: self.log.error('Error posting Slack message: {}'.format(err))
class ANCController(Base): _modclass = 'attocube_anc350' _modtype = 'hardware' _dll_location = ConfigOption('dll_location', missing='error') _sample_controller_number = ConfigOption("sample_controller_number", 0, missing='error') _tip_controller_number = ConfigOption("tip_controller_number", 1, missing='error') tip = None sample = None def on_activate(self): self.tip = ANC350(self._dll_location, self._tip_controller_number) self.sample = ANC350(self._dll_location, self._sample_controller_number) def on_deactivate(self): self.tip.close() self.sample.close()
class PiezoDummy(Base, PiezoInterface): """ Dummy for piezo interface Example config for copy-paste: piezo_dummy: module.Class: 'piezo.piezo_dummy.PiezoDummy' step: 0.01 # in µm """ step = ConfigOption('step', 0.01) def on_activate(self): """ Initialisation performed during activation of the module. """ pass def on_deactivate(self): """ Deinitialisation performed during deactivation of the module. """ pass def get_position(self): """ Retrieves the current position @returns: float pos: simulated position """ pos = np.random.normal() return pos def set_step(self, step): """ sets the step entered on the GUI by the user @returns: None """ self.step = step
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 SaveLogic(GenericLogic): """ A general class which saves all kinds of data in a general sense. Example config for copy-paste: savelogic: module.Class: 'save_logic.SaveLogic' win_data_directory: 'C:/Data' # DO NOT CHANGE THE DIRECTORY HERE! ONLY IN THE CUSTOM FILE! unix_data_directory: 'Data/' log_into_daily_directory: True save_pdf: True save_png: True """ _win_data_dir = ConfigOption('win_data_directory', 'C:/Data/') _unix_data_dir = ConfigOption('unix_data_directory', 'Data') log_into_daily_directory = ConfigOption('log_into_daily_directory', False, missing='warn') save_pdf = ConfigOption('save_pdf', False) save_png = ConfigOption('save_png', True) # Matplotlib style definition for saving plots mpl_qd_style = { 'axes.prop_cycle': cycler('color', [ '#1f17f4', '#ffa40e', '#ff3487', '#008b00', '#17becf', '#850085' ]) + cycler('marker', ['o', 's', '^', 'v', 'D', 'd']), 'axes.edgecolor': '0.3', 'xtick.color': '0.3', 'ytick.color': '0.3', 'axes.labelcolor': 'black', 'font.size': '14', 'lines.linewidth': '2', 'figure.figsize': '12, 6', 'lines.markeredgewidth': '0', 'lines.markersize': '5', 'axes.spines.right': True, 'axes.spines.top': True, 'xtick.minor.visible': True, 'ytick.minor.visible': True, 'savefig.dpi': '180' } # Matplotlib style definition for saving plots # Better than the default! mpl_qudihira_style = { 'axes.linewidth': 0.5, 'axes.labelweight': 'light', 'lines.linewidth': 0.5, 'xtick.major.width': 0.5, 'ytick.major.width': 0.5, 'font.weight': 'light', 'font.sans-serif': 'Calibri', 'mathtext.fontset': 'stixsans', 'mathtext.default': 'regular', 'axes.spines.right': True, 'axes.spines.top': True, 'xtick.minor.visible': True, 'ytick.minor.visible': True, 'savefig.dpi': '200', 'figure.figsize': '12, 6', } _additional_parameters = {} def __init__(self, config, **kwargs): super().__init__(config=config, **kwargs) # locking for thread safety self.lock = Mutex() # name of active POI, default to empty string self.active_poi_name = '' # Some default variables concerning the operating system: self.os_system = None # Chech which operation system is used and include a case if the # directory was not found in the config: if sys.platform in ('linux', 'darwin'): self.os_system = 'unix' self.data_dir = self._unix_data_dir elif 'win32' in sys.platform or 'AMD64' in sys.platform: self.os_system = 'win' self.data_dir = self._win_data_dir else: raise Exception('Identify the operating system.') # Expand environment variables in the data_dir path (e.g. $HOME) self.data_dir = os.path.expandvars(self.data_dir) # start logging into daily directory? if not isinstance(self.log_into_daily_directory, bool): self.log.warning( 'log entry in configuration is not a ' 'boolean. Falling back to default setting: False.') self.log_into_daily_directory = False self._daily_loghandler = None def on_activate(self): """ Definition, configuration and initialisation of the SaveLogic. """ if self.log_into_daily_directory: # adds a log handler for logging into daily directory self._daily_loghandler = DailyLogHandler( '%Y%m%d-%Hh%Mm%Ss-qudi.log', self) self._daily_loghandler.setFormatter( logging.Formatter( '%(asctime)s %(name)s %(levelname)s: %(message)s', datefmt='%Y-%m-%d %H:%M:%S')) self._daily_loghandler.setLevel(logging.DEBUG) logging.getLogger().addHandler(self._daily_loghandler) else: self._daily_loghandler = None def on_deactivate(self): if self._daily_loghandler is not None: # removes the log handler logging into the daily directory logging.getLogger().removeHandler(self._daily_loghandler) @property def dailylog(self): """ Returns the daily log handler. """ return self._daily_loghandler def dailylog_set_level(self, level): """ Sets the log level of the daily log handler @param level int: log level, see logging """ self._daily_loghandler.setLevel(level) def save_data(self, data, filepath=None, parameters=None, filename=None, filelabel=None, timestamp=None, filetype='text', fmt='%.15e', delimiter='\t', plotfig=None): """ General save routine for data. @param dictionary data: Dictionary containing the data to be saved. The keys should be strings containing the data header/description. The corresponding items are one or more 1D arrays or one 2D array containing the data (list or numpy.ndarray). Example: data = {'Frequency (MHz)': [1,2,4,5,6]} data = {'Frequency': [1, 2, 4], 'Counts': [234, 894, 743, 423]} data = {'Frequency (MHz),Counts':[[1,234], [2,894],...[30,504]]} @param string filepath: optional, the path to the directory, where the data will be saved. If the specified path does not exist yet, the saving routine will try to create it. If no path is passed (default filepath=None) the saving routine will create a directory by the name of the calling module inside the daily data directory. If no calling module can be inferred and/or the requested path can not be created the data will be saved in a subfolder of the daily data directory called UNSPECIFIED @param dictionary parameters: optional, a dictionary with all parameters you want to save in the header of the created file. @parem string filename: optional, if you really want to fix your own filename. If passed, the whole file will have the name <filename> If nothing is specified the save logic will generate a filename either based on the module name from which this method was called, or it will use the passed filelabel if that is speficied. You also need to specify the ending of the filename! @parem string filelabel: optional, if filelabel is set and no filename was specified, the savelogic will create a name which looks like YYYY-MM-DD_HHh-MMm-SSs_<filelabel>.dat The timestamp will be created at runtime if no user defined timestamp was passed. @param datetime timestamp: optional, a datetime.datetime object. You can create this object with datetime.datetime.now() in the calling module if you want to fix the timestamp for the filename. Be careful when passing a filename and a timestamp, because then the timestamp will be ignored. @param string filetype: optional, the file format the data should be saved in. Valid inputs are 'text', 'xml' and 'npz'. Default is 'text'. @param string or list of strings fmt: optional, format specifier for saved data. See python documentation for "Format Specification Mini-Language". If you want for example save a float in scientific notation with 6 decimals this would look like '%.6e'. For saving integers you could use '%d', '%s' for strings. The default is '%.15e' for numbers and '%s' for str. If len(data) > 1 you should pass a list of format specifiers; one for each item in the data dict. If only one specifier is passed but the data arrays have different data types this can lead to strange behaviour or failure to save right away. @param string delimiter: optional, insert here the delimiter, like '\n' for new line, '\t' for tab, ',' for a comma ect. 1D data ======= 1D data should be passed in a dictionary where the data trace should be assigned to one identifier like {'<identifier>':[list of values]} {'Numbers of counts':[1.4, 4.2, 5, 2.0, 5.9 , ... , 9.5, 6.4]} You can also pass as much 1D arrays as you want: {'Frequency (MHz)':list1, 'signal':list2, 'correlations': list3, ...} 2D data ======= 2D data should be passed in a dictionary where the matrix like data should be assigned to one identifier like {'<identifier>':[[1,2,3],[4,5,6],[7,8,9]]} which will result in: <identifier> 1 2 3 4 5 6 7 8 9 YOU ARE RESPONSIBLE FOR THE IDENTIFIER! DO NOT FORGET THE UNITS FOR THE SAVED TIME TRACE/MATRIX. """ start_time = time.time() # Create timestamp if none is present if timestamp is None: timestamp = datetime.datetime.now() # Try to cast data array into numpy.ndarray if it is not already one # Also collect information on arrays in the process and do sanity checks found_1d = False found_2d = False multiple_dtypes = False arr_length = [] arr_dtype = [] max_row_num = 0 max_line_num = 0 for keyname in data: # Cast into numpy array if not isinstance(data[keyname], np.ndarray): try: data[keyname] = np.array(data[keyname]) except: self.log.error( 'Casting data array of type "{0}" into numpy.ndarray failed. ' 'Could not save data.'.format(type(data[keyname]))) return -1 # determine dimensions if data[keyname].ndim < 3: length = data[keyname].shape[0] arr_length.append(length) if length > max_line_num: max_line_num = length if data[keyname].ndim == 2: found_2d = True width = data[keyname].shape[1] if max_row_num < width: max_row_num = width else: found_1d = True max_row_num += 1 else: self.log.error( 'Found data array with dimension >2. Unable to save data.') return -1 # determine array data types if len(arr_dtype) > 0: if arr_dtype[-1] != data[keyname].dtype: multiple_dtypes = True arr_dtype.append(data[keyname].dtype) # Raise error if data contains a mixture of 1D and 2D arrays if found_2d and found_1d: self.log.error( 'Passed data dictionary contains 1D AND 2D arrays. This is not allowed. ' 'Either fit all data arrays into a single 2D array or pass multiple 1D ' 'arrays only. Saving data failed!') return -1 # try to trace back the functioncall to the class which was calling it. try: frm = inspect.stack()[1] # this will get the object, which called the save_data function. mod = inspect.getmodule(frm[0]) # that will extract the name of the class. module_name = mod.__name__.split('.')[-1] except: # Sometimes it is not possible to get the object which called the save_data function # (such as when calling this from the console). module_name = 'UNSPECIFIED' # determine proper file path if filepath is None: filepath = self.get_path_for_module(module_name) elif not os.path.exists(filepath): os.makedirs(filepath) self.log.info( 'Custom filepath does not exist. Created directory "{0}"' ''.format(filepath)) # create filelabel if none has been passed if filelabel is None: filelabel = module_name if self.active_poi_name != '': filelabel = self.active_poi_name.replace(' ', '_') + '_' + filelabel # determine proper unique filename to save if none has been passed if filename is None: filename = timestamp.strftime('%Y%m%d-%H%M-%S' + '_' + filelabel + '.dat') # Check format specifier. if not isinstance(fmt, str) and len(fmt) != len(data): self.log.error( 'Length of list of format specifiers and number of data items differs. ' 'Saving not possible. Please pass exactly as many format specifiers as ' 'data arrays.') return -1 # Create header string for the file header = 'Saved Data from the class {0} on {1}.\n' \ ''.format(module_name, timestamp.strftime('%d.%m.%Y at %Hh%Mm%Ss')) header += '\nParameters:\n===========\n\n' # Include the active POI name (if not empty) as a parameter in the header if self.active_poi_name != '': header += 'Measured at POI: {0}\n'.format(self.active_poi_name) # add the parameters if specified: if parameters is not None: # check whether the format for the parameters have a dict type: if isinstance(parameters, dict): if isinstance(self._additional_parameters, dict): parameters = {**self._additional_parameters, **parameters} for entry, param in parameters.items(): if isinstance(param, float): header += '{0}: {1:.16e}\n'.format(entry, param) else: header += '{0}: {1}\n'.format(entry, param) # make a hardcore string conversion and try to save the parameters directly: else: self.log.error( 'The parameters are not passed as a dictionary! The SaveLogic will ' 'try to save the parameters nevertheless.') header += 'not specified parameters: {0}\n'.format(parameters) header += '\nData:\n=====\n' # write data to file # FIXME: Implement other file formats # write to textfile if filetype == 'text': # Reshape data if multiple 1D arrays have been passed to this method. # If a 2D array has been passed, reformat the specifier if len(data) != 1: identifier_str = '' if multiple_dtypes: field_dtypes = list( zip([ 'f{0:d}'.format(i) for i in range(len(arr_dtype)) ], arr_dtype)) new_array = np.empty(max_line_num, dtype=field_dtypes) for i, keyname in enumerate(data): identifier_str += keyname + delimiter field = 'f{0:d}'.format(i) length = data[keyname].size new_array[field][:length] = data[keyname] if length < max_line_num: if isinstance(data[keyname][0], str): new_array[field][length:] = 'nan' else: new_array[field][length:] = np.nan else: new_array = np.empty([max_line_num, max_row_num], arr_dtype[0]) for i, keyname in enumerate(data): identifier_str += keyname + delimiter length = data[keyname].size new_array[:length, i] = data[keyname] if length < max_line_num: if isinstance(data[keyname][0], str): new_array[length:, i] = 'nan' else: new_array[length:, i] = np.nan # discard old data array and use new one data = {identifier_str: new_array} elif found_2d: keyname = list(data.keys())[0] identifier_str = keyname.replace(', ', delimiter).replace( ',', delimiter) data[identifier_str] = data.pop(keyname) else: identifier_str = list(data)[0] header += list(data)[0] self.save_array_as_text(data=data[identifier_str], filename=filename, filepath=filepath, fmt=fmt, header=header, delimiter=delimiter, comments='#', append=False) # write npz file and save parameters in textfile elif filetype == 'npz': header += str(list(data.keys()))[1:-1] np.savez_compressed(filepath + '/' + filename[:-4], **data) self.save_array_as_text(data=[], filename=filename[:-4] + '_params.dat', filepath=filepath, fmt=fmt, header=header, delimiter=delimiter, comments='#', append=False) else: self.log.error( 'Only saving of data as textfile and npz-file is implemented. Filetype "{0}" is not ' 'supported yet. Saving as textfile.'.format(filetype)) self.save_array_as_text(data=data[identifier_str], filename=filename, filepath=filepath, fmt=fmt, header=header, delimiter=delimiter, comments='#', append=False) #-------------------------------------------------------------------------------------------- # Save thumbnail figure of plot if plotfig is not None: # create Metadata metadata = dict() metadata['Title'] = 'Image produced by qudi: ' + module_name metadata['Author'] = 'qudi - Software Suite' metadata[ 'Subject'] = 'Find more information on: https://github.com/Ulm-IQO/qudi' metadata[ 'Keywords'] = 'Python 3, Qt, experiment control, automation, measurement, software, framework, modular' metadata['Producer'] = 'qudi - Software Suite' if timestamp is not None: metadata['CreationDate'] = timestamp metadata['ModDate'] = timestamp else: metadata['CreationDate'] = time metadata['ModDate'] = time if self.save_pdf: # determine the PDF-Filename fig_fname_vector = os.path.join(filepath, filename)[:-4] + '_fig.pdf' # Create the PdfPages object to which we will save the pages: # The with statement makes sure that the PdfPages object is closed properly at # the end of the block, even if an Exception occurs. with PdfPages(fig_fname_vector) as pdf: pdf.savefig(plotfig, bbox_inches='tight', pad_inches=0.05) # We can also set the file's metadata via the PdfPages object: pdf_metadata = pdf.infodict() for x in metadata: pdf_metadata[x] = metadata[x] if self.save_png: # determine the PNG-Filename and save the plain PNG fig_fname_image = os.path.join(filepath, filename)[:-4] + '_fig.png' plotfig.savefig(fig_fname_image, bbox_inches='tight', pad_inches=0.05) # Use Pillow (an fork for PIL) to attach metadata to the PNG png_image = Image.open(fig_fname_image) png_metadata = PngImagePlugin.PngInfo() # PIL can only handle Strings, so let's convert our times metadata['CreationDate'] = metadata['CreationDate'].strftime( '%Y%m%d-%H%M-%S') metadata['ModDate'] = metadata['ModDate'].strftime( '%Y%m%d-%H%M-%S') for x in metadata: # make sure every value of the metadata is a string if not isinstance(metadata[x], str): metadata[x] = str(metadata[x]) # add the metadata to the picture png_metadata.add_text(x, metadata[x]) # save the picture again, this time including the metadata png_image.save(fig_fname_image, "png", pnginfo=png_metadata) # close matplotlib figure plt.close(plotfig) self.log.debug( 'Time needed to save data: {0:.2f}s'.format(time.time() - start_time)) #---------------------------------------------------------------------------------- def save_array_as_text(self, data, filename, filepath='', fmt='%.15e', header='', delimiter='\t', comments='#', append=False): """ An Independent method, which can save a 1D or 2D numpy.ndarray as textfile. Can append to files. """ # write to file. Append if requested. if append: with open(os.path.join(filepath, filename), 'ab') as file: np.savetxt(file, data, fmt=fmt, delimiter=delimiter, header=header, comments=comments) else: with open(os.path.join(filepath, filename), 'wb') as file: np.savetxt(file, data, fmt=fmt, delimiter=delimiter, header=header, comments=comments) return def get_daily_directory(self): """ Gets or creates daily save directory. @return string: path to the daily directory. If the daily directory does not exits in the specified <root_dir> path in the config file, then it is created according to the following scheme: <root_dir>\<year>\<month>\<yearmonthday> and the filepath is returned. There should be always a filepath returned. """ current_dir = os.path.join(self.data_dir, time.strftime("%Y"), time.strftime("%m"), time.strftime("%Y%m%d")) if not os.path.isdir(current_dir): self.log.info("Creating directory for today's data:\n" '{0}'.format(current_dir)) # The exist_ok=True is necessary here to prevent Error 17 "File Exists" # Details at http://stackoverflow.com/questions/12468022/python-fileexists-error-when-making-directory os.makedirs(current_dir, exist_ok=True) return current_dir def get_path_for_module(self, module_name): """ Method that creates a path for 'module_name' where data are stored. @param string module_name: Specify the folder, which should be created in the daily directory. The module_name can be e.g. 'Confocal'. @return string: absolute path to the module name """ dir_path = os.path.join(self.get_daily_directory(), module_name) if not os.path.isdir(dir_path): os.makedirs(dir_path, exist_ok=True) return dir_path def get_additional_parameters(self): """ Method that return the additional parameters dictionary securely """ return self._additional_parameters.copy() def update_additional_parameters(self, *args, **kwargs): """ Method to update one or multiple additional parameters @param dict args: Optional single positional argument holding parameters in a dict to update additional parameters from. @param kwargs: Optional keyword arguments to be added to additional parameters """ if len(args) == 0: param_dict = kwargs elif len(args) == 1 and isinstance(args[0], dict): param_dict = args[0] param_dict.update(kwargs) else: raise TypeError( '"update_additional_parameters" takes exactly 0 or 1 positional ' 'argument of type dict.') for key in param_dict.keys(): param_dict[key] = netobtain(param_dict[key]) self._additional_parameters.update(param_dict) return def remove_additional_parameter(self, key): """ remove parameter from additional parameters @param str key: The additional parameters key/name to delete """ self._additional_parameters.pop(key, None) return
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 LaserAomInterfuse(GenericLogic, SimpleLaserInterface): """ This interfuse can be used to control the laser power after an AOM driven by an analog ouput on a confocal scanner hardware (the 4th analog output 'a') The hardware module should be configured accordingly (range 0 to 1, voltage 0 to 1V for example) This module needs a calibration file for the AOM. This is a 2D array with the first column the relative power (power over maximum power) and the second column the associated voltage. This data is interpolated to define the power/voltage function """ # connector to the confocal scanner hardware that has analog output feature scanner = Connector(interface='ConfocalScannerInterface') # max power the AOM can deliver (in Watt) _max_power = ConfigOption('max_power', missing='error') # calibration file which can be read by numpy loadtxt with two columns : # relative power (0.0 to 1.0), voltage (V) _calibration_file = ConfigOption('calibration_file', missing='error') _power_to_voltage = None _power = 0 _laser_on = LaserState.OFF def on_activate(self): """ Activate module. """ self._scanner = self.scanner() if 'a' not in self._scanner.get_scanner_axes(): self.log.error( 'Scanner does not have an "a" axe configured. Can not use it to control an AOM.' ) calibration_data = np.loadtxt(self._calibration_file) power_rel_to_voltage = interp1d(calibration_data[:, 0], calibration_data[:, 1]) self._power_to_voltage = lambda power: power_rel_to_voltage( power / self._max_power) def on_deactivate(self): """ Deactivate module. """ pass def get_power_range(self): """ Return optical power range @return (float, float): power range """ return 0, self._max_power def get_power(self): """ Return laser power @return float: Laser power in watts """ return self._power def get_power_setpoint(self): """ Return optical power setpoint. @return float: power setpoint in watts """ return self._power def set_power(self, power): """ Set power setpoint. @param float power: power setpoint @return float: actual new power setpoint """ mini, maxi = self.get_power_range() if mini <= power <= maxi: self._power = power if self._laser_on == LaserState.ON: voltage = self._power_to_voltage(power) else: voltage = self._power_to_voltage(0) if self._scanner.module_state() == 'locked': self.log.error( 'Output device of the voltage for the AOM is locked, cannot set voltage.' ) else: if self._scanner.scanner_set_position(a=voltage) < 0: self.log.error( 'Could not set the voltage for the AOM because the scanner failed.' ) return self._power def get_current_unit(self): """ Get unit for laser current. @return str: unit """ return '%' def get_current_range(self): """ Get laser current range. @return (float, float): laser current range """ return 0, 100 def get_current(self): """ Get current laser current @return float: laser current in current curent units """ return 0 def get_current_setpoint(self): """ Get laser curent setpoint @return float: laser current setpoint """ return 0 def set_current(self, current): """ Set laser current setpoint @prarm float current: desired laser current setpoint @return float: actual laser current setpoint """ return 0 def allowed_control_modes(self): """ Get supported control modes @return list(): list of supported ControlMode """ return [ControlMode.POWER] def get_control_mode(self): """ Get the currently active control mode @return ControlMode: active control mode """ return ControlMode.POWER def set_control_mode(self, control_mode): """ Set the active control mode @param ControlMode control_mode: desired control mode @return ControlMode: actual active ControlMode """ return ControlMode.POWER def on(self): """ Turn on laser. @return LaserState: actual laser state """ return self.set_laser_state(LaserState.ON) def off(self): """ Turn off laser. @return LaserState: actual laser state """ return self.set_laser_state(LaserState.OFF) def get_laser_state(self): """ Get laser state @return LaserState: actual laser state """ return self._laser_on def set_laser_state(self, state): """ Set laser state. @param LaserState state: desired laser state @return LaserState: actual laser state """ self._laser_on = state self.set_power(self._power) def get_shutter_state(self): """ Get laser shutter state @return ShutterState: actual laser shutter state """ return ShutterState.NOSHUTTER def set_shutter_state(self, state): """ Set laser shutter state. @param ShutterState state: desired laser shutter state @return ShutterState: actual laser shutter state """ return ShutterState.NOSHUTTER def get_temperatures(self): """ Get all available temperatures. @return dict: dict of temperature namce and value in degrees Celsius """ return {} def set_temperatures(self, temps): """ Set temperatures for lasers with tunable temperatures. @return {}: empty dict, dummy not a tunable laser """ return {} def get_temperature_setpoints(self): """ Get temperature setpoints. @return dict: temperature setpoints for temperature tunable lasers """ return {} def get_extra_info(self): """ Multiple lines of dignostic information @return str: much laser, very useful """ return "" def set_max_power(self, maxi): """ Function to redefine the max power if the value has changed """ self._max_power = maxi
class TimeTaggerAutocorrelation(Base, AutocorrelationInterface): """ UNSTABLE Using the TimeTagger for autocorrelation measurement. Example config for copy-paste: timetagger_slowcounter: module.Class: 'timetagger_autocorrelation.TimeTaggerAutocorrelation' timetagger_channel_apd_0: 0 timetagger_channel_apd_1: 1 """ _channel_apd_0 = ConfigOption('timetagger_channel_apd_0', missing='error') _channel_apd_1 = ConfigOption('timetagger_channel_apd_1', missing='error') def on_activate(self): """ Initialisation performed during activation of the module. """ self._tagger = tt.createTimeTagger() self._count_length = int(10) self._bin_width = 1 # bin width in ps self._tagger.reset() self.correlation = None self.statusvar = 0 def on_deactivate(self): """ Deactivate the FPGA. """ if self.module_state() == 'locked': self.correlation.stop() self.correlation.clear() self.correlation = None self._tagger.reset() return 0 def get_constraints(self): """ Get hardware limits of TimeTagger autocorrelation device. @return AutocorrelationConstraints: constraints class for autocorrelation """ constraints = AutocorrelationConstraints() constraints.max_channels = 2 constraints.min_channels = 2 constraints.min_count_length = 1 constraints.min_bin_width = 0 return constraints def set_up_correlation(self, count_length=None, bin_width=None): """ Configuration of the fast counter. @param float bin_width: Length of a single time bin in the time trace histogram in picoseconds. @param float count_length: Total number of bins. @return tuple(bin_width, count_length): bin_width: float the actual set binwidth in picoseconds count_length: actual number of bins """ self._bin_width = bin_width self._count_length = count_length self.statusvar = 1 if self.correlation != None: self._reset_hardware() if self._tagger == None: return -1 self.correlation = tt.Correlation(tagger=self._tagger, channel_1=self._channel_apd_0, channel_2=self._channel_apd_1, binwidth=self._bin_width, n_bins=self._count_length) self.correlation.stop() return 0 def _reset_hardware(self): self.correlation.clear() return 0 def get_status(self): """ Receives the current status of the Fast Counter and outputs it as return value. 0 = unconfigured 1 = idle 2 = running 3 = paused -1 = error state """ return self.statusvar def start_measure(self): """ Start the fast counter. """ if self.module_state() != 'locked': self.lock() self.correlation.clear() self.correlation.start() self.statusvar = 2 return 0 def stop_measure(self): """ Stop the fast counter. """ if self.module_state() == 'locked': self.correlation.stop() self.module_state.unlock() self.statusvar = 1 return 0 def pause_measure(self): """ Pauses the current measurement. Fast counter must be initially in the run state to make it pause. """ if self.module_state() == 'locked': self.correlation.stop() self.statusvar = 3 return 0 def continue_measure(self): """ Continues the current measurement. If fast counter is in pause state, then fast counter will be continued. """ if self.module_state() == 'locked': self.correlation.start() self.statusvar = 2 return 0 def get_bin_width(self): """ Returns the width of a single timebin in the timetrace in picoseconds. @return float: current length of a single bin in seconds (seconds/bin) """ return self._bin_width def get_count_length(self): """ Returns the number of time bins. @return float: number of bins """ return 2 * self._count_length + 1 def get_data_trace(self): """ @return numpy.array: onedimensional array of dtype = int64. Size of array is determined by 2*count_length+1 """ return self.correlation.getData() def get_normalized_data_trace(self): """ @return numpy.array: onedimensional array of dtype = int64 normalized according to https://www.physi.uni-heidelberg.de/~schmiedm/seminar/QIPC2002/SinglePhotonSource/SolidStateSingPhotSource_PRL85(2000).pdf Size of array is determined by 2*count_length+1 """ return self.correlation.getDataNormalized() def get_bin_times(self): return self.correlation.getIndex() def close_correlation(self): """ Closes the counter and cleans up afterwards. @return int: error code (0:OK, -1:error) """ self._tagger.reset() return 0
class ProcessControlModifier(GenericLogic, ProcessControlInterface): """ This interfuse can be used to modify a process control on the fly. It needs a 2D array to interpolate General form : [[x_0, y_0], [x_1, y_1], ... , [x_n, y_n]] Example : [[0,0], [1,10]] With this example, the value 0.5 sent from the logic would be transformed to 5 sent to the hardware. This calibration is stored and remembered as a status variable. If this variable is None, the calibration can be read from a simple file with two columns : # X Y 0 0 1 10 """ hardware = Connector(interface='ProcessControlInterface') _calibration = StatusVar(default=None) _calibration_file = ConfigOption('calibration_file', None) _force_calibration_from_file = ConfigOption('force_calibration_from_file', False) _interpolated_function = None _interpolated_function_reversed = None _new_unit = ConfigOption('new_unit', None) _last_control_value = None def on_activate(self): """ Activate module. """ self._hardware = self.hardware() if self._force_calibration_from_file and self._calibration_file is None: self.log.error('Loading from calibration is enforced but no calibration file has been' 'given.') if self._force_calibration_from_file or (self._calibration is None and self._calibration_file is not None): self.log.info('Loading from calibration file.') calibration = np.loadtxt(self._calibration_file) self.update_calibration(calibration) else: self.update_calibration() def on_deactivate(self): """ Deactivate module. """ pass def update_calibration(self, calibration=None): """ Construct the interpolated function from the calibration data calibration (optional) 2d array : A new calibration to set """ if calibration is not None: self._calibration = calibration if self._calibration is None: self._interpolated_function = lambda x: x self._interpolated_function_reversed = lambda x: x else: self._interpolated_function = interp1d(self._calibration[:, 0], self._calibration[:, 1]) self._interpolated_function_reversed = interp1d(self._calibration[:, 1], self._calibration[:, 0]) if self._last_control_value is not None: self.set_control_value(self._last_control_value) def reset_to_identity(self): """ Reset the calibration data to use identity """ self._calibration = None self.update_calibration() def get_control_value(self): """ Return the original control value """ if self._interpolated_function_reversed is not None: return self._interpolated_function_reversed(self._hardware.get_control_value()) else: self.log.error('No calibration was found, please set the control value modifier data first.') def set_control_value(self, value): """ Set the control value modified """ if self._interpolated_function is not None: self._hardware.set_control_value(self._interpolated_function(value)) else: self.log.error('No calibration was found, please set the control value modifier data first.') def get_control_unit(self): """ Return the process unit """ if self._new_unit is not None: return self._new_unit else: return self._hardware.get_control_unit() def get_control_limit(self): """ Return limits within which the controlled value can be set as a tuple of (low limit, high limit) """ mini, maxi = self._hardware.get_control_limit() mini = float(self._interpolated_function_reversed(mini)) maxi = float(self._interpolated_function_reversed(maxi)) return mini, maxi
class Main(Base, ScienceCameraInterface): """ Hardware class for Andor CCD spectroscopy cameras Tested with : - Newton 940 Example config for copy-paste: andor_camera: module.Class: 'camera.andor_camera.Main' dll_location: 'C:\\camera\\andor.dll' # path to library file default_trigger_mode: 'INTERNAL' shutter_TTL: 1 shutter_switching_time: 100e-3 """ _dll_location = ConfigOption('dll_location', missing='error') _start_cooler_on_activate = ConfigOption('start_cooler_on_activate', True) _default_temperature_degree = ConfigOption('default_temperature', -80) # Temperature in °C (not Kelvin !) _default_trigger_mode = ConfigOption('default_trigger_mode', 'INTERNAL') _has_external_shutter = ConfigOption('has_external_shutter', False) # The typ parameter allows the user to control the TTL signal output to an external shutter. # 0 Output TTL low signal to open shutter # 1 Output TTL high signal to open shutter _shutter_TTL = ConfigOption('shutter_TTL', 1) # The opening and closing time specify the time required to open and close the shutter # (this information is required for calculating acquisition timings) _shutter_switching_time = ConfigOption('shutter_switching_time', 100e-3) # Declarations of attributes to make Pycharm happy def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self._constraints = None self._dll = None self._active_tracks = None self._image_advanced_parameters = None self._readout_speed = None self._preamp_gain = None self._read_mode = None self._trigger_mode = None self._shutter_status = None self._cooler_status = None self._temperature_setpoint = None self._acquisition_mode = None ############################################################################## # Basic module activation/deactivation ############################################################################## def on_activate(self): """ Initialization performed during activation of the module. """ try: self._dll = ct.cdll.LoadLibrary(self._dll_location) except OSError: self.log.error('Error during dll loading of the Andor camera, check the dll path.') # This module handle only one camera. DLL support up to 8 cameras. status_code = self._dll.Initialize() if status_code != OK_CODE: self.log.error('Problem during camera initialization') return self._constraints = self._build_constraints() if self._constraints.has_cooler and self._start_cooler_on_activate: self.set_cooler_on(True) self.set_read_mode(ReadMode.FVB) # Good default value, the logic will handle it from here self.set_trigger_mode(self._default_trigger_mode) self.set_temperature_setpoint(self._default_temperature_degree + 273.15) self.set_readout_speed(self._constraints.readout_speeds[0]) self.set_gain(self._constraints.internal_gains[0]) self._set_acquisition_mode(AcquisitionMode.SINGLE_SCAN) self.set_trigger_mode(self._constraints.trigger_modes[0]) if self._constraints.has_shutter: self.set_shutter_state(ShutterState.AUTO) if self._constraints.has_cooler: self.set_cooler_on(True) self._active_tracks = np.array([0, self._constraints.height-1]) self._image_advanced_parameters = ImageAdvancedParameters() self._image_advanced_parameters.horizontal_end = self._constraints.width-1 self._image_advanced_parameters.vertical_end = self._constraints.height-1 def on_deactivate(self): """ De-initialisation performed during deactivation of the module. """ if self.module_state() == 'locked': self.stop_acquisition() # if self._close_shutter_on_deactivate: # self.set_shutter_open_state(False) try: self._dll.ShutDown() except: self.log.warning('Error while shutting down Andor camera via dll.') ############################################################################## # Error management ############################################################################## def _check(self, func_val): """ Check routine for the received error codes. @param (int) func_val: Status code returned by the DLL @return: The DLL function error code """ if not func_val == OK_CODE: self.log.error('Error in Andor camera with error_code {}:{}'.format(func_val, ERROR_DICT[func_val])) return func_val ############################################################################## # Constraints functions ############################################################################## def _build_constraints(self): """ Internal method that build the constraints once at initialisation This makes multiple call to the DLL, so it will be called only once by on_activate """ constraints = Constraints() constraints.name = self._get_name() constraints.width, constraints.height = self._get_image_size() constraints.pixel_size_width, constraints.pixel_size_height = self._get_pixel_size() constraints.internal_gains = self._get_available_gains() constraints.readout_speeds = self._get_available_speeds() constraints.trigger_modes = self._get_available_trigger_modes() constraints.has_shutter = self._has_shutter() constraints.read_modes = [ReadMode.FVB] if constraints.height > 1: constraints.read_modes.extend([ReadMode.MULTIPLE_TRACKS, ReadMode.IMAGE, ReadMode.IMAGE_ADVANCED]) constraints.has_cooler = True # All Andor camera have one constraints.temperature.min, constraints.temperature.max = self._get_temperature_range() constraints.temperature.step = 1 # Andor cameras use integer for control return constraints def get_constraints(self): """ Returns all the fixed parameters of the hardware which can be used by the logic. @return (Constraints): An object of class Constraints containing all fixed parameters of the hardware """ return self._constraints ############################################################################## # Basic functions ############################################################################## def start_acquisition(self): """ Starts the acquisition """ self._check(self._dll.StartAcquisition()) def _wait_for_acquisition(self): """ Internal function, can be used to wait till acquisition is finished """ self._dll.WaitForAcquisition() def abort_acquisition(self): """ Aborts the acquisition """ self._check(self._dll.AbortAcquisition()) def get_ready_state(self): """ Get the status of the camera, to know if the acquisition is finished or still ongoing. @return (bool): True if the camera is ready, False if an acquisition is ongoing As there is no synchronous acquisition in the interface, the logic needs a way to check the acquisition state. """ code = ct.c_int() self._dll.GetStatus(ct.byref(code)) if ERROR_DICT[code.value] == 'DRV_IDLE': return True elif ERROR_DICT[code.value] == 'DRV_ACQUIRING': return False else: self._check(code.value) def get_acquired_data(self): """ Return an array of last acquired data. @return: Data in the format depending on the read mode. Depending on the read mode, the format is : 'FVB' : 1d array 'MULTIPLE_TRACKS' : list of 1d arrays 'IMAGE' 2d array of shape (width, height) 'IMAGE_ADVANCED' 2d array of shape (width, height) Each value might be a float or an integer. """ width = self.get_constraints().width if self.get_read_mode() == ReadMode.FVB: height = 1 elif self.get_read_mode() == ReadMode.MULTIPLE_TRACKS: height = int(len(self.get_active_tracks())/2) elif self.get_read_mode() == ReadMode.IMAGE: height = self.get_constraints().height elif self.get_read_mode() == ReadMode.IMAGE_ADVANCED: params = self.get_image_advanced_parameters() height = int((params.vertical_end - params.vertical_start+1)/params.vertical_binning) width = int((params.horizontal_end - params.horizontal_start+1)/params.horizontal_binning) dimension = int(width * height) c_image_array = ct.c_int32 * dimension c_image = c_image_array() status_code = self._dll.GetAcquiredData(ct.pointer(c_image), dimension) if status_code != OK_CODE: self.log.error('Could not retrieve data from camera. {0}'.format(ERROR_DICT[status_code])) if self.get_read_mode() == ReadMode.FVB: return np.array(c_image) if self.get_read_mode() == ReadMode.MULTIPLE_TRACKS: sorted_data = np.array([int(i) for i in self._sorted_tracks[::2]/2]) return np.reshape(np.array(c_image), (height, width))[sorted_data] else: return np.reshape(np.array(c_image), (height, width)) ############################################################################## # Read mode functions ############################################################################## def get_read_mode(self): """ Getter method returning the current read mode used by the camera. @return (ReadMode): Current read mode """ return self._read_mode def set_read_mode(self, value): """ Setter method setting the read mode used by the camera. @param (ReadMode) value: read mode to set """ if value not in self.get_constraints().read_modes: self.log.error('read_mode not supported') return conversion_dict = {ReadMode.FVB: 0, ReadMode.MULTIPLE_TRACKS: 2, ReadMode.IMAGE: 4, ReadMode.IMAGE_ADVANCED: 4} n_mode = conversion_dict[value] status_code = self._check(self._dll.SetReadMode(n_mode)) if status_code == OK_CODE: self._read_mode = value if value == ReadMode.IMAGE or value == ReadMode.IMAGE_ADVANCED: self._update_image() elif value == ReadMode.MULTIPLE_TRACKS: self._update_active_tracks() def get_readout_speed(self): """ Get the current readout speed (in Hz) @return (float): the readout_speed (Horizontal shift) in Hz """ return self._readout_speed # No getter in the DLL def set_readout_speed(self, value): """ Set the readout speed (in Hz) @param (float) value: horizontal readout speed in Hz """ if value in self.get_constraints().readout_speeds: readout_speed_index = self.get_constraints().readout_speeds.index(value) self._check(self._dll.SetHSSpeed(0, readout_speed_index)) self._readout_speed = value else: self.log.error('Readout_speed value error, value {} is not in correct.'.format(value)) def get_active_tracks(self): """ Getter method returning the read mode tracks parameters of the camera. @return (list): active tracks positions [(start_1, end_1), (start_2, end_2), ... ] """ return self._active_tracks # No getter in the DLL def set_active_tracks(self, value): """ Setter method for the active tracks of the camera. @param (ndarray) value: active tracks positions as [start_1, end_1, start_2, end_2, ... ] """ self._active_tracks = value self._update_active_tracks() def _update_active_tracks(self): """ Internal function that send the current active tracks to the DLL """ if self.get_read_mode() == ReadMode.MULTIPLE_TRACKS: self._sorted_tracks = np.argsort(self._active_tracks) tracks = self._active_tracks[self._sorted_tracks]+1 self._dll.SetRandomTracks.argtypes = [ct.c_int32, ct.c_void_p] status_code = self._check(self._dll.SetRandomTracks(int(len(tracks)/2), tracks.ctypes.data)) self._check(status_code) if status_code != OK_CODE: # Clear tracks if an error has occurred self._active_tracks = [] def get_image_advanced_parameters(self): """ Getter method returning the image parameters of the camera. @return (ImageAdvancedParameters): Current image advanced parameters Can be used in any mode """ return self._image_advanced_parameters # No getter in the DLL def set_image_advanced_parameters(self, value): """ Setter method setting the read mode image parameters of the camera. @param (ImageAdvancedParameters) value: Parameters to set Can be used in any mode """ if not isinstance(value, ImageAdvancedParameters): self.log.error('ImageAdvancedParameters value error. Value {} is not correct.'.format(value)) self._image_advanced_parameters = value self._update_image() def _update_image(self): """ Internal method that send the current appropriate image settings to the DLL""" if self.get_read_mode() == ReadMode.IMAGE: status_code = self._dll.SetImage(1, 1, 1, self.get_constraints().width, 1, self.get_constraints().height) self._check(status_code) elif self.get_read_mode() == ReadMode.IMAGE_ADVANCED: params = self._image_advanced_parameters status_code = self._dll.SetImage(int(params.horizontal_binning), int(params.vertical_binning), int(params.horizontal_start+1), int(params.horizontal_end+1), int(params.vertical_start+1), int(params.vertical_end+1)) self._check(status_code) ############################################################################## # Acquisition mode functions ############################################################################## def _get_acquisition_mode(self): """ Getter method returning the current acquisition mode used by the camera. @return (str): acquisition mode """ return self._acquisition_mode # No getter in the DLL def _set_acquisition_mode(self, value): """ Setter method setting the acquisition mode used by the camera. @param (str|AcquisitionMode): Acquisition mode as a string or an object This method is not part of the interface, so we might need to use it from a script directly. Hence, here it is worth it to accept a string. """ if isinstance(value, str) and value in AcquisitionMode.__members__: value = AcquisitionMode[value] if not isinstance(value, AcquisitionMode): self.log.error('{} acquisition mode is not supported'.format(value)) return n_mode = ct.c_int(value.value) self._check(self._dll.SetAcquisitionMode(n_mode)) def get_exposure_time(self): """ Get the exposure time in seconds @return (float) : exposure time in s """ return self._get_acquisition_timings()['exposure'] def _get_acquisition_timings(self): """ Get the acquisitions timings from the dll @return (dict): dict containing keys 'exposure', 'accumulate', 'kinetic' and their values in seconds """ exposure, accumulate, kinetic = ct.c_float(), ct.c_float(), ct.c_float() self._check(self._dll.GetAcquisitionTimings(ct.byref(exposure), ct.byref(accumulate), ct.byref(kinetic))) return {'exposure': exposure.value, 'accumulate': accumulate.value, 'kinetic': kinetic.value} def set_exposure_time(self, value): """ Set the exposure time in seconds @param (float) value: desired new exposure time """ if value < 0: self.log.error('Exposure_time ({} s) can not be negative.'.format(value)) return self._check(self._dll.SetExposureTime(ct.c_float(value))) def get_gain(self): """ Get the gain @return (float): exposure gain """ return self._preamp_gain # No getter in the DLL def set_gain(self, value): """ Set the gain @param (float) value: New gain, value should be one in the constraints internal_gains list. """ if value not in self.get_constraints().internal_gains: self.log.error('gain value {} is not available.'.format(value)) return gain_index = self.get_constraints().internal_gains.index(value) self._check(self._dll.SetPreAmpGain(gain_index)) self._preamp_gain = value ############################################################################## # Trigger mode functions ############################################################################## def get_trigger_mode(self): """ Getter method returning the current trigger mode used by the camera. @return (str): current trigger mode """ return self._trigger_mode # No getter in the DLL def set_trigger_mode(self, value): """ Setter method for the trigger mode used by the camera. @param (str) value: trigger mode (must be compared to a dict) """ if value not in self.get_constraints().trigger_modes: self.log.error('Trigger mode {} is not declared by hardware.'.format(value)) return n_mode = TriggerMode[value].value status_code = self._check(self._dll.SetTriggerMode(n_mode)) if status_code == OK_CODE: self._trigger_mode = value ############################################################################## # Shutter mode functions ############################################################################## def get_shutter_state(self): """ Getter method returning the shutter state. @return (ShutterState): The current shutter state """ if not self.get_constraints().has_shutter: self.log.error('Can not get state of the shutter, camera does not have a shutter') return return self._shutter_status.name # No getter in the DLL def set_shutter_state(self, value): """ Setter method setting the shutter state. @param (ShutterState) value: the shutter state to set """ if not self.get_constraints().has_shutter: self.log.error('Can not set state of the shutter, camera does not have a shutter') conversion_dict = {ShutterState.AUTO: 0, ShutterState.OPEN: 1, ShutterState.CLOSED: 2} mode = conversion_dict[value] shutter_TTL = int(self._shutter_TTL) shutter_time = int(round(self._shutter_switching_time*1e3)) # DLL use ms status_code = self._check(self._dll.SetShutter(shutter_TTL, mode, shutter_time, shutter_time)) if status_code == OK_CODE: self._shutter_status = value ############################################################################## # Temperature functions ############################################################################## def get_cooler_on(self): """ Getter method returning the cooler status @return (bool): True if the cooler is on """ return self._cooler_status # No getter in the DLL def set_cooler_on(self, value): """ Setter method for the the cooler status @param (bool) value: True to turn it on, False to turn it off """ if value: status_code = self._dll.CoolerON() else: status_code = self._dll.CoolerOFF() self._check(status_code) if status_code == OK_CODE: self._cooler_status = value def get_temperature(self): """ Getter method returning the temperature of the camera. @return (float): temperature (in Kelvin) """ temperature = ct.c_float() self._dll.GetTemperatureF(ct.byref(temperature)) return temperature.value + 273.15 def get_temperature_setpoint(self): """ Getter method for the temperature setpoint of the camera. @return (float): Current setpoint in Kelvin """ return self._temperature_setpoint # Not present in Andor DLL def set_temperature_setpoint(self, value): """ Setter method for the the temperature setpoint of the camera. @param (float) value: New setpoint in Kelvin """ constraints = self.get_constraints().temperature if not(constraints.min < value < constraints.max): self.log.error('Temperature {} K is not in the validity range.'.format(value)) return temperature = int(round(value - 273.15)) status_code = self._check(self._dll.SetTemperature(temperature)) if status_code == OK_CODE: self._temperature_setpoint = temperature + 273.15 ############################################################################## # Internal functions, for constraints preparation ############################################################################## def _get_serial_number(self): """ Get the serial number of the camera as a string @return (str): serial number of the camera """ serial = ct.c_int() self._check(self._dll.GetCameraSerialNumber(ct.byref(serial))) return serial.value def _get_name(self): """ Get a name for the camera @return (str): local camera name with serial number """ return "Camera SN: {}".format(self._get_serial_number()) def _get_image_size(self): """ Returns the sensor size in pixels (width, height) @return tuple(int, int): number of pixel in width and height """ nx_px = ct.c_int() ny_px = ct.c_int() self._check(self._dll.GetDetector(ct.byref(nx_px), ct.byref(ny_px))) return nx_px.value, ny_px.value def _get_pixel_size(self): """ Get the physical pixel size (width, height) in meter @return tuple(float, float): physical pixel size in meter """ x_px = ct.c_float() y_px = ct.c_float() self._check(self._dll.GetPixelSize(ct.byref(x_px), ct.byref(y_px))) return y_px.value * 1e-6, x_px.value * 1e-6 def _get_temperature_range(self): """ Get the temperature minimum and maximum of the camera, in K @return tuple(float, float): The minimum minimum and maximum allowed for the setpoint in K """ mini = ct.c_int() maxi = ct.c_int() self._check(self._dll.GetTemperatureRange(ct.byref(mini), ct.byref(maxi))) return mini.value+273.15, maxi.value+273.15 def _get_available_gains(self): """ Return a list of the possible preamplifier gains @return (list(float)): A list of the gains supported by the camera """ number = ct.c_int() self._dll.GetNumberPreAmpGains(ct.byref(number)) gains = [] for i in range(number.value): gain = ct.c_float() self._check(self._dll.GetPreAmpGain(i, ct.byref(gain))) gains.append(gain.value) return gains def _get_available_speeds(self): """ Return a list of the possible readout speeds @return (list(float)): A list of the readout speeds supported by the camera """ number = ct.c_int() self._dll.GetNumberHSSpeeds(0, 0, ct.byref(number)) # Amplification: 0 = electron multiplication, 1 = conventional speeds = [] for i in range(number.value): speed = ct.c_float() self._check(self._dll.GetHSSpeed(0, 0, i, ct.byref(speed))) # AD Channel index, Amplification speeds.append(speed.value * 1e6) # DLL talks in MHz return speeds def _get_available_trigger_modes(self): """ Return a list of the trigger mode available to the camera @return list(str): A list of the trigger mode available to the dll """ modes = [] for mode in TriggerMode: status_code = self._dll.IsTriggerModeAvailable(mode.value) # The answer is encoded in the status code if status_code == OK_CODE: modes.append(mode.name) return modes def _has_shutter(self): """ Return if the camera have a mechanical shutter installed @return (bool): True if the camera have a shutter """ if self._has_external_shutter: return True result = ct.c_int() self._check(self._dll.IsInternalMechanicalShutter(ct.byref(result))) return bool(result.value) # 0: Mechanical shutter not installed, 1: Mechanical shutter installed. def _get_current_config(self): """ Internal helper method to get the camera parameters in a printable dict. @return (dict): dictionary with camera current configuration. """ config = { 'camera ID..................................': self._get_name(), 'sensor size (pixels).......................': self._get_image_size(), 'pixel size (m)............................': self._get_pixel_size(), 'acquisition mode...........................': self._get_acquisition_mode(), 'read mode..................................': self.get_read_mode().name, 'readout speed (Hz).........................': self.get_readout_speed(), 'gain (x)...................................': self.get_gain(), 'trigger_mode...............................': self.get_trigger_mode(), 'exposure_time..............................': self.get_exposure_time(), 'tracks definition (readmode = RANDOM TRACK)': self.get_active_tracks(), 'temperature (K)............................': self.get_temperature(), 'shutter_status.............................': self.get_shutter_state().name, } return config
class TimeTaggerODMRCounter(Base, ODMRCounterInterface): """ Using the TimeTagger as a slow counter. # TODO: Write a proper config Example config for copy-paste: tt_odmr: module.Class: 'timetagger__odmr_counter.TimeTaggerODMRCounter' timetagger_channel_apd_0: 0 timetagger_channel_apd_1: 1 timetagger_channel_trigger: 6 """ _modtype = 'TTCounter' _modclass = 'hardware' _channel_apd_0 = ConfigOption('timetagger_channel_apd_0', missing='error') _channel_apd_1 = ConfigOption('timetagger_channel_apd_1', None, missing='warn') _cw_channel_trigger = ConfigOption('cw_timetagger_channel_trigger', missing='error') _pulsed_channel_trigger = ConfigOption('pulsed_timetagger_channel_trigger', missing='error') sweep_length = 100 def on_activate(self): """ Start up TimeTagger interface """ self._tagger = tt.createTimeTagger() # Configuring if we are working with one APD or two if self._channel_apd_1 is None: self._mode = 1 else: self._mode = 2 def on_deactivate(self): """ Shut down the TimeTagger. """ self._tagger.reset() return 0 def set_up_odmr(self, counter_channel=None, photon_source=None, clock_channel=None, odmr_trigger_channel=None): """ Setting up the counters in the time tagger, depending on the mode set to it. """ """ This is the is the original implementation, that was changed due to difficulty using the combiner channel. It is a more elegant solution if these issues are resolved. """ # Default trigger levels are 0.5V. The channel with the splitter needs a higher trigger to avoid ghost counts. # self._tagger.setTriggerLevel(self._channel_apd_0, 1.0) # self._tagger.setTriggerLevel(self._channel_apd_1, 1.0) # self._tagger.setTriggerLevel(self._channel_trigger, 1.0) # Combine channel internally for both continuous and pulsed measurements self.trigger_combined = tt.Combiner(tagger=self._tagger, channels=[self._cw_channel_trigger, self._pulsed_channel_trigger]) self._channel_trigger = self.trigger_combined.getChannel() if self._mode == 1: self._channel_clicks = self._channel_apd_0 if self._mode == 2: self.combined = tt.Combiner(tagger=self._tagger, channels=[self._channel_apd_0, self._channel_apd_1]) self._channel_clicks = self.combined.getChannel() self.triggered_counter = tt.CountBetweenMarkers(tagger=self._tagger, click_channel=self._channel_clicks, begin_channel=self._channel_trigger, n_values=self.sweep_length) self.log.info('set up counter with {0} channels and sweep length {1}'.format(self._mode, self.sweep_length)) return 0 def get_counter_channels(self): return self._channel_clicks def get_constraints(self): """ Get hardware limits the device @return SlowCounterConstraints: constraints class for slow counter FIXME: ask hardware for limits when module is loaded """ # TODO: See if someone needs this method, at the moment it is not being used. constraints = SlowCounterConstraints() constraints.max_detectors = 2 constraints.min_count_frequency = 1e-3 constraints.max_count_frequency = 10e9 constraints.counting_mode = [CountingMode.CONTINUOUS] return constraints def count_odmr(self, length=100, pulsed=False): """ Obtains the counts from the count between markers method of the Time Tagger. Each bin belongs to a different frequency in the sweep. Returns the counts per second for each bin. The length argument is required by the interface, but not used here because it was already defined when the counter was initialized. """ t0 = time.time() t = 0 while (not self.triggered_counter.ready()) and (t < 5): time.sleep(0.1) t = time.time()-t0 if t >= 5: self.log.error('ODMR measurement timed out after {:03.2f} seconds'.format(t)) err = True count_rates = [] else: err = False count_array = self.triggered_counter.getData() bin_widths = self.triggered_counter.getBinWidths() count_rates = np.divide(count_array, (bin_widths * 10 ** -12)) # The count array intentionally discards the last bin because it isn't terminated by a pulse (at the end of # the average_factor number of repetitions. To allow for later reshaping of the array we add another cell # with zero. This should have very little effect on the counts. if pulsed: count_rates = np.append(count_rates, [0]) return err, count_rates def clear_odmr(self): """Clear the current measurement in the Time Tagger""" self.triggered_counter.clear() return 0 def get_counter(self, samples=None): """ Returns the current counts per second of the counter. @param int samples: if defined, number of samples to read in one go @return numpy.array(uint32): the photon counts per second """ # TODO: Implement this using counter, which requires setting up a clock and and a Counter return 0 # From here these are methods from the ODMR counter interface that need to be implemented for the Time Tagger def close_odmr(self): """ Close the odmr and clean up afterwards. @return int: error code (0:OK, -1:error) """ self._tagger.reset() return 0 def set_odmr_length(self, length=100): """ Set length of ODMR in pixels. It should be called before set_up_odmr to ensure array is created correctly. @param length: Length of ODMR in pixels @return: int: error code (0:OK, -1:error) """ self.sweep_length = length return 0 # These are useless methods, but the interface requires them def get_odmr_channels(self): # This sets the channels in the droplist next to the ODMR plot, but I don't think it is useful. ch = [1] return ch def set_up_odmr_clock(self, clock_frequency=None, clock_channel=None): return 0 def close_odmr_clock(self): return 0 def oversampling(self): pass def lock_in_active(self): pass
class CameraThorlabs(Base, CameraInterface): """ Main class of the module Example config for copy-paste: thorlabs_camera: module.Class: 'camera.thorlabs.thorlabs_DCx.CameraThorlabs' default_exposure: 0.1 default_gain: 1.0 id_camera: 0 # if more tha one camera is present """ _default_exposure = ConfigOption('default_exposure', 0.1) _default_gain = ConfigOption('default_gain', 1.0) _id_camera = ConfigOption('id_camera', 0) # if more than one camera is present _dll = None _camera_handle = None _exposure = _default_exposure _gain = _default_gain _width = 0 _height = 0 _pos_x = 0 _pos_y = 0 _bit_depth = 0 _cam = None _acquiring = False _live = False _last_acquisition_mode = None # useful if config changes during acq _sensor_info = None _image_memory = None _image_pid = None def on_activate(self): """ Initialisation performed during activation of the module. """ # Load the dll if present self._load_dll() self._connect_camera() self._init_camera() def _check_error(self, code, message): """ Check that the code means OK and log message as error if not. Return True if OK, False otherwise. """ if code != IS_SUCCESS: self.log.error(message) return False else: return True def _check_int_range(self, value, mini, maxi, message): """ Check that value is in the range [mini, maxi] and log message as error if not. Return True if OK. """ if value < mini or value > maxi: self.log.error('{} - Value {} must be between {} and {}'.format( message, value, mini, maxi)) return False else: return True def _load_dll(self): """ Load the dll for the camera """ try: if platform.system() == "Windows": if platform.architecture()[0] == "64bit": self._dll = ctypes.cdll.uc480_64 else: self._dll = ctypes.cdll.uc480 # for Linux elif platform.system() == "Linux": self._dll = ctypes.cdll.LoadLibrary('libueye_api.so') else: self.log.error( "Can not detect operating system to load Thorlabs DLL.") except OSError: self.log.error("Can not log Thorlabs DLL.") def _connect_camera(self): """ Connect to the camera and get basic info on it """ number_of_cameras = ctypes.c_int(0) self._dll.is_GetNumberOfCameras(byref(number_of_cameras)) if number_of_cameras.value < 1: self.log.error("No Thorlabs camera detected.") elif number_of_cameras.value - 1 < self._id_camera: self.log.error( "A Thorlabs camera has been detected but the id specified above the number of camera(s)" ) else: self._camera_handle = ctypes.c_int(0) ret = self._dll.is_InitCamera(ctypes.pointer(self._camera_handle)) self._check_error(ret, "Could not initialize camera") self._sensor_info = SENSORINFO() self._dll.is_GetSensorInfo(self._camera_handle, byref(self._sensor_info)) self.log.debug('Connected to camera : {}'.format( str(self._sensor_info.strSensorName))) def _init_camera(self): """ Set the parameters of the camera for our usage """ # Color mode code = self._dll.is_SetColorMode(self._camera_handle, ctypes.c_int(IS_SET_CM_Y8)) self._check_error(code, "Could set color mode IS_SET_CM_Y8") self._bit_depth = 8 # Image size self.set_image_size(self._sensor_info.nMaxWidth, self._sensor_info.nMaxHeight) # Image position self.set_image_position(0, 0) # Binning code = self._dll.is_SetBinning(self._camera_handle, ctypes.c_int(0)) # Disable binning self._check_error(code, "Could set binning disabled") # Sub sampling code = self._dll.is_SetSubSampling( self._camera_handle, ctypes.c_int(0)) # Disable sub sampling self._check_error(code, "Could set sub sampling disabled") # Allocate image memory self._image_pid = ctypes.c_int() self._image_memory = ctypes.c_char_p() code = self._dll.is_AllocImageMem(self._camera_handle, self._width, self._height, self._bit_depth, byref(self._image_memory), byref(self._image_pid)) self._check_error(code, "Could not allocate image memory") # Set image memory code = self._dll.is_SetImageMem(self._camera_handle, self._image_memory, self._image_pid) self._check_error(code, "Could not set image memory") # Set auto exit code = self._dll.is_EnableAutoExit(self._camera_handle, 1) # Enable auto-exit self._check_error(code, "Could not set auto exit") self.set_exposure(self._exposure) self.set_gain(self._gain) def set_image_size(self, width=None, height=None): """ Set the size of the image, here the camera will acquire only part of the image from a given position """ if width is not None: width = int(width) self._check_int_range(width, 1, self._sensor_info.nMaxWidth, 'Can not set image width') self._width = width if height is not None: height = int(height) self._check_int_range(height, 1, self._sensor_info.nMaxHeight, 'Can not set image height') self._height = height code = self._dll.is_SetImageSize(self._camera_handle, ctypes.c_int(self._width), ctypes.c_int(self._height)) return self._check_error(code, "Could not set image size") def set_image_position(self, pos_x, pos_y): """ Set image position reference coordinate """ if pos_x is not None: pos_x = int(pos_x) self._check_int_range(pos_x, 0, self._sensor_info.nMaxWidth - 1, 'Can not set image position x') self._pos_x = pos_x if pos_y is not None: pos_y = int(pos_y) self._check_int_range(pos_y, 0, self._sensor_info.nMaxHeight - 1, 'Can not set image position y') self._pos_y = pos_y code = self._dll.is_SetImagePos(self._camera_handle, ctypes.c_int(self._pos_x), ctypes.c_int(self._pos_y)) return self._check_error(code, "Could not set image position") def on_deactivate(self): """ Deinitialisation performed during deactivation of the module. """ self._dll.is_ExitCamera(self._camera_handle) self._acquiring = False self._live = False def get_name(self): """ Return a name for the camera """ return self._sensor_info.strSensorName def get_size(self): """ Return the max size of the camera """ return self._width, self._height def support_live_acquisition(self): """ Return whether or not this camera support live acquisition """ return True def start_live_acquisition(self): """ Set the camera in live mode """ if self.get_ready_state(): self._acquiring = True self._live = True code = self._dll.is_CaptureVideo(self._camera_handle, c_int(IS_DONT_WAIT)) no_error = self._check_error(code, "Could not start live acquisition") if not no_error: self._acquiring = False self._live = False return False return True else: return False def start_single_acquisition(self): """ Start the acquisition of a single image """ if self.get_ready_state(): self._acquiring = True code = self._dll.is_FreezeVideo(self._camera_handle, c_int(IS_WAIT)) self._acquiring = False return self._check_error(code, "Could not start single acquisition") else: return False def stop_acquisition(self): """ Stop live acquisition """ no_error = True if self._acquiring: code = self._dll.is_StopLiveVideo(self._camera_handle, c_int(IS_FORCE_VIDEO_STOP)) no_error = self._check_error(code, "Could not stop acquisition") self._acquiring = False self._live = False return no_error def get_acquired_data(self): """ Return last acquired data from the dll """ # Allocate memory for image: img_size = self._width * self._height c_array = ctypes.c_char * img_size c_img = c_array() # copy camera memory to accessible memory code = self._dll.is_CopyImageMem(self._camera_handle, self._image_memory, self._image_pid, c_img) self._check_error(code, "Could copy image to memory") # Convert to numpy 2d array of float from 0 to 1 img_array = np.frombuffer(c_img, dtype=ctypes.c_ubyte) img_array = img_array.astype(float) img_array.shape = np.array((self._height, self._width)) return img_array def get_bit_depth(self): """ Return the bit depth of the image """ return self._bit_depth def set_exposure(self, time): """ Set the exposure in second Return the new exposure """ exp = c_double(time * 1e3) # in ms new_exp = c_double(0) code = self._dll.is_SetExposureTime(self._camera_handle, exp, byref(new_exp)) self._check_error(code, "Could not set exposure") self._exposure = float(new_exp.value) / 1000 # in ms return self._exposure def get_exposure(self): """ Return current exposure """ return self._exposure def get_ready_state(self): """ Return whether or not the camera is ready for an acquisition """ if self.module_state() != 'idle': return False return not self._acquiring def set_gain(self, gain): """ Set the gain """ pass def get_gain(self): """ Get the gain """ return self._gain
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 MicrowaveAnritsu(Base, MicrowaveInterface): """ Hardware control file for Anritsu Devices. Tested for the model MG3691C with OPTION 2. cw and list modes are tested. Important: Trigger for frequency sweep is totally independent with most trigger syntax. In addition, it has to be Aux I/O pin connection. Example config for copy-paste: mw_source_anritsu_mg3691c: module.Class: 'microwave.mw_source_anritsu_mg3691.MicrowaveAnritsu' gpib_address: 'GPIB0::12::INSTR' gpib_timeout: 10 # in seconds """ _gpib_address = ConfigOption('gpib_address', missing='error') _gpib_timeout = ConfigOption('gpib_timeout', 10, missing='warn') # Indicate how fast frequencies within a list or sweep mode can be changed: _FREQ_SWITCH_SPEED = 0.009 # Frequency switching speed in s (acc. to specs) def on_activate(self): """ Initialisation performed during activation of the module. """ # trying to load the visa connection to the module self.rm = visa.ResourceManager() try: self._gpib_connection = self.rm.open_resource( self._gpib_address, timeout=self._gpib_timeout * 1000) except: self.log.error('This is MWanritsu: could not connect to the GPIB ' 'address >>{}<<.'.format(self._gpib_address)) raise self._gpib_connection.write('SYST:LANG "SCPI"') self.model = self._gpib_connection.query('*IDN?').split(',')[1] self.log.info('MicrowaveAnritsu initialised and connected to ' 'hardware.') def on_deactivate(self): """ Deinitialisation performed during deactivation of the module. """ self._gpib_connection.close() self.rm.close() def _command_wait(self, command_str): """ Writes the command in command_str via GPIB and waits until the device has finished processing it. @param command_str: The command to be written """ self._gpib_connection.write(command_str + '*WAI') return def get_limits(self): """ Right now, this is for Anritsu MG3691C with Option 2 only.""" limits = MicrowaveLimits() limits.supported_modes = (MicrowaveMode.CW, MicrowaveMode.LIST) limits.min_frequency = 10e6 limits.max_frequency = 20e9 limits.min_power = -130 limits.max_power = 30 limits.list_minstep = 0.001 limits.list_maxstep = 10e9 limits.list_maxentries = 10001 limits.sweep_minstep = 0.001 limits.sweep_maxstep = 10e9 limits.sweep_maxentries = 10001 if self.model == 'MG3961C': limits.max_frequency = 10e9 limits.min_frequency = 10e6 limits.min_power = -130 limits.max_power = 30 return limits def off(self): """ Switches off any microwave output. Must return AFTER the device is actually stopped. @return int: error code (0:OK, -1:error) """ self._gpib_connection.write('OUTP:STAT OFF') while int( self._gpib_connection.query('OUTP:STAT?').strip('\r\n')) != 0: time.sleep(0.2) return 0 def get_status(self): """ Gets the current status of the MW source, i.e. the mode (cw, list or sweep) and the output state (stopped, running) @return str, bool: mode ['cw', 'list', 'sweep'], is_running [True, False] """ is_running = bool( int(self._gpib_connection.query('OUTP:STAT?').strip('\r\n'))) mode = self._gpib_connection.query(':FREQ:MODE?').strip('\r\n') if mode == 'CW': mode = 'cw' if 'SWE' in mode: mode = 'sweep' if 'LIST' in mode: mode = 'list' return mode, is_running def get_power(self): """ Gets the microwave output power. @return float: the power set at the device in dBm """ if self.get_status()[0] == 'cw': power = float(self._gpib_connection.query(':POW?').strip('\r\n')) if self.get_status()[0] == 'list': self._command_wait(':LIST:IND 0') power = self._gpib_connection.query(':LIST:POW?').strip('\r\n') if self.get_status()[0] == 'sweep': power = self._gpib_connection.query(':POW?').strip('\r\n') return power def get_frequency(self): """ Gets the frequency of the microwave output. Returns single float value if the device is in cw mode. Returns list like [start, stop, step] if the device is in sweep mode. Returns list of frequencies if the device is in list mode. @return [float, list]: frequency(s) currently set for this device in Hz """ mode, is_running = self.get_status() if 'cw' in mode: return_val = float( self._gpib_connection.query(':FREQ?').strip('\r\n')) elif 'sweep' in mode: start = float( self._gpib_connection.query(':FREQ:STAR?').strip('\r\n')) stop = float( self._gpib_connection.query(':FREQ:STOP?').strip('\r\n')) step = float( self._gpib_connection.query(':SWE:FREQ:STEP?').strip('\r\n')) return_val = [start, stop, step] elif 'list' in mode: stop_index = int( self._gpib_connection.query(':LIST:STOP?').strip('\r\n')) self._gpib_connection.write(':LIST:IND {0}'.format(stop_index)) stop = float( self._gpib_connection.query(':LIST:FREQ?').strip('\r\n')) self._gpib_connection.write(':LIST:IND 0') start = float( self._gpib_connection.query(':LIST:FREQ?').strip('\r\n')) step = (stop - start) / stop_index return_val = np.arange(start, stop + step, step) return return_val def cw_on(self): """ Switches on any preconfigured microwave output. @return int: error code (0:OK, -1:error) """ mode, is_running = self.get_status() if is_running: if mode == 'cw': return 0 else: self.off() if mode != 'cw': self._command_wait(':FREQ:MODE CW') self._gpib_connection.write(':OUTP:STAT ON') dummy, is_running = self.get_status() while not is_running: time.sleep(0.2) dummy, is_running = self.get_status() return 0 def set_cw(self, frequency=None, power=None): """ Configures the device for cw-mode and optionally sets frequency and/or power @param float frequency: frequency to set in Hz @param float power: power to set in dBm @param bool useinterleave: If this mode exists you can choose it. @return float, float, str: current frequency in Hz, current power in dBm, current mode Interleave option is used for arbitrary waveform generator devices. """ mode, is_running = self.get_status() if is_running: self.off() if mode != 'cw': self._command_wait(':FREQ:MODE CW') if frequency is not None: self._command_wait(':FREQ {0:f}'.format(frequency)) if power is not None: self._command_wait(':POW {0:.2f}'.format(power)) mode, dummy = self.get_status() actual_freq = self.get_frequency() actual_power = self.get_power() return actual_freq, actual_power, mode def list_on(self): """ Switches on the list mode microwave output. Must return AFTER the device is actually running. @return int: error code (0:OK, -1:error) """ mode, is_running = self.get_status() if is_running: if mode == 'list': return 0 else: self.off() if mode != 'list': self._command_wait(':FREQ:MODE LIST') self._gpib_connection.write(':LIST:MODE MNT') self._gpib_connection.write(':OUTP:STAT ON') dummy, is_running = self.get_status() while not is_running: time.sleep(0.2) dummy, is_running = self.get_status() return 0 def set_list(self, frequency=None, power=None): """ Configures the device for list-mode and optionally sets frequencies and/or power @param list frequency: list of frequencies in Hz @param float power: MW power of the frequency list in dBm @return list, float, str: current frequencies in Hz, current power in dBm, current mode """ mode, is_running = self.get_status() if is_running: self.off() if mode != 'list': self._command_wait(':FREQ:MODE LIST') self._command_wait(':LIST:IND 0') if frequency is not None: s = ' {0:f},'.format(frequency[0]) for f in frequency[:-1]: s += ' {0:f},'.format(f) s += ' {0:f}'.format(frequency[-1]) self._command_wait(':LIST:FREQ' + s) self._command_wait(':LIST:STAR 0') self._command_wait(':LIST:STOP {0}'.format(len(frequency) - 1)) self._gpib_connection.write(':LIST:MODE MAN') if power is not None: self._command_wait(':LIST:IND 0') self._command_wait( ':LIST:POW {0}'.format((len(frequency) - 1) * (str(power) + ', ') + str(power))) self._command_wait(':LIST:IND 0') actual_power = self.get_power() actual_freq = self.get_frequency() mode, dummy = self.get_status() return actual_freq, actual_power, mode def reset_listpos(self): """ Reset of MW list mode position to start (first frequency step) @return int: error code (0:OK, -1:error) """ self._command_wait(':LIST:IND 0') return 0 def sweep_on(self): """ Switches on the sweep mode. @return int: error code (0:OK, -1:error) """ return -1 def set_sweep(self, start=None, stop=None, step=None, power=None): """ Configures the device for sweep-mode and optionally sets frequency start/stop/step and/or power @return float, float, float, float, str: current start frequency in Hz, current stop frequency in Hz, current frequency step in Hz, current power in dBm, current mode """ actual_power = self.get_power() mode, dummy = self.get_status() return -1, -1, -1, actual_power, mode def reset_sweeppos(self): """ Reset of MW sweep mode position to start (start frequency) @return int: error code (0:OK, -1:error) """ return -1 def set_ext_trigger(self, pol, timing): """ Set the external trigger for this device with proper polarization. @param TriggerEdge pol: polarisation of the trigger (basically rising edge or falling edge) @param float timing: estimated time between triggers @return object, float: current trigger polarity [TriggerEdge.RISING, TriggerEdge.FALLING], trigger timing """ if pol == TriggerEdge.RISING: edge = 'POS' elif pol == TriggerEdge.FALLING: edge = 'NEG' else: self.log.warning( 'No valid trigger polarity passed to microwave hardware module.' ) edge = None if edge is not None: self._command_wait(':TRIG:SEQ3:SLOP {0}'.format(edge)) polarity = self._gpib_connection.query(':TRIG:SEQ3:SLOP?').strip( '\r\n') if polarity == 'NEG': return TriggerEdge.FALLING, timing else: return TriggerEdge.RISING, timing def trigger(self): """ Trigger the next element in the list or sweep mode programmatically. @return int: error code (0:OK, -1:error) Ensure that the Frequency was set AFTER the function returns, or give the function at least a save waiting time. """ # WARNING: # The manual trigger functionality was not tested for this device! # Might not work well! Please check that! self._gpib_connection.write('*TRG') time.sleep(self._FREQ_SWITCH_SPEED) # that is the switching speed return 0
class FastComtec(Base, FastCounterInterface): """ Hardware Class for the FastComtec Card. This module is also compatible with model 7889 by specifying the model config option. Example config for copy-paste: fastcomtec_p7887: module.Class: 'fastcomtec.fastcomtecp7887.FastComtec' gated: False trigger_safety: 200e-9 aom_delay: 400e-9 minimal_binwidth: 0.25e-9 """ gated = ConfigOption('gated', False, missing='warn') trigger_safety = ConfigOption('trigger_safety', 200e-9, missing='warn') aom_delay = ConfigOption('aom_delay', 400e-9, missing='warn') minimal_binwidth = ConfigOption('minimal_binwidth', 0.25e-9, missing='warn') model = ConfigOption('model', '7887') use_dma = ConfigOption('use_dma', False) 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])) #this variable has to be added because there is no difference #in the fastcomtec it can be on "stopped" or "halt" self.stopped_or_halt = "stopped" self.timetrace_tmp = [] def on_activate(self): """ Initialisation performed during activation of the module. """ dll_name = 'dp{}.dll'.format(self.model) self.dll = ctypes.windll.LoadLibrary(dll_name) if self.gated: self.change_sweep_mode(gated=True) else: self.change_sweep_mode(gated=False) return def on_deactivate(self): """ Deinitialisation performed during deactivation of the module. """ return def get_constraints(self): """ Retrieve the hardware constrains from the Fast counting device. @return dict: dict with keys being the constraint names as string and items are the definition for the constaints. The keys of the returned dictionary are the str name for the constraints (which are set in this method). NO OTHER KEYS SHOULD BE INVENTED! 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 all files containing this interface. The items of the keys are again dictionaries which have the generic dictionary form: {'min': <value>, 'max': <value>, 'step': <value>, 'unit': '<value>'} Only the key 'hardware_binwidth_list' differs, since they contain the list of possible binwidths. If the constraints cannot be set in the fast counting hardware then write just zero to each key of the generic dicts. Note that there is a difference between float input (0.0) and integer input (0), because some logic modules might rely on that distinction. ALL THE PRESENT KEYS OF THE CONSTRAINTS DICT MUST BE ASSIGNED! """ constraints = dict() # the unit of those entries are seconds per bin. In order to get the # current binwidth in seconds use the get_binwidth method. constraints['hardware_binwidth_list'] = list( self.minimal_binwidth * (2**np.array(np.linspace(0, 24, 25)))) constraints['max_sweep_len'] = 6.8 return constraints def configure(self, bin_width_s, record_length_s, number_of_gates=0, filename=None): """ Configuration of the fast counter. @param float bin_width_s: Length of a single time bin in the time trace histogram in seconds. @param float record_length_s: Total length of the timetrace/each single gate in seconds. @param int number_of_gates: optional, number of gates in the pulse sequence. Ignore for not gated counter. @return tuple(binwidth_s, record_length_s, number_of_gates): binwidth_s: float the actual set binwidth in seconds gate_length_s: the actual record length in seconds number_of_gates: the number of gated, which are accepted, None if not-gated """ # when not gated, record length = total sequence length, when gated, record length = laser length. # subtract 200 ns to make sure no sequence trigger is missed record_length_FastComTech_s = record_length_s if self.gated: # add time to account for AOM delay no_of_bins = int((record_length_FastComTech_s + self.aom_delay) / self.set_binwidth(bin_width_s)) else: # subtract time to make sure no sequence trigger is missed no_of_bins = int( (record_length_FastComTech_s - self.trigger_safety) / self.set_binwidth(bin_width_s)) self.set_length(no_of_bins, preset=1, cycles=number_of_gates) if filename is not None: self._change_filename(filename) return self.get_binwidth( ), record_length_FastComTech_s, number_of_gates def get_status(self): """ Receives the current status of the Fast Counter and outputs it as return value. 0 = unconfigured 1 = idle 2 = running 3 = paused -1 = error state """ status = AcqStatus() self.dll.GetStatusData(ctypes.byref(status), 0) if status.started == 1: return 2 elif status.started == 0: if self.stopped_or_halt == "stopped": return 1 elif self.stopped_or_halt == "halt": return 3 else: self.log.error('FastComTec neither stopped nor halt') return -1 else: self.log.error( 'There is an unknown status from FastComtec. The status message was %s' % (str(status.started))) return -1 def get_current_runtime(self): """ Returns the current runtime. @return float runtime: in s """ status = AcqStatus() self.dll.GetStatusData(ctypes.byref(status), 0) return status.runtime def get_current_sweeps(self): """ Returns the current sweeps. @return int sweeps: in sweeps The fastcomtec has "start" and "sweep" parameters that are generally equal but might differ depending on the configuration. Here the number of trigger events is called "start". This is what is meant by the "sweep" parameter of the fast_counter interface. """ status = AcqStatus() self.dll.GetStatusData(ctypes.byref(status), 0) return status.stevents # the number of trigger is named "stevents". def start_measure(self): """Start the measurement. """ status = self.dll.Start(0) while self.get_status() != 2: time.sleep(0.05) return status def pause_measure(self): """Make a pause in the measurement, which can be continued. """ self.stopped_or_halt = "halt" status = self.dll.Halt(0) while self.get_status() != 3: time.sleep(0.05) if self.gated: self.timetrace_tmp = self.get_data_trace() return status def stop_measure(self): """Stop the measurement. """ self.stopped_or_halt = "stopped" status = self.dll.Halt(0) while self.get_status() != 1: time.sleep(0.05) if self.gated: self.timetrace_tmp = [] return status def continue_measure(self): """Continue a paused measurement. """ if self.gated: status = self.start_measure() else: status = self.dll.Continue(0) while self.get_status() != 2: time.sleep(0.05) return status def get_binwidth(self): """ Returns the width of a single timebin in the timetrace in seconds. @return float: current length of a single bin in seconds (seconds/bin) The red out bitshift will be converted to binwidth. The binwidth is defined as 2**bitshift*minimal_binwidth. """ return self.minimal_binwidth * (2**int(self.get_bitshift())) def is_gated(self): """ Check the gated counting possibility. @return bool: Boolean value indicates if the fast counter is a gated counter (TRUE) or not (FALSE). """ return self.gated def get_data_trace(self): """ Polls the current timetrace data from the fast counter and returns it as a numpy array (dtype = int64). The binning specified by calling configure() must be taken care of in this hardware class. A possible overflow of the histogram bins must be caught here and taken care of. If the counter is UNgated it will return a 1D-numpy-array with returnarray[timebin_index] If the counter is gated it will return a 2D-numpy-array with returnarray[gate_index, timebin_index] @return arrray: Time trace. """ setting = AcqSettings() self.dll.GetSettingData(ctypes.byref(setting), 0) N = setting.range if self.gated: bsetting = AcqSettings() self.dll.GetSettingData(ctypes.byref(bsetting), 0) H = bsetting.cycles data = np.empty((H, int(N / H)), dtype=np.uint32) else: data = np.empty((N, ), dtype=np.uint32) p_type_ulong = ctypes.POINTER(ctypes.c_uint32) ptr = data.ctypes.data_as(p_type_ulong) self.dll.LVGetDat(ptr, 0) time_trace = np.int64(data) if self.gated and self.timetrace_tmp != []: time_trace = time_trace + self.timetrace_tmp info_dict = { 'elapsed_sweeps': self.get_current_sweeps(), 'elapsed_time': None } return time_trace, info_dict def get_data_testfile(self): """ Load data test file """ data = np.loadtxt( os.path.join(get_main_dir(), 'tools', 'FastComTec_demo_timetrace.asc')) time.sleep(0.5) return data # ========================================================================= # Non Interface methods # ========================================================================= def get_bitshift(self): """Get bitshift from Fastcomtec. @return int settings.bitshift: the red out bitshift """ settings = AcqSettings() self.dll.GetSettingData(ctypes.byref(settings), 0) return int(settings.bitshift) def set_bitshift(self, bitshift): """ Sets the bitshift properly for this card. @param int bitshift: @return int: asks the actual bitshift and returns the red out value """ cmd = 'BITSHIFT={0}'.format(bitshift) self.dll.RunCmd(0, bytes(cmd, 'ascii')) return self.get_bitshift() def set_binwidth(self, binwidth): """ Set defined binwidth in Card. @param float binwidth: the current binwidth in seconds @return float: Red out bitshift converted to binwidth The binwidth is converted into to an appropiate bitshift defined as 2**bitshift*minimal_binwidth. """ bitshift = int(np.log2(binwidth / self.minimal_binwidth)) new_bitshift = self.set_bitshift(bitshift) return self.minimal_binwidth * (2**new_bitshift) #TODO: Check such that only possible lengths are set. def set_length(self, length_bins, preset=None, cycles=None, sequences=None): """ Sets the length of the length of the actual measurement. @param int length_bins: Length of the measurement in bins @return float: Red out length of measurement """ constraints = self.get_constraints() if length_bins * self.get_binwidth() < constraints['max_sweep_len']: cmd = 'RANGE={0}'.format(int(length_bins)) self.dll.RunCmd(0, bytes(cmd, 'ascii')) cmd = 'roimax={0}'.format(int(length_bins)) self.dll.RunCmd(0, bytes(cmd, 'ascii')) if preset is not None: cmd = 'swpreset={0}'.format(preset) self.dll.RunCmd(0, bytes(cmd, 'ascii')) if cycles: cmd = 'cycles={0}'.format(cycles) self.dll.RunCmd(0, bytes(cmd, 'ascii')) if sequences: cmd = 'sequences={0}'.format(sequences) self.dll.RunCmd(0, bytes(cmd, 'ascii')) return self.get_length() else: self.log.error('Length of sequence is too high: %s' % (str(length_bins * self.get_binwidth()))) return -1 def set_preset(self, preset): cmd = 'swpreset={0}'.format(preset) self.dll.RunCmd(0, bytes(cmd, 'ascii')) return preset def set_cycles(self, cycles): cmd = 'cycles={0}'.format(cycles) self.dll.RunCmd(0, bytes(cmd, 'ascii')) return cycles def get_length(self): """ Get the length of the current measurement. @return int: length of the current measurement """ setting = AcqSettings() self.dll.GetSettingData(ctypes.byref(setting), 0) return int(setting.range) def _change_filename(self, name): """ Changed the name in FCT""" cmd = 'datname=%s' % name self.dll.RunCmd(0, bytes(cmd, 'ascii')) return name def change_sweep_mode(self, gated): if gated: number = 1978500 number = number + 32 if self.use_dma else number cmd = 'sweepmode={0}'.format(hex(number)) self.dll.RunCmd(0, bytes(cmd, 'ascii')) cmd = 'prena={0}'.format(hex(16)) #To select starts preset # cmd = 'prena={0}'.format(hex(4)) #To select sweeps preset self.dll.RunCmd(0, bytes(cmd, 'ascii')) self.gated = True else: # fastcomtch standard settings for ungated acquisition (check manual) number = 1978496 number = number + 32 if self.use_dma else number cmd = 'sweepmode={0}'.format(hex(number)) self.dll.RunCmd(0, bytes(cmd, 'ascii')) cmd = 'prena={0}'.format(hex(0)) self.dll.RunCmd(0, bytes(cmd, 'ascii')) self.gated = False return gated def change_save_mode(self, mode): """ Changes the save mode of p7887 @param int mode: Specifies the save mode (0: No Save at Halt, 1: Save at Halt, 2: Write list file, No Save at Halt, 3: Write list file, Save at Halt @return int mode: specified save mode """ cmd = 'savedata={0}'.format(mode) self.dll.RunCmd(0, bytes(cmd, 'ascii')) return mode def set_delay_start(self, delay_s): """ Sets the record delay length @param int delay_s: Record delay after receiving a start trigger @return int mode: specified save mode """ # A delay can only be adjusted in steps of 6.4ns delay_bins = np.rint(delay_s / 6.4e-9 / 2.5) cmd = 'fstchan={0}'.format(int(delay_bins)) self.dll.RunCmd(0, bytes(cmd, 'ascii')) return delay_bins def get_delay_start(self): """ Returns the current record delay length @return float delay_s: current record delay length in seconds """ bsetting = AcqSettings() self.dll.GetSettingData(ctypes.byref(bsetting), 0) delay_s = bsetting.fstchan * 6.4e-9 * 2.5 #prena = bsetting.prena return delay_s # ========================================================================= # The following methods have to be carefully reviewed and integrated as # internal methods/function, because they might be important one day. # ========================================================================= def SetDelay(self, t): #~ setting = AcqSettings() #~ self.dll.GetSettingData(ctypes.byref(setting), 0) #~ setting.fstchan = t/6.4 #~ self.dll.StoreSettingData(ctypes.byref(setting), 0) #~ self.dll.NewSetting(0) self.dll.RunCmd(0, 'DELAY={0:f}'.format(t)) return self.GetDelay() def GetDelay(self): setting = AcqSettings() self.dll.GetSettingData(ctypes.byref(setting), 0) return setting.fstchan * 6.4 #former SaveData_fast def SaveData_locally(self, filename, laser_index): # os.chdir(r'D:\data\FastComTec') data = self.get_data() fil = open(filename + '.asc', 'w') for i in laser_index: for n in data[i:i + int( round(3000 / (self.minimal_binwidth * 2**self.GetBitshift()))) + int( round(1000 / (self.minimal_binwidth * 2**self.GetBitshift())))]: fil.write('{0!s}\n'.format(n)) fil.close() def SetLevel(self, start, stop): setting = AcqSettings() self.dll.GetSettingData(ctypes.byref(setting), 0) def FloatToWord(r): return int((r + 2.048) / 4.096 * int('ffff', 16)) setting.dac0 = (setting.dac0 & int('ffff0000', 16)) | FloatToWord(start) setting.dac1 = (setting.dac1 & int('ffff0000', 16)) | FloatToWord(stop) self.dll.StoreSettingData(ctypes.byref(setting), 0) self.dll.NewSetting(0) return self.GetLevel() def GetLevel(self): setting = AcqSettings() self.dll.GetSettingData(ctypes.byref(setting), 0) def WordToFloat(word): return (word & int('ffff', 16)) * 4.096 / int('ffff', 16) - 2.048 return WordToFloat(setting.dac0), WordToFloat(setting.dac1) #used in one script for SSR #Todo: Remove def Running(self): s = self.GetStatus() return s.started def GetStatus(self): status = AcqStatus() self.dll.GetStatusData(ctypes.byref(status), 0) return status def load_setup(self, configname): cmd = 'loadcnf={0}'.format(configname) self.dll.RunCmd(0, bytes(cmd, 'ascii'))
class HbtLogic(GenericLogic): """ This is the logic for running HBT experiments """ _modclass = 'hbtlogic' _modtype = 'logic' _channel_apd_0 = ConfigOption('timetagger_channel_apd_0', missing='error') _channel_apd_1 = ConfigOption('timetagger_channel_apd_1', missing='error') _bin_width = ConfigOption('bin_width', 500, missing='info') _n_bins = ConfigOption('bins', 2000, missing='info') savelogic = Connector(interface='SaveLogic') hbt_updated = QtCore.Signal() hbt_fit_updated = QtCore.Signal() hbt_saved = QtCore.Signal() sigStart = QtCore.Signal() sigStop = QtCore.Signal() def __init__(self, config, **kwargs): super().__init__(config=config, **kwargs) self.fit_times = [] self.bin_times = [] self.fit_g2 = [] self.g2_data = [] self.g2_data_normalised = [] self.hbt_available = False self._setup_measurement() self._close_measurement() def on_activate(self): """ Connect and configure the access to the FPGA. """ self._save_logic = self.get_connector('savelogic') self._number_of_gates = int(100) self.g2_data = np.zeros_like(self.bin_times) self.g2_data_normalised = np.zeros_like(self.bin_times) self.fit_times = self.bin_times self.fit_g2 = np.zeros_like(self.fit_times) self.timer = QtCore.QTimer() self.timer.timeout.connect(self.update) self.sigStart.connect(self._start_hbt) self.sigStop.connect(self._stop_hbt) def _setup_measurement(self): self._tagger = tt.createTimeTagger() self.coin = tt.Correlation(self._tagger, self._channel_apd_0, self._channel_apd_1, binwidth=self._bin_width, n_bins=self._n_bins) self.bin_times = self.coin.getIndex() def _close_measurement(self): self.coin.stop() self.coin = None self._tagger = None def start_hbt(self): self.sigStart.emit() def stop_hbt(self): self.sigStop.emit() def _start_hbt(self): self._setup_measurement() self.coin.clear() self.coin.start() self.timer.start(500) # 0.5s def update(self): self.bin_times = self.coin.getIndex() self.g2_data = self.coin.getData() self.hbt_available = True lvl = np.mean(self.g2_data[0:100]) if lvl > 0: self.g2_data_normalised = self.g2_data / lvl else: self.g2_data_normalised = np.zeros_like(self.g2_data) self.hbt_updated.emit() def pause_hbt(self): if self.coin is not None: self.coin.stop() def continue_hbt(self): if self.coin is not None: self.coin.start() def _stop_hbt(self): if self.coin is not None: self._close_measurement() self.timer.stop() def fit_data(self): pass # 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 = 100.0 # param['P_sat'].min = 0.0 # param['P_sat'].value = 1.0 # param['slope'].min = 0.0 # param['slope'].value = 1e3 # param['offset'].min = 0.0 # fit = self.fitlogic.make_hyperbolicsaturation_fit(x_axis=self.psat_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'] def save_hbt(self): # File path and name filepath = self._save_logic.get_path_for_module(module_name='HBT') # We will fill the data OrderedDict to send to savelogic data = OrderedDict() data['Time (ns)'] = np.array(self.bin_times) data['g2(t)'] = np.array(self.g2_data) data['g2(t) normalised'] = np.array(self.g2_data_normalised) self._save_logic.save_data(data, filepath=filepath, filelabel='g2data', fmt=['%.6e', '%.6e', '%.6e']) self.log.debug('HBT data saved to:\n{0}'.format(filepath)) self.hbt_saved.emit() return 0 def on_deactivate(self): """ Reverse steps of activation @return int: error code (0:OK, -1:error) """ return 0
class ATC204(Base, ProcessInterface, ProcessControlInterface): """ Methods to control Arios ATC-204(東邦電子 TTM-204). https://dia-pe-titech.esa.io/posts/158 Example config for copy-paste: APC: module.Class: 'ATC204_pressure.ATC204' port: 'COM4' baudrate: 9600 timeout: 100 limit_min: 0 # Pa limit_max: 40e3 # Pa """ _port = ConfigOption('port') _baudrate = ConfigOption('baudrate', 9600, missing='warn') _timeout = ConfigOption('timeout', 10, missing='warn') _limit_min = ConfigOption('limit_min', 0, missing='warn') _limit_max = ConfigOption('limit_max', 40e3, missing='warn') CMD_RETRY_NUM = 10 CMD_ZERO_NUM = 10 STX = b'\x02' ETX = b'\x03' def on_activate(self): """ Activate module. """ self._serial_connection = serial.Serial( port=self._port, baudrate=self._baudrate, bytesize=serial.EIGHTBITS, parity=serial.PARITY_NONE, stopbits=serial.STOPBITS_TWO, timeout=self._timeout) self._openflag = self._serial_connection.is_open if not(self._openflag): self._serial_connection.open() # なぜか初期値が0.2になってしまうのでリセット if self.get_control_value() == .2e3: self.set_control_value(0.0) def on_deactivate(self): """ Deactivate module. """ if not(self._openflag): self._serial_connection.close() def get_process_unit(self, channel=None): """ Process unit, here Pa. @return float: process unit """ return 'Pa', 'pascal' def get_control_unit(self, channel=None): """ Get unit of control value. @return tuple(str): short and text unit of control value """ return 'Pa', 'pascal' def get_control_limit(self, channel=None): """ Get minimum and maximum of control value. @return tuple(float, float): minimum and maximum of control value """ return self._limit_min, self._limit_max def get_minimal_step(self): return 100.0 def get_process_value(self, channel=None): """ Process value, here temperature. @return float: process value """ return self._send_cmd('01RPV1') * 1e3 def get_control_value(self, channel=None): """ Get current control value, here heating power @return float: current control value """ return self._send_cmd('01RSV1') * 1e3 def set_control_value(self, value, channel=None): """ Set control value, here heating power. @param flaot value: control value """ set_value = round(value / 1e3 * 10) cmd = '01WSV1' + str(set_value).zfill(5) # '01WSV1' + '{:0=5}'.format( set_value ) でも同じ結果になる return self._send_cmd(cmd) * 1e3 # print(cmd) # return True def _send_cmd(self, cmd): """ Send command """ cmd = self.STX + cmd.encode() + self.ETX bcc = 0 for data in cmd: bcc = bcc ^ data final_cmd = cmd + bytes([bcc]) for i in range(self.CMD_RETRY_NUM): # if self._serial_connection.out_waiting > 0: # self._serial_connection.reset_output_buffer() self._serial_connection.write(final_cmd) res = self._read_data() if not res: return False elif res == b'\x0201\x06\x03\x06': return True else: try: return float(res[7:12].decode()) / 10 except ValueError: # clear receive buffer self._serial_connection.reset_input_buffer() self.log.warning('clear receive buffer') self.log.warning('cmd res error:', res) return False def _read_data(self): in_reading = True in_resiving_data = False _num_of_zero = 0 data = b'' while in_reading: res = self._serial_connection.read(1) if res == b'\x00' or res == b'': _num_of_zero += 1 if _num_of_zero >= self.CMD_ZERO_NUM: self.log.error( self.__class__.__name__ + '> Communication Error: This hardware might be off!') return False elif res == self.STX: in_resiving_data = True data = res elif in_resiving_data: if res == self.ETX: in_resiving_data = False in_reading = False data += res # BCC res = self._serial_connection.read(1) data += res return data
class PM100D(Base, SimpleDataInterface, ProcessInterface): """ Hardware module for Thorlabs PM100D powermeter. Example config : powermeter: module.Class: 'powermeter.PM100D.PM100D' address: 'USB0::0x1313::0x8078::P0013645::INSTR' This module needs the ThorlabsPM100 package from PyPi, this package is not included in the environment To add install it, type : pip install ThorlabsPM100 in the Anaconda prompt after having activated qudi environment """ _address = ConfigOption('address', missing='error') _timeout = ConfigOption('timeout', 1) _power_meter = None def on_activate(self): """ Startup the module """ rm = visa.ResourceManager() try: self._inst = rm.open_resource(self._address, timeout=self._timeout) except: self.log.error( 'Could not connect to hardware. Please check the wires and the address.' ) self._power_meter = ThorlabsPM100(inst=self._inst) def on_deactivate(self): """ Stops the module """ self._inst.close() def getData(self): """ SimpleDataInterface function to get the power from the powermeter """ return np.array([self.get_power()]) def getChannels(self): """ SimpleDataInterface function to know how many data channel the device has, here 1. """ return 1 def get_power(self): """ Return the power read from the ThorlabsPM100 package """ return self._power_meter.read def get_process_value(self): """ Return a measured value """ return self.get_power() def get_process_unit(self): """ Return the unit that hte value is measured in as a tuple of ('abreviation', 'full unit name') """ return ('W', 'watt') def get_wavelength(self): """ Return the current wavelength in nanometers """ return self._power_meter.sense.correction.wavelength def set_wavelength(self, value=None): """ Set the new wavelength in nanometers """ mini, maxi = self.get_wavelength_range() if value is not None: if mini <= value <= maxi: self._power_meter.sense.correction.wavelength = value else: self.log.error( 'Wavelength {} is out of the range [{}, {}].'.format( value, mini, maxi)) return self.get_wavelength() def get_wavelength_range(self): """ Return the wavelength range of the power meter in nanometers """ return self._power_meter.sense.correction.minimum_beamdiameter,\ self._power_meter.sense.correction.maximum_wavelength def set_autorange(self, value=1): """ Set the powermeter power range in automatic""" if value == 1: self._power_meter.sense.power.dc.range.auto = 1 elif value == 0: self._power_meter.sense.power.dc.range.auto = 0 else: print("not a valid command, autorange is {}".format( self._power_meter.sense.power.dc.range.auto)) return self._power_meter.sense.power.dc.range.auto
class AutofocusLogic(GenericLogic): """ This logic connect to the instruments necessary for the autofocus method based on the FPGA + QPD. This logic can be accessed from the focus_logic controlling the piezo position. autofocus_logic: module.Class: 'autofocus_logic.AutofocusLogic' autofocus_ref_axis : 'X' # 'Y' proportional_gain : 0.1 # in %% integration_gain : 1 # in %% exposure = 0.001 connect: camera : 'thorlabs_camera' fpga: 'nifpga' stage: 'ms2000' """ # declare connectors fpga = Connector(interface='FPGAInterface') # to check _ a new interface was defined for FPGA connection stage = Connector(interface='MotorInterface') camera = Connector(interface='CameraInterface') # camera attributes _exposure = ConfigOption('exposure', 0.001, missing='warn') _camera_acquiring = False _threshold = None # for compatibility with focus logic, not used # autofocus attributes _focus_offset = ConfigOption('focus_offset', 0, missing='warn') _ref_axis = ConfigOption('autofocus_ref_axis', 'X', missing='warn') _autofocus_stable = False _autofocus_iterations = 0 # pid attributes _pid_frequency = 0.2 # in s, frequency for the autofocus PID update _P_gain = ConfigOption('proportional_gain', 0, missing='warn') _I_gain = ConfigOption('integration_gain', 0, missing='warn') _setpoint = None _last_pid_output_values = np.zeros((10,)) # signals sigOffsetDefined = QtCore.Signal() sigStageMoved = QtCore.Signal() def __init__(self, config, **kwargs): super().__init__(config=config, **kwargs) # self.threadpool = QtCore.QThreadPool() def on_activate(self): """ Initialisation performed during activation of the module. """ # hardware connections self._fpga = self.fpga() self._stage = self.stage() self._camera = self.camera() self._camera.set_exposure(self._exposure) def on_deactivate(self): """ Required deactivation. """ self.stop_camera_live() # ======================================================================================= # Public method for the autofocus, used by all the methods (camera or FPGA/QPD based) # ======================================================================================= def read_detector_signal(self): """ General function returning the reference signal for the autofocus correction. In the case of the method using a FPGA, it returns the QPD signal measured along the reference axis. """ return self.qpd_read_position() # fpga only def read_detector_intensity(self): """ Function used for the focus search. Measured the intensity of the reflection instead of reading its position """ return self.qpd_read_sum() def autofocus_check_signal(self): """ Check that the intensity detected by the QPD is above a specific threshold (300). If the signal is too low, the function returns False to indicate that the autofocus signal is lost. :return bool: True: signal ok, False: signal too low """ qpd_sum = self.qpd_read_sum() print(qpd_sum) if qpd_sum < 300: return False else: return True def define_pid_setpoint(self): """ Initialize the pid setpoint """ self.qpd_reset() self._setpoint = self.read_detector_signal() return self._setpoint def init_pid(self): """ Initialize the pid for the autofocus """ self.qpd_reset() self._fpga.init_pid(self._P_gain, self._I_gain, self._setpoint, self._ref_axis) self.set_worker_frequency() self._autofocus_stable = False self._autofocus_iterations = 0 def read_pid_output(self, check_stabilization): """ Read the pid output signal in order to adjust the position of the objective """ pid_output = self._fpga.read_pid() if check_stabilization: self._autofocus_iterations += 1 self._last_pid_output_values = np.concatenate((self._last_pid_output_values[1:10], [pid_output])) return pid_output, self.check_stabilization() else: return pid_output def check_stabilization(self): """ Check for the stabilization of the focus """ if self._autofocus_iterations > 10: p = Poly.fit(np.linspace(0, 9, num=10), self._last_pid_output_values, deg=1) slope = p(9) - p(0) if np.absolute(slope) < 10: self._autofocus_stable = True else: self._autofocus_stable = False return self._autofocus_stable def start_camera_live(self): """ Launch live acquisition of the camera """ self._camera.start_live_acquisition() self._camera_acquiring = True def stop_camera_live(self): """ Stop live acquisition of the camera """ self._camera.stop_acquisition() self._camera_acquiring = False def get_latest_image(self): """ Get the latest acquired image from the camera. This function returns the raw image as well as the threshold image """ im = self._camera.get_acquired_data() return im def calibrate_offset(self): """ Calibrate the offset between the sample position and a reference on the bottom of the coverslip. This method is inspired from the LSM-Zeiss microscope and is used when the sample (such as embryos) is interfering too much with the IR signal and makes the regular focus stabilization unstable. """ # Read the stage position z_up = self._stage.get_pos()['z'] offset = self._focus_offset # Move the stage by the default offset value along the z-axis self.stage_move_z(offset) # rescue autofocus when no signal detected if not self.autofocus_check_signal(): self.rescue_autofocus() # Look for the position with the maximum intensity - for the QPD the SUM signal is used. max_sum = 0 z_range = 5 # in µm z_step = 0.1 # in µm self.stage_move_z(-z_range/2) for n in range(int(z_range/z_step)): self.stage_move_z(z_step) sum = self.read_detector_intensity() print(sum) if sum > max_sum: max_sum = sum elif sum < max_sum and max_sum > 500: break # Calculate the offset for the stage and move back to the initial position offset = self._stage.get_pos()['z'] - z_up offset = np.round(offset, decimals=1) # avoid moving stage while QPD signal is read sleep(0.1) self.stage_move_z(-(offset)) # send signal to focus logic that will be linked to define_autofocus_setpoint self.sigOffsetDefined.emit() self._focus_offset = offset return offset def rescue_autofocus(self): """ When the autofocus signal is lost, launch a rescuing procedure by using the MS2000 translation stage. The z position of the stage is moved until the piezo signal is found again. """ success = False z_range = 20 while not self.autofocus_check_signal() and z_range <= 40: self.stage_move_z(-z_range/2) for z in range(z_range): step = 1 self.stage_move_z(step) if self.autofocus_check_signal(): success = True print("autofocus signal found!") return success if not self.autofocus_check_signal(): self.stage_move_z(-z_range/2) z_range += 10 return success def stage_move_z(self, step): self._stage.move_rel({'z': step}) def do_position_correction(self, step): self._stage.set_velocity({'z': 0.01}) sleep(1) self.stage_move_z(step) self._stage.wait_for_idle() self._stage.set_velocity({'z': 1.9}) self.sigStageMoved.emit() # ----------------to be completed---------------------------------- # def start_piezo_position_correction(self, direction): # """ When the piezo position gets too close to the limits, the MS2000 stage is used to move the piezo back # to a standard position. If the piezo close to the lower limit (<5µm) it is moved to 25µm. If the piezo is too # close to the upper limit (>70µm), it is moved back to 50µm. # """ # # if direction == "up": # step = 1 # elif direction == "down": # step = -1 # else: # self.log.warning('no valid direction specified') # return # # self._pos_dict = {'z': step} # # # this needs to be modified : # if not self._run_autofocus: # self.start_autofocus() # # stage_worker = StageAutofocusWorker() # stage_worker.signals.sigFinished.connect(self.run_piezo_position_correction) # self.threadpool.start(stage_worker) # # def run_piezo_position_correction(self): # """ Correct the piezo position by moving the MS2000 stage while the autofocus ON # """ # z = self.get_position() # z posiiton of the piezo -- to be modified, this needs to be passed in by the focus_logic # # if not self._autofocus_lost and (z < 25 or z > 50): # if not self.autofocus_check_signal and (z < 25 or z > 50): # self._stage.move_rel(self._pos_dict) # # stage_worker = StageAutofocusWorker() # stage_worker.signals.sigFinished.connect(self.run_piezo_position_correction) # self.threadpool.start(stage_worker) # --------------------------------------------------------------- # ================================================================= # private methods for QPD-based autofocus # ================================================================= def qpd_read_position(self): """ Read the QPD signal from the FPGA. The signal is read from X/Y positions. In order to make sure we are always reading from the latest piezo position, the method is waiting for a new count. """ qpd = self._fpga.read_qpd() last_count = qpd[3] while last_count == qpd[3]: qpd = self._fpga.read_qpd() sleep(0.01) if self._ref_axis == 'X': return qpd[0] elif self._ref_axis == 'Y': return qpd[1] def qpd_read_sum(self): """ Read the SUM signal from the QPD. Returns an indication whether there is a detected signal or not """ qpd = self._fpga.read_qpd() return qpd[2] def set_worker_frequency(self): """ Update the worker frequency according to the iteration time of the fpga """ qpd = self._fpga.read_qpd() self._pid_frequency = qpd[4] / 1000 + 0.01 def qpd_reset(self): """ Reset the QPD counter """ self._fpga.reset_qpd_counter()
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 InjectionsLogic(GenericLogic): """ Class containing the logic to configure and save an injection sequence Example config for copy-paste: injections_logic: module.Class: 'injections_logic.InjectionsLogic' probe_valve_number: 7 number_of_valve_positions: 8 number_of_probes: 100 """ probe_valve_number = ConfigOption('probe_valve_number', missing='warn') num_valve_positions = ConfigOption('number_of_valve_positions', missing='warn') num_probes = ConfigOption('number_of_probes', missing='warn') # signals sigBufferListChanged = QtCore.Signal() sigProbeListChanged = QtCore.Signal() sigHybridizationListChanged = QtCore.Signal() sigPhotobleachingListChanged = QtCore.Signal() sigIncompleteLoad = QtCore.Signal() # attributes procedures = ['Hybridization', 'Photobleaching'] products = ['Probe'] buffer_dict = { } # key: value = valve_number: buffer_name (to make sure that each valve is only used once) probe_dict = {} # key: value = position_number: probe_name (same comment) def __init__(self, config, **kwargs): super().__init__(config=config, **kwargs) def on_activate(self): """ Initialisation performed during activation of the module. """ self.buffer_list_model = BufferListModel() self.probe_position_model = ProbePositionModel() self.hybridization_injection_sequence_model = InjectionSequenceModel() self.photobleaching_injection_sequence_model = InjectionSequenceModel() # add the probe entry as default into the bufferlist self.add_buffer('Probe', self.probe_valve_number) def on_deactivate(self): """ Perform required deactivation. """ pass # ---------------------------------------------------------------------------------------------------------------------- # Methods to insert / delete items into the listview models # ---------------------------------------------------------------------------------------------------------------------- def add_buffer(self, buffername, valve_number): """ This method adds an entry to the buffer dict and to the buffer list model and informs the GUI that the data was changed. :param: str buffername :param: int valve_number """ self.buffer_dict[valve_number] = buffername # synchronize buffer_dict and list_model, this approach also ensures that each valve number is only set once self.buffer_list_model.items = [ (self.buffer_dict[key], key) for key in sorted(self.buffer_dict.keys()) ] self.sigBufferListChanged.emit() def delete_buffer(self, index): """ This method deletes a single entry from the buffer list model and from the buffer dict. The signal informs the GUI that the data was changed. :param: QtCore.QModelIndex index """ key = self.buffer_list_model.items[index.row()][1] # the valve number if key == self.probe_valve_number: pass else: del self.buffer_dict[key] del self.buffer_list_model.items[index.row()] self.sigBufferListChanged.emit() def delete_all_buffer(self): """ This method resets the buffer dict and deletes all items from the buffer list model. The GUI is informed that the data has changed. """ self.buffer_dict = {} self.buffer_list_model.items = [] # add the default entry Probe self.add_buffer('Probe', self.probe_valve_number) self.sigBufferListChanged.emit() def add_probe(self, probename, probe_position): """ This method adds an entry to the probe dict and to the probe position model and informs the GUI that the data was changed. :param: str probename :param: int probe_position """ self.probe_dict[probe_position] = probename # synchronize buffer_dict and list_model, this approach also ensures that each valve number is only set once self.probe_position_model.items = [ (self.probe_dict[key], key) for key in sorted(self.probe_dict.keys()) ] self.sigProbeListChanged.emit() def delete_probe(self, index): """ This method deletes a single entry from the probe position model and from the probe dict. The signal informs the GUI that the data was changed. :param: QtCore.QModelIndex index """ key = self.probe_position_model.items[index.row()][ 1] # the probe position number # delete the entry both in the dictionary and in the list model del self.probe_dict[key] del self.probe_position_model.items[index.row()] self.sigProbeListChanged.emit() def delete_all_probes(self): """ This method resets the probe position dict and deletes all items from the probe position model. The GUI is informed that the data has changed. """ self.probe_dict = {} self.probe_position_model.items = [] self.sigProbeListChanged.emit() @QtCore.Slot(str, str, int, int) def add_injection_step(self, procedure, product, volume, flowrate): """ This method adds an entry tho the hybridization or photobleaching sequence model and informs the GUI that the underlying data has changed. :param: str procedure: identifier to select to which model the entry should be added :param: str product :param: int volume: amount of product to be injected (in ul) :param: int flowrate: target flowrate in ul/min """ if procedure == 'Hybridization': self.hybridization_injection_sequence_model.items.append( (procedure, product, volume, flowrate, None)) self.sigHybridizationListChanged.emit() elif procedure == 'Photobleaching': self.photobleaching_injection_sequence_model.items.append( (procedure, product, volume, flowrate, None)) self.sigPhotobleachingListChanged.emit() else: pass @QtCore.Slot(str, int) def add_incubation_step(self, procedure, time): """ This method adds an incubation time entry tho the hybridization or photobleaching sequence model and emits a signal to inform the GUI that the underlying data has changed. :param: str procedure: identifier to select to which model the entry should be added :param: int time: incubation time in seconds """ if procedure == 'Hybridization': self.hybridization_injection_sequence_model.items.append( (procedure, None, None, None, time)) self.sigHybridizationListChanged.emit() elif procedure == 'Photobleaching': self.photobleaching_injection_sequence_model.items.append( (procedure, None, None, None, time)) self.sigPhotobleachingListChanged.emit() else: pass @QtCore.Slot(QtCore.QModelIndex) def delete_hybr_step(self, index): """ This method deletes an entry from the hybridization sequence model and notifies about data modification. @:param: QtCore.QModelIndex index: selected entry """ del self.hybridization_injection_sequence_model.items[index.row()] self.sigHybridizationListChanged.emit() @QtCore.Slot(QtCore.QModelIndex) def delete_photobl_step(self, index): """ This method deletes an entry from the photobleaching sequence model and notifies about data modification. @:param: QtCore.QModelIndex index: selected entry """ del self.photobleaching_injection_sequence_model.items[index.row()] self.sigPhotobleachingListChanged.emit() def delete_hybr_all(self): """ This method deletes all entries from the hybridization sequence model and notifies about data modification. """ self.hybridization_injection_sequence_model.items = [] self.sigHybridizationListChanged.emit() def delete_photobl_all(self): """ This method deletes all entries from the photobleaching sequence model and notifies about data modification. """ self.photobleaching_injection_sequence_model.items = [] self.sigPhotobleachingListChanged.emit() # ---------------------------------------------------------------------------------------------------------------------- # Methods to load and save files # ---------------------------------------------------------------------------------------------------------------------- def load_injections(self, path): """ This method allows to open a file and fills in data to the buffer dict, probe dict and to the models if the specified document contains all relevant information. The GUI is notified and updated with the new data. :param: str path: full path to the file """ try: # delete the current content of models self.delete_all_buffer() self.delete_all_probes() self.delete_hybr_all() self.delete_photobl_all() with open(path, 'r') as stream: documents = yaml.safe_load( stream ) # yaml.full_load(stream) # use this when pyyaml updated everywhere self.buffer_dict = documents['buffer'] self.probe_dict = documents['probes'] hybridization_list = documents['hybridization list'] photobleaching_list = documents['photobleaching list'] # update the models based on the dictionaries / lists content self.buffer_list_model.items = [(self.buffer_dict[key], key) for key in self.buffer_dict] self.probe_position_model.items = [(self.probe_dict[key], key) for key in self.probe_dict] for i in range(len(hybridization_list)): entry = hybridization_list[i] # entry is a dict self.hybridization_injection_sequence_model.items.append( (entry['procedure'], entry['product'], entry['volume'], entry['flowrate'], entry['time'])) for i in range(len(photobleaching_list)): entry = photobleaching_list[i] # entry is a dict self.photobleaching_injection_sequence_model.items.append( (entry['procedure'], entry['product'], entry['volume'], entry['flowrate'], entry['time'])) self.sigBufferListChanged.emit() self.sigProbeListChanged.emit() self.sigHybridizationListChanged.emit() self.sigPhotobleachingListChanged.emit() except KeyError: self.log.warning('Injections not loaded. Document is incomplete.') self.sigIncompleteLoad.emit() def save_injections(self, path): """ This method allows to write the data from the models and from the buffer dict and probe position dict to a file. For readability, hybridization and photobleaching sequence model entries are converted into a dictionary format. :param: str path: full path """ # prepare the list entries in dictionary format for good readability in the file hybridization_list = [] photobleaching_list = [] for entry_num, item in enumerate( self.hybridization_injection_sequence_model.items, 1): if item[1] is not None: product = str.split( item[1], ':' )[0] # write just the product name, not the corresponding valve pos else: product = None entry = self.make_dict_entry(entry_num, item[0], product, item[2], item[3], item[4]) hybridization_list.append(entry) for entry_num, item in enumerate( self.photobleaching_injection_sequence_model.items, 1): if item[1] is not None: product = str.split(item[1], ':')[0] else: product = None entry = self.make_dict_entry(entry_num, item[0], product, item[2], item[3], item[4]) photobleaching_list.append(entry) # write a complete file containing buffer_dict, probe_dict, hybridization_list and photobleaching_list with open(path, 'w') as file: dict_file = { 'buffer': self.buffer_dict, 'probes': self.probe_dict, 'hybridization list': hybridization_list, 'photobleaching list': photobleaching_list } yaml.safe_dump(dict_file, file, default_flow_style=False) # , sort_keys=False self.log.info('Injections saved to {}'.format(path)) @staticmethod def make_dict_entry(step_num, procedure, product, volume, flowrate, time=None): """ Helper function. :param int step_num: number of the current step in an injection list :param str procedure: name of the procedure: Hybridization or Photobleaching :param str product: name of the product :param int volume: quantity of product in ul to be injected :param int flowrate: target flowrate in ul/min :param int time: time in seconds for incubation steps """ inj_step_dict = { 'step_number': step_num, 'procedure': procedure, 'product': product, 'volume': volume, 'flowrate': flowrate, 'time': time } return inj_step_dict
class MotorStagePI(Base, MotorInterface): """unstable: Christoph Müller, Simon Schmitt This is the Interface class to define the controls for the simple microwave hardware. Example config for copy-paste: motorstage_pi: module.Class: 'motor.motor_stage_pi.MotorStagePI' com_port_pi_xyz: 'ASRL1::INSTR' pi_xyz_baud_rate: 9600 pi_xyz_timeout: 1000 pi_xyz_term_char: '\n' pi_first_axis_label: 'x' pi_second_axis_label: 'y' pi_third_axis_label: 'z' pi_first_axis_ID: '1' pi_second_axis_ID: '2' pi_third_axis_ID: '3' pi_first_min: -0.1 # in m pi_first_max: 0.1 # in m pi_second_min: -0.1 # in m pi_second_max: 0.1 # in m pi_third_min: -0.1 # in m pi_third_max: 0.1 # in m pi_first_axis_step: 1e-7 # in m pi_second_axis_step: 1e-7 # in m pi_third_axis_step: 1e-7 # in m vel_first_min: 1e-5 # in m/s vel_first_max: 5e-2 # in m/s vel_second_min: 1e-5 # in m/s vel_second_max: 5e-2 # in m/s vel_third_min: 1e-5 # in m/s vel_third_max: 5e-2 # in m/s vel_first_axis_step: 1e-5 # in m/s vel_second_axis_step: 1e-5 # in m/s vel_third_axis_step: 1e-5 # in m/s """ _com_port_pi_xyz = ConfigOption('com_port_pi_xyz', 'ASRL1::INSTR', missing='warn') _pi_xyz_baud_rate = ConfigOption('pi_xyz_baud_rate', 9600, missing='warn') _pi_xyz_timeout = ConfigOption('pi_xyz_timeout', 1000, missing='warn') _pi_xyz_term_char = ConfigOption('pi_xyz_term_char', '\n', missing='warn') _first_axis_label = ConfigOption('pi_first_axis_label', 'x', missing='warn') _second_axis_label = ConfigOption('pi_second_axis_label', 'y', missing='warn') _third_axis_label = ConfigOption('pi_third_axis_label', 'z', missing='warn') _first_axis_ID = ConfigOption('pi_first_axis_ID', '1', missing='warn') _second_axis_ID = ConfigOption('pi_second_axis_ID', '2', missing='warn') _third_axis_ID = ConfigOption('pi_third_axis_ID', '3', missing='warn') _min_first = ConfigOption('pi_first_min', -0.1, missing='warn') _max_first = ConfigOption('pi_first_max', 0.1, missing='warn') _min_second = ConfigOption('pi_second_min', -0.1, missing='warn') _max_second = ConfigOption('pi_second_max', 0.1, missing='warn') _min_third = ConfigOption('pi_third_min', -0.1, missing='warn') _max_third = ConfigOption('pi_third_max', 0.1, missing='warn') step_first_axis = ConfigOption('pi_first_axis_step', 1e-7, missing='warn') step_second_axis = ConfigOption('pi_second_axis_step', 1e-7, missing='warn') step_third_axis = ConfigOption('pi_third_axis_step', 1e-7, missing='warn') _vel_min_first = ConfigOption('vel_first_min', 1e-5, missing='warn') _vel_max_first = ConfigOption('vel_first_max', 5e-2, missing='warn') _vel_min_second = ConfigOption('vel_second_min', 1e-5, missing='warn') _vel_max_second = ConfigOption('vel_second_max', 5e-2, missing='warn') _vel_min_third = ConfigOption('vel_third_min', 1e-5, missing='warn') _vel_max_third = ConfigOption('vel_third_max', 5e-2, missing='warn') _vel_step_first = ConfigOption('vel_first_axis_step', 1e-5, missing='warn') _vel_step_second = ConfigOption('vel_second_axis_step', 1e-5, missing='warn') _vel_step_third = ConfigOption('vel_third_axis_step', 1e-5, missing='warn') def __init__(self, **kwargs): super().__init__(**kwargs) def on_activate(self): """ Initialisation performed during activation of the module. @return: error code """ self.rm = visa.ResourceManager() self._serial_connection_xyz = self.rm.open_resource( resource_name=self._com_port_pi_xyz, baud_rate=self._pi_xyz_baud_rate, timeout=self._pi_xyz_timeout) return 0 def on_deactivate(self): """ Deinitialisation performed during deactivation of the module. @return: error code """ self._serial_connection_xyz.close() self.rm.close() return 0 def get_constraints(self): """ Retrieve the hardware constrains from the motor device. @return dict: dict with constraints for the sequence generation and GUI Provides all the constraints for the xyz stage and rot stage (like total movement, velocity, ...) Each constraint is a tuple of the form (min_value, max_value, stepsize) """ constraints = OrderedDict() axis0 = {'label': self._first_axis_label, 'ID': self._first_axis_ID, 'unit': 'm', 'ramp': None, 'pos_min': self._min_first, 'pos_max': self._max_first, 'pos_step': self.step_first_axis, 'vel_min': self._vel_min_first, 'vel_max': self._vel_max_first, 'vel_step': self._vel_step_first, 'acc_min': None, 'acc_max': None, 'acc_step': None} axis1 = {'label': self._second_axis_label, 'ID': self._second_axis_ID, 'unit': 'm', 'ramp': None, 'pos_min': self._min_second, 'pos_max': self._max_second, 'pos_step': self.step_second_axis, 'vel_min': self._vel_min_second, 'vel_max': self._vel_max_second, 'vel_step': self._vel_step_second, 'acc_min': None, 'acc_max': None, 'acc_step': None} axis2 = {'label': self._third_axis_label, 'ID': self._third_axis_ID, 'unit': 'm', 'ramp': None, 'pos_min': self._min_third, 'pos_max': self._max_third, 'pos_step': self.step_third_axis, 'vel_min': self._vel_min_third, 'vel_max': self._vel_max_third, 'vel_step': self._vel_step_third, 'acc_min': None, 'acc_max': None, 'acc_step': None} # assign the parameter container for x to a name which will identify it constraints[axis0['label']] = axis0 constraints[axis1['label']] = axis1 constraints[axis2['label']] = axis2 return constraints def move_rel(self, param_dict): """Moves stage in given direction (relative movement) @param dict param_dict: dictionary, which passes all the relevant parameters, which should be changed. Usage: {'axis_label': <the-abs-pos-value>}. 'axis_label' must correspond to a label given to one of the axis. @return dict pos: dictionary with the current magnet position """ # There are sometimes connections problems therefore up to 3 attempts are started for attempt in range(3): try: for axis_label in param_dict: step = param_dict[axis_label] self._do_move_rel(axis_label, step) except: self.log.warning('Motor connection problem! Try again...') else: break else: self.log.error('Motor cannot move!') #The following two lines have been commented out to speed up #pos = self.get_pos() #return pos return param_dict def move_abs(self, param_dict): """Moves stage to absolute position @param dict param_dict: dictionary, which passes all the relevant parameters, which should be changed. Usage: {'axis_label': <the-abs-pos-value>}. 'axis_label' must correspond to a label given to one of the axis. The values for the axes are in millimeter, the value for the rotation is in degrees. @return dict pos: dictionary with the current axis position """ # There are sometimes connections problems therefore up to 3 attempts are started for attept in range(3): try: for axis_label in param_dict: move = param_dict[axis_label] self._do_move_abs(axis_label, move) while not self._motor_stopped(): time.sleep(0.02) except: self.log.warning('Motor connection problem! Try again...') else: break else: self.log.error('Motor cannot move!') #The following two lines have been commented out to speed up #pos = self.get_pos() #return pos return param_dict def abort(self): """Stops movement of the stage @return int: error code (0:OK, -1:error) """ constraints = self.get_constraints() try: for axis_label in constraints: self._write_xyz(axis_label,'AB') while not self._motor_stopped(): time.sleep(0.2) return 0 except: self.log.error('MOTOR MOVEMENT NOT STOPPED!!!)') return -1 def get_pos(self, param_list=None): """ Gets current position of the stage arms @param list param_list: optional, if a specific position of an axis is desired, then the labels of the needed axis should be passed in the param_list. If nothing is passed, then from each axis the position is asked. @return dict: with keys being the axis labels and item the current position. """ constraints = self.get_constraints() param_dict = {} # unfortunately, probably due to connection problems this specific command sometimes failing # although it should run.... therefore some retries are added try: if param_list is not None: for axis_label in param_list: for attempt in range(5): # self.log.debug(attempt) try: pos = int(self._ask_xyz(axis_label,'TT').split(":",1)[1]) param_dict[axis_label] = pos * 1e-7 except: continue else: break else: for axis_label in constraints: for attempt in range(5): #self.log.debug(attempt) try: #pos = int(self._ask_xyz(axis_label,'TT')[8:]) pos = int(self._ask_xyz(axis_label, 'TT').split(":",1)[1]) param_dict[axis_label] = pos * 1e-7 except: continue else: break return param_dict except: self.log.error('Could not find current xyz motor position') return -1 def get_status(self, param_list=None): """ Get the status of the position @param list param_list: optional, if a specific status of an axis is desired, then the labels of the needed axis should be passed in the param_list. If nothing is passed, then from each axis the status is asked. @return dict: with the axis label as key and the status number as item. The meaning of the return value is: Bit 0: Ready Bit 1: On target Bit 2: Reference drive active Bit 3: Joystick ON Bit 4: Macro running Bit 5: Motor OFF Bit 6: Brake ON Bit 7: Drive current active """ constraints = self.get_constraints() param_dict = {} try: if param_list is not None: for axis_label in param_list: status = self._ask_xyz(axis_label,'TS').split(":",1)[1] param_dict[axis_label] = status else: for axis_label in constraints: status = self._ask_xyz(axis_label, 'TS').split(":",1)[1] param_dict[axis_label] = status return param_dict except: self.log.error('Status request unsuccessful') return -1 def calibrate(self, param_list=None): """ Calibrates the stage. @param dict param_list: param_list: optional, if a specific calibration of an axis is desired, then the labels of the needed axis should be passed in the param_list. If nothing is passed, then all connected axis will be calibrated. After calibration the stage moves to home position which will be the zero point for the passed axis. @return dict pos: dictionary with the current position of the ac#xis """ #constraints = self.get_constraints() param_dict = {} try: for axis_label in param_list: self._write_xyz(axis_label,'FE2') while not self._motor_stopped(): time.sleep(0.2) for axis_label in param_list: self._write_xyz(axis_label,'DH') except: self.log.error('Calibration did not work') for axis_label in param_list: param_dict[axis_label] = 0.0 self.move_abs(param_dict) pos = self.get_pos() return pos def get_velocity(self, param_list=None): """ Gets the current velocity for all connected axes in m/s. @param list param_list: optional, if a specific velocity of an axis is desired, then the labels of the needed axis should be passed as the param_list. If nothing is passed, then from each axis the velocity is asked. @return dict : with the axis label as key and the velocity as item. """ constraints = self.get_constraints() param_dict = {} try: if param_list is not None: for axis_label in param_list: vel = int(self._ask_xyz(axis_label, 'TY').split(":",1)[1]) param_dict[axis_label] = vel * 1e-7 else: for axis_label in constraints: vel = int(self._ask_xyz(axis_label, 'TY').split(":",1)[1]) param_dict[axis_label] = vel * 1e-7 return param_dict except: self.log.error('Could not find current axis velocity') return -1 def set_velocity(self, param_dict): """ Write new value for velocity in m/s. @param dict param_dict: dictionary, which passes all the relevant parameters, which should be changed. Usage: {'axis_label': <the-velocity-value>}. 'axis_label' must correspond to a label given to one of the axis. @return dict param_dict2: dictionary with the updated axis velocity """ #constraints = self.get_constraints() try: for axis_label in param_dict: vel = int(param_dict[axis_label] * 1.0e7) self._write_xyz(axis_label, 'SV{0:d}'.format(vel)) #The following two lines have been commented out to speed up #param_dict2 = self.get_velocity() #retrun param_dict2 return param_dict except: self.log.error('Could not set axis velocity') return -1 ########################## internal methods ################################## def _write_xyz(self,axis,command): """this method just sends a command to the motor! DOES NOT RETURN AN ANSWER! @param axis string: name of the axis that should be asked @param command string: command @return error code (0:OK, -1:error) """ constraints = self.get_constraints() try: #self.log.info(constraints[axis]['ID'] + command + '\n') self._serial_connection_xyz.write(constraints[axis]['ID'] + command + '\n') trash=self._read_answer_xyz() # deletes possible answers return 0 except: self.log.error('Command was no accepted') return -1 def _read_answer_xyz(self): """this method reads the answer from the motor! @return answer string: answer of motor """ still_reading = True answer='' while still_reading: try: answer = answer + self._serial_connection_xyz.read()[:-1] except: still_reading = False return answer def _ask_xyz(self,axis,question): """this method combines writing a command and reading the answer @param axis string: name of the axis that should be asked @param command string: command @return answer string: answer of motor """ constraints = self.get_constraints() self._serial_connection_xyz.write(constraints[axis]['ID']+question+'\n') answer=self._read_answer_xyz() return answer def _do_move_rel(self, axis, step): """internal method for the relative move @param axis string: name of the axis that should be moved @param float step: step in meter @return str axis: axis which is moved move float: absolute position to move to """ constraints = self.get_constraints() if not(abs(constraints[axis]['pos_step']) < abs(step)): self.log.warning('Cannot make the movement of the axis "{0}"' 'since the step is too small! Ignore command!') else: current_pos = self.get_pos(axis)[axis] move = current_pos + step self._do_move_abs(axis, move) return axis, move def _do_move_abs(self, axis, move): """internal method for the absolute move in meter @param axis string: name of the axis that should be moved @param float move: desired position in meter @return str axis: axis which is moved move float: absolute position to move to """ constraints = self.get_constraints() #self.log.info(axis + 'MA{0}'.format(int(move*1e8))) if not(constraints[axis]['pos_min'] <= move <= constraints[axis]['pos_max']): self.log.warning('Cannot make the movement of the axis "{0}"' 'since the border [{1},{2}] would be crossed! Ignore command!' ''.format(axis, constraints[axis]['pos_min'], constraints[axis]['pos_max'])) else: self._write_xyz(axis,'MA{0}'.format(int(move*1e7))) # 1e7 to convert meter to SI units #self._write_xyz(axis, 'MP') return axis, move def _in_movement_xyz(self): """this method checks if the magnet is still moving and returns a dictionary which of the axis are moving. @return: dict param_dict: Dictionary displaying if axis are moving: 0 for immobile and 1 for moving """ constraints=self.get_constraints() param_dict = {} for axis_label in constraints: tmp0 = int(self._ask_xyz(constraints[axis_label]['label'],'TS')[8:]) param_dict[axis_label] = tmp0%2 return param_dict def _motor_stopped(self): """this method checks if the magnet is still moving and returns False if it is moving and True of it is immobile @return: bool stopped: False for immobile and True for moving """ param_dict=self._in_movement_xyz() stopped=True for axis_label in param_dict: if param_dict[axis_label] != 0: self.log.info(axis_label + ' is moving') stopped=False return stopped
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