class WlmMonitor: """ A script class for monitoring and locking lasers based on the wavemeter """ def __init__(self, wlm_client, logger_client, gui='wavemeter_monitor', ao_clients=None, display_pts=5000, threshold=0.0002, port=None, params=None, three_lasers=False): """ Instantiates WlmMonitor script object for monitoring wavemeter :param wlm_client: (obj) instance of wavemeter client :param gui_client: (obj) instance of GUI client. :param logger_client: (obj) instance of logger client. :param ao_clients: (dict, optional) dictionary of ao client objects with keys to identify. Exmaple: {'ni_usb_1': nidaqmx_usb_client_1, 'ni_usb_2': nidaqmx_usb_client_2, 'ni_pxi_multi': nidaqmx_pxi_client} :param display_pts: (int, optional) number of points to display on plot :param threshold: (float, optional) threshold in THz for lock error signal :param port: (int) port number for update server :param params: (dict) see set_parameters below for details :three_lasers: (bool) If three lasers are in use (instead of 2) """ self.channels = [] if three_lasers: gui = 'wavemeter_monitor_3lasers' self.wlm_client = wlm_client self.ao_clients = ao_clients self.display_pts = display_pts self.threshold = threshold self.log = LogHandler(logger_client) # Instantiate gui self.gui = Window( gui_template=gui, host=get_ip(), port=port, ) # Setup stylesheet. self.gui.apply_stylesheet() if three_lasers: self.widgets = get_gui_widgets(gui=self.gui, freq=3, sp=3, rs=3, lock=3, error_status=3, graph=6, legend=6, clear=6, zero=6, voltage=3, error=3) else: self.widgets = get_gui_widgets(gui=self.gui, freq=2, sp=2, rs=2, lock=2, error_status=2, graph=4, legend=4, clear=4, zero=4, voltage=2, error=2) # Set parameters self.set_parameters(**params) # Configure plots # Get actual legend widgets self.widgets['legend'] = [ get_legend_from_graphics_view(legend) for legend in self.widgets['legend'] ] self.widgets['curve'] = [] self.initialize_channels() for channel in self.channels: self.update_parameters( dict(channel=channel.number, setpoint=channel.data[-1])) def set_parameters(self, channel_params): """ Instantiates new channel objects with given parameters and assigns them to the WlmMonitor Note: parameters for a channel that has already been assigned can be set or updated later using the update_parameters() method via an update client in a separate python process. :param channel_params: (list) of dictionaries containing all parameters. Example of full parameter set: {'channel': 1, 'name': 'Velocity', 'setpoint': 406.7, 'lock':True, 'memory': 20, 'pid': {'p': 1, 'i': 0.1, 'd': 0}, 'ao': {'client':'nidaqmx_client', 'channel': 'ao1'}} In more detail: - 'channel': should be from 1-8 for the High-Finesse Wavemeter (with switch) and should ALWAYS be provided, as a reference so that we know which channel to assign all the other parameters to - 'name': a string that can just be provided once and is used as a user-friendly name for the channel. Initializes to 'Channel X' where X is a random integer if not provided - 'setpoint': setpoint for this channel. - 'lock': boolean that tells us whether or not to turn on the lock. Ignored if setpoint is None. Default is False. - 'memory': Number of points for integral memory of pid (history of the integral). Default is 20. - 'pid': dict containing pid parameters. Uses the pylabnet.scripts.pid module. By default instantiates the default PID() object. - 'ao': dict containing two elements: 'client' which is a string that is the name of the ao client to use for locking. This should match up with a key in self.ao_clients. 'channel'is an identifier for which analog output to use for this channel. By default it is set to None and no active locking is performed """ # Check if it is only a single channel if type(channel_params) is dict: channel_params = [channel_params] # Initialize each channel individually for channel_param_set in channel_params: self.channels.append( Channel(channel_param_set, self.ao_clients, log=self.log)) def update_parameters(self, parameters): """ Updates only the parameters given. Can be used in the middle of the script operation via an update client. :param parameters: (list) list of dictionaries, see set_parameters() for details """ if not isinstance(parameters, list): parameters = [parameters] for parameter in parameters: # Make sure a channel is given if 'channel' in parameter: # Check if it is a channel that is already logged channel_list = self._get_channels() if parameter['channel'] in channel_list: # Find index of the desired channel index = channel_list.index(parameter['channel']) channel = self.channels[channel_list.index( parameter['channel'])] # Set all other parameters for this channel if 'name' in parameter: channel.name = parameter['name'] if 'setpoint' in parameter: # self.widgets['sp'][index].setValue(parameter['setpoint']) # channel.setpoint = parameter['setpoint'] # Mark that we should override GUI setpoint, since it has been updated by the script channel.setpoint_override = parameter['setpoint'] if 'lock' in parameter: self.widgets['lock'][index].setChecked( parameter['lock']) # channel.lock = parameter['lock'] # Mark that we should override the GUI lock since it has been updated by the script # channel.lock_override = True if 'memory' in parameter: channel.memory = parameter['memory'] if 'pid' in parameter: channel.pid.set_parameters( p=parameter['pid']['p'], i=parameter['pid']['i'], d=parameter['pid']['d'], ) # Ignore ao requests if clients have not been assigned if 'ao' in parameter and self.ao_clients is not None: # Convert ao from string to object using lookup try: channel.ao = { 'client': self.ao_clients[parameter['ao']['client']], 'channel': parameter['ao']['channel'] } # Handle case where the ao client does not exist except KeyError: channel.ao = None # Otherwise, it is a new channel so we should add it else: self.channels.append(Channel(parameter)) self._initialize_channel(index=len(self.channels) - 1, channel=self.channels[-1]) def initialize_channels(self): """Initializes all channels and outputs to the GUI""" for index, channel in enumerate(self.channels): self._initialize_channel(index, channel) def clear_channel(self, channel): """ Clears the plot output for this channel :param channel: Channel object to clear """ try: channel.initialize(channel.data[-1]) # If the channel isn't monitored except: self.log.warn('Could not clear channel') def clear_all(self): """ Clears all channels """ for channel in self.channels: self.clear_channel(channel) def run(self): """Runs the WlmMonitor Can be stopped using the pause() method """ self._get_gui_data() self._update_channels() self.gui.force_update() def zero_voltage(self, channel): """ Zeros the output voltage for this channel :param channel: Channel object to zero voltage of """ try: channel.zero_voltage() self.log.info(f'Voltage centered for channel {channel.name}') # If the channel isn't monitored except: self.log.warn('Failed to zero voltage') def go_to(self, channel, value, step_size, hold_time): """ Sends laser to a setpoint value gradually :param channel: (int) channel number on wavemeter :param value: (float) value to set laser frequency to :param step_size: (float) step size in THz for laser freq steps :param hold_time: (float) time in seconds to wait between steps """ # Index of channel physical_channel = self.channels[self._get_channels().index(channel)] # Generate array of points to go to traverse = np.linspace( physical_channel.setpoint, value, int((value - physical_channel.setpoint) / step_size)) for frequency in traverse: self.set_parameters([dict(channel=channel, setpoint=frequency)]) time.sleep(hold_time) # Technical methods def _initialize_channel(self, index, channel): """Initializes a channel and outputs to the GUI Should only be called in the beginning of channel use to assign physical GUI widgets """ # Get wavelength and initialize data arrays channel.initialize(wavelength=self.wlm_client.get_wavelength( channel.number), display_pts=self.display_pts) # Create curves # frequency self.widgets['curve'].append(self.widgets['graph'][2 * index].plot( pen=pg.mkPen(color=self.gui.COLOR_LIST[0]))) add_to_legend(legend=self.widgets['legend'][2 * index], curve=self.widgets['curve'][4 * index], curve_name=channel.curve_name) # Setpoint self.widgets['curve'].append(self.widgets['graph'][2 * index].plot( pen=pg.mkPen(color=self.gui.COLOR_LIST[1]))) add_to_legend(legend=self.widgets['legend'][2 * index], curve=self.widgets['curve'][4 * index + 1], curve_name=channel.setpoint_name) # Clear data self.widgets['clear'][2 * index].clicked.connect( lambda: self.clear_channel(channel)) self.widgets['clear'][2 * index + 1].clicked.connect( lambda: self.clear_channel(channel)) # Setpoint reset self.widgets['rs'][index].clicked.connect( lambda: self.update_parameters( dict(channel=channel.number, setpoint=channel.data[-1]))) # Voltage self.widgets['curve'].append(self.widgets['graph'][2 * index + 1].plot( pen=pg.mkPen(color=self.gui.COLOR_LIST[0]))) add_to_legend(legend=self.widgets['legend'][2 * index + 1], curve=self.widgets['curve'][4 * index + 2], curve_name=channel.voltage_curve) # Error self.widgets['curve'].append(self.widgets['graph'][2 * index + 1].plot( pen=pg.mkPen(color=self.gui.COLOR_LIST[1]))) add_to_legend(legend=self.widgets['legend'][2 * index + 1], curve=self.widgets['curve'][4 * index + 3], curve_name=channel.error_curve) # zero self.widgets['zero'][2 * index].clicked.connect( lambda: self.zero_voltage(channel)) self.widgets['zero'][2 * index + 1].clicked.connect( lambda: self.zero_voltage(channel)) def _update_channels(self): """ Updates all channels + displays Called continuously inside run() method to refresh WLM data and output on GUI """ for index, channel in enumerate(self.channels): # Check for override if channel.setpoint_override: self.widgets['sp'][index].setValue(channel.setpoint_override) channel.setpoint_override = 0 # Update data with the new wavelength channel.update(self.wlm_client.get_wavelength(channel.number)) # Update frequency self.widgets['curve'][4 * index].setData(channel.data) self.widgets['freq'][index].setValue(channel.data[-1]) # Update setpoints self.widgets['curve'][4 * index + 1].setData(channel.sp_data) # Update the setpoint to GUI directly if it has been changed # if channel.setpoint_override: # # Tell GUI to pull data provided by script and overwrite direct GUI input # self.widgets['sp'][index].setValue(channel.setpoint) # If the lock has been updated, override the GUI # if channel.lock_override: # self.widgets['lock'][index].setChecked(channel.lock) # Set the error boolean (true if the lock is active and we are outside the error threshold) if channel.lock and np.abs(channel.data[-1] - channel.setpoint) > self.threshold: self.widgets['error_status'][index].setChecked(True) else: self.widgets['error_status'][index].setChecked(False) # Now update lock + voltage plots self.widgets['curve'][4 * index + 2].setData(channel.voltage) self.widgets['voltage'][index].setValue(channel.voltage[-1]) self.widgets['curve'][4 * index + 3].setData(channel.error) self.widgets['error'][index].setValue(channel.error[-1]) def _get_gui_data(self): """ Updates setpoint and lock parameters with data pulled from GUI Does not overwrite the script setpoints and locks, but stores the GUI values for comparison based on context. See Channel.update() method for behavior on how script chooses whether to use internal values or GUI values """ for index, channel in enumerate(self.channels): # Pull the current value from the GUI channel.gui_setpoint = self.widgets['sp'][index].value() channel.gui_lock = self.widgets['lock'][index].isChecked() def _get_channels(self): """ Returns all active channel numbers Usually used for checking whether a newly input channel has already been assigned to the script :return: (list) all active channel numbers """ channel_list = [] for channel in self.channels: channel_list.append(channel.number) return channel_list def get_wavelength(self, channel): # Index of channel physical_channel = self.channels[self._get_channels().index(channel)] return self.wlm_client.get_wavelength(physical_channel.number)
class Driver: """Driver class for NI PXI 654x HSDIO card """ def __init__(self, dev_name_str, logger=None): # # Define internal variables # self.log = LogHandler(logger=logger) self.map_dict = dict() self.writn_wfm_set = set() # # "Load" niHSDIO DLL # try: self.dll = ctypes.WinDLL(NI_HSDIO_DLL_PATH) except OSError: msg_str = 'DLL loading failed. \n' \ 'Ensure that niHSDIO DLL path is correct: \n' \ 'it should be specified in pylabnet.hardware.p_gen.ni_hsdio.__init__.py \n' \ 'Current value is: \n' \ '"{}"'.format(NI_HSDIO_DLL_PATH) self.log.error(msg_str=msg_str) raise PGenError(msg_str) # Build C-prototypes (in particular, specify the return # types such that Python reads results correctly) build_c_func_prototypes(self.dll) # # Connect to device # # Create blank session handle self._handle = NITypes.ViSession() # Convert dev_name to the DLL-readable format self._dev_name = NITypes.ViRsrc(dev_name_str.encode('ascii')) self._er_chk( self.dll.niHSDIO_InitGenerationSession( self._dev_name, # ViRsrc resourceName NIConst.VI_TRUE, # ViBoolean IDQuery NIConst.VI_TRUE, # ViBoolean resetDevice NIConst. VI_NULL, # ViConstString optionString - not used, set to VI_NULL ctypes.byref(self._handle) # ViSession * session_handle )) # Log info message serial_str = self._get_attr_str(NIConst.NIHSDIO_ATTR_SERIAL_NUMBER) self.log.info( 'Connected to NI HSDIO card {0}. Serial number: {1}. Session handle: {2}' ''.format(dev_name_str, serial_str, self._handle)) def reset(self): self.writn_wfm_set = set() self.map_dict = dict() return self._er_chk(self.dll.niHSDIO_reset(self._handle)) def start(self): return self._er_chk(self.dll.niHSDIO_Initiate(self._handle)) def stop(self): return self._er_chk(self.dll.niHSDIO_Abort(self._handle)) def disconnect(self): self.reset() op_status = self._er_chk(self.dll.niHSDIO_close(self._handle)) return op_status # ================================================================ # Hardware settings # ================================================================ def get_samp_rate(self): return self._get_attr_real64(NIConst.NIHSDIO_ATTR_SAMPLE_CLOCK_RATE) def set_samp_rate(self, samp_rate): # Sanity check if not samp_rate <= self.constraints['samp_rate']['max']: self.log.warn( 'set_samp_rate({0} MHz): the requested value exceeds hardware constraint max={1} MHz.\n' 'The max possible value will be set instead.' ''.format(samp_rate / 1e6, self.constraints['samp_rate']['max'] / 1e6)) samp_rate = self.constraints['samp_rate']['max'] elif not self.constraints['samp_rate']['min'] <= samp_rate: self.log.warn( 'set_samp_rate({0} Hz): the requested value is below the hardware constraint min={1} Hz.\n' 'The min possible value will be set instead.' ''.format(samp_rate, self.constraints['samp_rate']['min'])) samp_rate = self.constraints['samp_rate']['min'] # Call DLL function # Currently, the onboard clock is used as the sample clock source self._er_chk( self.dll.niHSDIO_ConfigureSampleClock( self._handle, # ViSession vi NIConst. NIHSDIO_VAL_ON_BOARD_CLOCK_STR, # ViConstString clockSource NITypes.ViReal64(samp_rate) # ViReal64 clockRate )) # Return the actual final sample rate return self.get_samp_rate() def get_active_chs(self): return self._get_attr_str(NIConst.NIHSDIO_ATTR_DYNAMIC_CHANNELS) def set_active_chs(self, chs_str=None): if chs_str is None: # un-assign all channels chs_str = 'none' chs_str = NITypes.ViString(chs_str.encode('ascii')) self._er_chk( self.dll.niHSDIO_AssignDynamicChannels( self._handle, # ViSession vi, chs_str # ViConstString channelList )) return self.get_active_chs() def get_mode(self): """ :return: (str) "W" - Waveform, "S" - Scripted """ mode_id = self._get_attr_int32( attr_id=NIConst.NIHSDIO_ATTR_GENERATION_MODE) if mode_id == NIConst.NIHSDIO_VAL_WAVEFORM.value: return 'W' elif mode_id == NIConst.NIHSDIO_VAL_SCRIPTED.value: return 'S' else: msg_str = 'get_mode(): self._get_attr_int32(NIHSDIO_ATTR_GENERATION_MODE) ' \ 'returned unknown mode_id = {0}'.format(mode_id) self.log.error(msg_str) raise PGenError(msg_str) def set_mode(self, mode_string): """ :param mode_string: (str) "W" - Waveform, "S" - Scripted :return: actual run mode string ("W"/"S") """ if mode_string == 'W': run_mode = NIConst.NIHSDIO_VAL_WAVEFORM elif mode_string == 'S': run_mode = NIConst.NIHSDIO_VAL_SCRIPTED else: msg_str = 'set_mode({0}): invalid argument. Valid values: "W" - Waveform, "S" - scripted. \n' \ 'Run mode was not changed. Actual run mode string was returned.'.format(mode_string) self.log.error(msg_str=msg_str) raise PGenError(msg_str) # Call DLL function self._er_chk( self.dll.niHSDIO_ConfigureGenerationMode( self._handle, # ViSession vi run_mode # ViInt32 generationMode )) # Return actual run mode return self.get_mode() @property def constraints(self): # Total memory size # [in samples; one sample contains 32 bits and covers all channels] max_wfm_len = self._get_attr_int32( attr_id=NIConst.NIHSDIO_ATTR_TOTAL_GENERATION_MEMORY_SIZE) constr_dict = dict(samp_rate=dict(min=48, max=100e6), wfm_len=dict(min=2, step=2, max=max_wfm_len)) return constr_dict def get_status(self): try: # Record current samp_rate to restore it later current_rate = self.get_samp_rate() rate_lims = self.constraints['samp_rate'] test_rate = (rate_lims['min'] + rate_lims['max']) / 2 # Try changing samp_rate op_status = self.dll.niHSDIO_ConfigureSampleClock( self._handle, # ViSession vi NIConst. NIHSDIO_VAL_ON_BOARD_CLOCK_STR, # ViConstString clockSource NITypes.ViReal64(test_rate) # ViReal64 clockRate ) # If device is idle, operation should be successful: # op_status = 0. # Restore original samp rate and return 0 - "idle" if op_status == 0: self.set_samp_rate(samp_rate=current_rate) return 0 # If device is running, attempt to change samp_rate should return # the following error code: # -1074118617 # "Specified property cannot be set while the session is running. # Set the property prior to initiating the session, # or abort the session prior to setting the property." elif op_status == -1074118617: # Device is running return 1 # This method cannot interpret any other error/warning code and has # to raise an exception else: raise PGenError( 'get_status(): the attempt to test-change samp_rate returned unknown error code {}' ''.format(op_status)) # If connection to the device is lost # or any operation fails, raise an exception. except Exception as exc_obj: self.log.exception( 'get_status(): an exception was produced. \n' 'This might mean that connection to the device is lost ' 'or there is some bug in the get_status() method. \n') raise exc_obj # ================================================================ # Waveform Generation # ================================================================ def write_wfm(self, pb_obj, len_adj=True): # # Sanity checks # # Only data_width=32 write is currently implemented # (DLL function niHSDIO_WriteNamedWaveformU32) hrdw_data_width = 8 * self._get_attr_int32( NIConst.NIHSDIO_ATTR_DATA_WIDTH) if hrdw_data_width != 32: msg_txt = 'write_wfm(): the card you use has data_width = {0} bits. \n' \ 'The method was written assuming 32-bit width and have to be modified for your card. \n' \ 'Rewrite bit_ar construction part and use niHSDIO_WriteNamedWaveformU{1}() DLL function' \ ''.format(hrdw_data_width, hrdw_data_width) self.log.error(msg_txt) raise PGenError(msg_txt) # # Sample PulseBlock # # Map user-friendly names onto physical channel numbers pb_obj = copy.deepcopy(pb_obj) pb_obj.ch_map(map_dict=self.map_dict) # Sample pulse block samp_rate = self.get_samp_rate() samp_dict, n_pts, add_pts = pb_sample( pb_obj=pb_obj, samp_rate=samp_rate, len_min=self.constraints['wfm_len']['min'], len_max=self.constraints['wfm_len']['max'], len_step=self.constraints['wfm_len']['step'], len_adj=len_adj) wfm_name = pb_obj.name del pb_obj self.log.info( 'write_wfm(): sampled PulseBlock "{}". \n' 'Sample array has {} points. {} samples were added to match hardware wfm len step' ''.format(wfm_name, n_pts, add_pts)) # # Pack samp_dict into bit_ar # # Create a blank bit_ar - all elements zero (all channels off) bit_ar = np.zeros(shape=n_pts, dtype=np.uint32) # Iterate through each channel and set corresponding bit to '1' # if value is True for ch_num in samp_dict.keys(): # This number in uint32 representation has all zeros and # exactly one '1' at the ch_num-th bit from the LSB ch_bit_one = 2**ch_num for idx, val in enumerate(samp_dict[ch_num]): if val: bit_ar[idx] += ch_bit_one # TODO: consider making very fast with numpy: # ch_bit_ar = samp_dict[ch_num].astype(int) * 2**ch_num # bit_ar = np.add(bit_ar, ch_bit_ar) # # Load bit_ar to memory # # Delete waveform with the same name, # if it is already present in the memory if wfm_name in self.writn_wfm_set: self.del_wfm(wfm_name=wfm_name) # Create C-pointer to bit_ar using numpy.ndarray.ctypes attribute bit_ar_ptr = bit_ar.ctypes.data_as(ctypes.POINTER(NITypes.ViUInt32)) # Call DLL function self._er_chk( self.dll.niHSDIO_WriteNamedWaveformU32( self._handle, # ViSession vi NITypes.ViConstString( wfm_name.encode('ascii')), # ViConstString waveformName NITypes.ViInt32(n_pts), # ViInt32 samplesToWrite bit_ar_ptr # ViUInt32 data[] )) self.writn_wfm_set.add(wfm_name) return 0 def del_wfm(self, wfm_name): self._er_chk( self.dll.niHSDIO_DeleteNamedWaveform( self._handle, # ViSession vi NITypes.ViConstString( wfm_name.encode('ascii')) # ViConstString waveformName )) self.writn_wfm_set.remove(wfm_name) return 0 def clr_mem(self): wfm_set = copy.deepcopy(self.writn_wfm_set) for wfm_name in wfm_set: self.del_wfm(wfm_name=wfm_name) return 0 def get_rep(self): """Returns number of repetitions in Waveform generation mode. On the hardware level, it is just a pair of attributes NIHSDIO_ATTR_REPEAT_MODE and NIHSDIO_ATTR_REPEAT_COUNT which are not bound to any specific waveform. :return: (int) repeat mode + number of repetitions: 0 - repeat infinitely positive integer - finite, number of repetitions PGenError exception - error """ rep_mode = self._get_attr_int32(NIConst.NIHSDIO_ATTR_REPEAT_MODE) if rep_mode == NIConst.NIHSDIO_VAL_CONTINUOUS.value: rep_num = 0 elif rep_mode == NIConst.NIHSDIO_VAL_FINITE.value: rep_num = self._get_attr_int32(NIConst.NIHSDIO_ATTR_REPEAT_COUNT) else: msg_str = 'get_rep(): DLL call returned unknown repetition mode code {}'.format( rep_mode) self.log.error(msg_str=msg_str) raise PGenError(msg_str) return rep_num def set_rep(self, rep_num): """Set repeat mode and number of repetitions :param rep_num: (int) repeat mode + number of repetitions: 0 - repeat infinitely positive integer - finite, number of repetitions :return: (int) actual repeat mode + number of repetitions: 0 - repeat infinitely positive integer - finite, number of repetitions PGenError exception - error """ if rep_num == 0: rep_mode = NIConst.NIHSDIO_VAL_CONTINUOUS rep_num = NIConst.VI_NULL elif rep_num > 0: rep_mode = NIConst.NIHSDIO_VAL_FINITE rep_num = NITypes.ViInt32(rep_num) else: msg_str = 'set_rep() invalid argument {} \n' \ 'Valid values: 0 - infinite, positive integer - finite' \ ''.format(rep_num) self.log.error(msg_str=msg_str) raise PGenError(msg_str) self._er_chk( self.dll.niHSDIO_ConfigureGenerationRepeat( self._handle, # ViSession vi rep_mode, # ViInt32 repeatMode rep_num # ViInt32 repeatCount )) return self.get_rep() def get_wfm_to_gen(self): return self._get_attr_str(NIConst.NIHSDIO_ATTR_WAVEFORM_TO_GENERATE) def set_wfm_to_gen(self, wfm_name): self._er_chk( self.dll.niHSDIO_ConfigureWaveformToGenerate( self._handle, # ViSession vi NITypes.ViConstString( wfm_name.encode('ascii')) # ViConstString waveformName )) return self.get_wfm_to_gen() # ================================================================ # Script Generation # ================================================================ def write_script(self, script_str): # Sanity check: script_str is a string if not isinstance(script_str, str): msg_str = 'write_script(): passed argument is not a string' self.log.error(msg_str=msg_str) raise PGenError(msg_str) plain_script_str = script_str.replace('\n', ' ') # Convert into C-string c_script_str = NITypes.ViConstString(plain_script_str.encode('ascii')) op_status = self._er_chk( self.dll.niHSDIO_WriteScript( self._handle, # ViSession vi c_script_str # ViConstString script )) return op_status def get_scr_to_gen(self): return self._get_attr_str(NIConst.NIHSDIO_ATTR_SCRIPT_TO_GENERATE) def set_scr_to_gen(self, script_name): # Convert script_name into C-string script_name = NITypes.ViConstString(script_name.encode('ascii')) self._er_chk( self.dll.niHSDIO_ConfigureScriptToGenerate( self._handle, # ViSession vi script_name # ViConstString scriptName )) return self.get_scr_to_gen() # ================================================================ # Wrappers for C DLL helper functions # ================================================================ def _er_chk(self, error_code): # C:\Program Files\National Instruments\Shared\Errors\English\IVI-errors.txt if error_code == 0: # Success return 0 else: # Warning or Error # Create buffer for DLL function to output the error message string msg_buf = ctypes.create_string_buffer(256) # Call DLL function to generate readable error message self.dll.niHSDIO_error_message( self._handle, # ViSession vi NITypes.ViStatus(error_code), # ViStatus errorCode msg_buf # ViChar errorMessage[256] ) msg_str = msg_buf.value.decode('ascii') if error_code > 0: # Warning self.log.warn(msg_str=msg_str) return error_code else: # Error self.log.error(msg_str=msg_str) raise PGenError(msg_str) def _get_attr_int32(self, attr_id, ch=None): """ :param attr_id: :param ch: (int) :return: (int) obtained value in the case of success PGenError exception is produced in the case of error """ # Create buffer where niHSDIO_GetAttribute will store the result buf = NITypes.ViInt32() # Convert channel number into C-string # (used to request channel-agnostic/-specific attributes) if ch is None: ch_str = NIConst.VI_NULL else: ch_str = str(ch) # Convert into C-string ch_str = ctypes.c_char_p(ch_str.encode('ascii')) # Call DLL function try: self._er_chk( self.dll.niHSDIO_GetAttributeViInt32( self._handle, # ViSession vi ch_str, # ViConstString channelName attr_id, # ViAttr attribute ctypes.byref(buf) # ViInt32 *value )) return buf.value except OSError: # DLL normally handles all "moderate" errors and returns error code, # which is being analyzed by self._er_chk. # "try" handles OSError when the DLL function fails completely # and isn't able to handle the error by itself msg_str = '_get_attr_int32(): OSError, DLL function call failed' self.log.error(msg_str=msg_str) raise PGenError(msg_str) def _get_attr_str(self, attr_id, ch=None): """ :param attr_id: :param ch: (int) :return: (str) obtained value in the case of success Exception is produced in the case of error """ # Create buffer where niHSDIO_GetAttribute will store the result buf_size = 1024 # Buffer size of 1024 was chosen for no specific reason. Increase if needed. buf = ctypes.create_string_buffer(buf_size) # Convert channel number into C-string # (used to request channel-agnostic/-specific attributes) if ch is None: ch_str = NIConst.VI_NULL else: ch_str = str(ch) # Convert into C-string ch_str = ctypes.c_char_p(ch_str.encode('ascii')) # Call DLL function try: self._er_chk( self.dll.niHSDIO_GetAttributeViString( self._handle, # ViSession vi ch_str, # ViConstString channelName attr_id, # ViAttr attribute NITypes.ViInt32(buf_size), # ViInt32 bufSize buf # ViChar value[] )) return buf.value.decode('ascii') except OSError: # DLL normally handles all "moderate" errors and returns error code, # which is being analyzed by self._er_chk. # "try" handles OSError when the DLL function fails completely # and isn't able to handle the error by itself msg_str = '_get_attr_str(): OSError, DLL function call failed' self.log.error(msg_str=msg_str) raise PGenError(msg_str) def _get_attr_real64(self, attr_id, ch=None): """ :param attr_id: :param ch: (int) :return: (float) obtained value in the case of success Exception is produced in the case of error """ # Create buffer where niHSDIO_GetAttribute will store the result buf = NITypes.ViReal64() # Convert channel number into C-string # (used to request channel-agnostic/-specific attributes) if ch is None: ch_str = NIConst.VI_NULL else: ch_str = str(ch) # Convert into C-string ch_str = ctypes.c_char_p(ch_str.encode('ascii')) # Call DLL function try: self._er_chk( self.dll.niHSDIO_GetAttributeViReal64( self._handle, # ViSession vi ch_str, # ViConstString channelName attr_id, # ViAttr attribute ctypes.byref(buf) # ViReal64 *value )) return buf.value except OSError: # DLL normally handles all "moderate" errors and returns error code, # which is being analyzed by self._er_chk. # "try" handles OSError when the DLL function fails completely # and isn't able to handle the error by itself msg_str = '_get_attr_real64(): OSError, DLL function call failed' self.log.error(msg_str=msg_str) raise PGenError(msg_str)
class DLC_Pro: """ Driver class for Toptica DLC Pro """ def __init__(self, host, port=1998, logger=None, num_lasers=2): """ Instantiates DLC_Pro object :param host: (str) hostname of laser (IP address) :param port: (int) port number, toptica defaults to 1998 :num_lasers: Number of installed lasers :param logger: (LogClient) """ self.host = host self.port = port self.log = LogHandler(logger) self.dlc = None self.laser_nums = range(1, num_lasers + 1) # Check connection try: # Check laser connection self.dlc = Telnet(host=self.host, port=self.port) self.dlc.read_until(b'>', timeout=1) for laser_num in self.laser_nums: self._check_laser_connection(laser_num) except ConnectionRefusedError: self.log.error('Could not connect to Toptica DLC Pro at ' f'IP address: {self.host}, port: {self.port}') def _check_laser_connection(self, laser_num=1): """ Read out laser number :laser_num: (int) 1 or 2, indicating laser 1 or laser 2. """ self.dlc.read_until(b'>', timeout=1) self.dlc.write( f"(param-disp 'laser{laser_num}:dl:type)\n".encode('utf')) laser_type = self.dlc.read_until( b'>', timeout=1).split()[-3].decode('utf')[1:-1] self.dlc.write( f"(param-disp 'laser{laser_num}:dl:serial-number)\n".encode('utf')) serial = int( self.dlc.read_until(b'>', timeout=1).split()[-3].decode('utf')[1:-1]) self.log.info( f'Connected to Toptica {laser_type} {laser_num}, S/N {serial}') def is_laser_on(self, laser_num=1): """ Checks if the laser is on or off :return: (bool) whether or not emission is on or off """ self.dlc.write( f"(param-disp 'laser{laser_num}:dl:cc:emission)\n".encode('utf')) result = self.dlc.read_until(b'>', timeout=1).split()[-3].decode('utf') status = result[1] if status == 't': return True elif status == 'f': return False else: self.log.warn( 'Could not determine properly whether the laser is on or off') return False def turn_on(self, laser_num=1): """ Turns on the laser """ # Check if laser is on already if self.is_laser_on(laser_num): self.log.info(f'Laser {laser_num} is already on') else: self.dlc.write( f"(param-set! 'laser{laser_num}:dl:cc:enabled #t)\n".encode( 'utf')) self.dlc.read_until(b'>', timeout=1) if self.is_laser_on(laser_num): self.log.info(f'Turned on Toptica DL-Pro laser {laser_num}') else: self.log.warn( f'Laser {laser_num} could not be turned on. Physical emission button may need to be pressed' ) def turn_off(self, laser_num=1): """ Turns off the laser """ if self.is_laser_on(laser_num): self.dlc.write( f"(param-set! 'laser{laser_num}:dl:cc:enabled #f)\n".encode( 'utf')) self.dlc.read_until(b'>', timeout=1) if self.is_laser_on(laser_num): self.log.warn( f'Failed to verify that DL-Pro laser {laser_num} turned off' ) else: self.log.info(f'Turned off Toptica DL-Pro laser {laser_num}') else: self.log.info(f'Laser {laser_num} is already off') def voltage(self, laser_num=1): """ Gets current voltage on laser piezo :return: (float) current voltage on piezo """ self.dlc.write( f"(param-disp 'laser{laser_num}:dl:pc:voltage-set)\n".encode( 'utf')) voltage = float(self.dlc.read_until(b'>', timeout=1).split()[-3]) return voltage def set_voltage(self, voltage, laser_num=1): """ Sets voltage to the piezo :param voltage: (float) voltage to set """ v = deepcopy(voltage) write_data = f"(param-set! 'laser{laser_num}:dl:pc:voltage-set {v})\n".encode( 'utf') self.dlc.write(write_data) self.dlc.read_until(b'>', timeout=1) def current_sp(self, laser_num=1): """ Gets current setpoint :return: (float) value of current setpoint """ self.dlc.write( f"(param-disp 'laser{laser_num}:dl:cc:current-set)\n".encode( 'utf')) return float(self.dlc.read_until(b'>', timeout=1).split()[-3]) def current_act(self, laser_num=1): """ Gets measured current :return: (float) value of actual current """ self.dlc.write( f"(param-disp 'laser{laser_num}:dl:cc:current-act)\n".encode( 'utf')) return float(self.dlc.read_until(b'>', timeout=1).split()[-3]) def set_current(self, current, laser_num=1): """ Sets the current to desired value :param current: (float) value of current to set as setpoint """ c = deepcopy(current) write_data = f"(param-set! 'laser{laser_num}:dl:cc:current-set {c})\n".encode( 'utf') self.dlc.write(write_data) self.dlc.read_until(b'>', timeout=1) def temp_sp(self, laser_num=1): """ Gets temperature setpoint :return: (float) value of temperature setpoint """ self.dlc.write( f"(param-disp 'laser{laser_num}:dl:tc:temp-set)\n".encode('utf')) return float(self.dlc.read_until(b'>', timeout=1).split()[-3]) def temp_act(self, laser_num=1): """ Gets actual DL temp :return: (float) value of temperature """ self.dlc.write( f"(param-disp 'laser{laser_num}:dl:tc:temp-act)\n".encode('utf')) return float(self.dlc.read_until(b'>', timeout=1).split()[-3]) def set_temp(self, temp, laser_num=1): """ Sets the current to desired value :param temp: (float) value of temperature to set to in Celsius """ t = deepcopy(temp) write_data = f"(param-set! 'laser{laser_num}:dl:tc:temp-set {t})\n".encode( 'utf') self.dlc.write(write_data) self.dlc.read_until(b'>', timeout=1) def configure_scan(self, offset=65, amplitude=100, frequency=0.2, laser_num=1): """ Sets the scan parameters for piezo scanning :param offset: (float) scan offset (center value) in volts (between 0 and 130) :param amplitude: (float) scan amplitude (peak to peak) in volts :param frequency: (Float) scan frequency in Hz """ # Check that parameters are within range if (offset + (amplitude / 2) > 130) or (offset - (amplitude / 2) < -1): self.log.warn('Warning, invalid scan parameters set.' 'Make sure voltages are between -1 and 130 V') else: o = deepcopy(offset) write_data = f"(param-set! 'laser{laser_num}:scan:offset {o})\n".encode( 'utf') self.dlc.write(write_data) self.dlc.read_until(b'>', timeout=1) a = deepcopy(amplitude) write_data = f"(param-set! 'laser{laser_num}:scan:amplitude {a})\n".encode( 'utf') self.dlc.write(write_data) self.dlc.read_until(b'>', timeout=1) f = deepcopy(frequency) write_data = f"(param-set! 'laser{laser_num}:scan:frequency {f})\n".encode( 'utf') self.dlc.write(write_data) self.dlc.read_until(b'>', timeout=1) self.log.info( f'Scan with offset {o}, amplitude {a}, frequency {f} for laser {laser_num} successfully configured.' ) def start_scan(self, laser_num=1): """ Starts a piezo scan """ write_data = f"(param-set! 'laser{laser_num}:scan:enabled #t)\n".encode( 'utf') self.dlc.write(write_data) self.dlc.read_until(b'>', timeout=1) def stop_scan(self, laser_num=1): """ Stops piezo scan """ write_data = f"(param-set! 'laser{laser_num}:scan:enabled #f)\n".encode( 'utf') self.dlc.write(write_data) self.dlc.read_until(b'>', timeout=1)
class Driver(WavemeterInterface): """ Hardware class to control High Finesse Wavemeter.""" def __init__(self, logger=None): """ Instantiate wavemeter :param logger: instance of LogClient class (optional) """ # Log self.log = LogHandler(logger=logger) # Load WLM DLL try: self._wavemeterdll = ctypes.windll.LoadLibrary('wlmData.dll') except: msg_str = 'High-Finesse WS7 Wavemeter is not properly installed on this computer' self.log.error(msg_str) raise WavemeterError(msg_str) # Set all DLL function parameters and return value types self._wavemeterdll.GetWLMVersion.restype = ctypes.c_long self._wavemeterdll.GetWLMVersion.argtype = ctypes.c_long self._wavemeterdll.GetWLMCount.restype = ctypes.c_long self._wavemeterdll.GetWLMCount.argtype = ctypes.c_long self._wavemeterdll.GetWavelengthNum.restype = ctypes.c_double self._wavemeterdll.GetWavelengthNum.argtypes = [ ctypes.c_long, ctypes.c_double ] self._wavemeterdll.GetFrequencyNum.restype = ctypes.c_double self._wavemeterdll.GetFrequencyNum.argtypes = [ ctypes.c_long, ctypes.c_double ] # Check that WLM is running and log details self._is_running = self._wavemeterdll.GetWLMCount(0) if self._is_running > 0: self._wlm_version = self._wavemeterdll.GetWLMVersion(0) self.log.info('Connected to High-Finesse Wavemeter WS-{0}'.format( self._wlm_version)) else: msg_str = 'High-Finesse WS7 Wavemeter software not running.\n' 'Please run the Wavemeter software and try again.' self.log.warn(msg_str) # raise WavemeterError(msg_str) def get_wavelength(self, channel=1, units='Frequency (THz)'): """ Returns the wavelength in specified units for a given channel :param channel: Channel number from 1-8 :param units: "Frequency (THz)" or "Wavelength (nm)". Defaults to frequency. """ if units == 'Wavelength (nm)': return self._wavemeterdll.GetWavelengthNum(channel, 0) else: return self._wavemeterdll.GetFrequencyNum(channel, 0)
class DataTaker: def __init__(self, logger=None, client_tuples=None, config=None, config_name=None): self.log = LogHandler(logger) self.dataset = None # Instantiate GUI window self.gui = Window(gui_template='data_taker', host=get_ip()) # Configure list of experiments self.gui.config.setText(config_name) self.config = config self.exp_path = self.config['exp_path'] if self.exp_path is None: self.exp_path = os.getcwd() sys.path.insert(1, self.exp_path) self.update_experiment_list() # Configure list of clients self.clients = {} # Configure list of missing clients self.missing_clients = {} # Setup Autosave # First check whether autosave is specified in config file if 'auto_save' in self.config: if self.config['auto_save']: self.gui.autosave.setChecked(True) # Retrieve Clients for client_entry in self.config['servers']: client_type = client_entry['type'] client_config = client_entry['config'] client = find_client(clients=client_tuples, settings=client_config, client_type=client_type, client_config=client_config, logger=self.log) if (client == None): self.missing_clients[f"{client_type}_{client_config}"] = [ client_type, client_config ] else: self.clients[f"{client_type}_{client_config}"] = client for client_name, client_obj in self.clients.items(): client_item = QtWidgets.QListWidgetItem(client_name) client_item.setToolTip(str(client_obj)) self.gui.clients.addItem(client_item) for client_name, client_config in self.missing_clients.items(): client_item = QtWidgets.QListWidgetItem(client_name) client_item.setForeground(Qt.gray) self.gui.clients.addItem(client_item) self.log.error("Datataker missing client: " + client_name) # Configure button clicks self.gui.configure.clicked.connect(self.configure) self.gui.run.clicked.connect(self.run) self.gui.save.clicked.connect(self.save) self.gui.clearData.clicked.connect(self.clear_data) self.gui.load_config.clicked.connect(self.reload_config) self.gui.showMaximized() self.gui.apply_stylesheet() def update_experiment_list(self): """ Updates list of experiments """ self.gui.exp.clear() for filename in os.listdir(self.exp_path): if filename.endswith('.py'): self.gui.exp.addItem(filename[:-3]) self.gui.exp.itemClicked.connect(self.display_experiment) def display_experiment(self, item): """ Displays the currently clicked experiment in the text browser :param item: (QlistWidgetItem) with label of name of experiment to display """ with open(os.path.join(self.exp_path, f'{item.text()}.py'), 'r') as exp_file: exp_content = exp_file.read() self.gui.exp_preview.setText(exp_content) self.gui.exp_preview.setStyleSheet('font: 10pt "Consolas"; ' 'color: rgb(255, 255, 255); ' 'background-color: rgb(0, 0, 0);') self.log.update_metadata(experiment_file=exp_content) def configure(self): """ Configures the currently selected experiment + dataset """ # If the experiment is running, do nothing try: if self.experiment_thread.isRunning(): self.log.warn('Did not configure experiment, since it ' 'is still in progress') return except: pass # Load the config self.reload_config() # Set all experiments to normal state and highlight configured expt for item_no in range(self.gui.exp.count()): self.gui.exp.item(item_no).setBackground( QtGui.QBrush(QtGui.QColor('black'))) self.gui.exp.currentItem().setBackground( QtGui.QBrush(QtGui.QColor('darkRed'))) exp_name = self.gui.exp.currentItem().text() self.module = importlib.import_module(exp_name) self.module = importlib.reload(self.module) # Clear graph area and set up new or cleaned up dataset for index in reversed(range(self.gui.graph_layout.count())): try: self.gui.graph_layout.itemAt(index).widget().deleteLater() except AttributeError: try: self.gui.graph_layout.itemAt(index).layout().deleteLater() except AttributeError: pass self.gui.windows = {} # If we're not setting up a new measurement type, just clear the data # We are reading in the required base-dataset by looking at the define_dataset() as defined in the experiment script. try: classname = self.module.define_dataset() except AttributeError: error_msg = "No 'define_dataset' method found in experiment script." self.log.error( "No 'define_dataset' method found in experiment script.") return try: self.dataset = getattr(datasets, classname)(gui=self.gui, log=self.log, config=self.config) except AttributeError: error_msg = f"Dataset name {classname} as provided in 'define_dataset' method in experiment script is not valid." self.log.error(error_msg) return # Run any pre-experiment configuration try: self.module.configure(dataset=self.dataset, **self.clients) except AttributeError: pass self.experiment = self.module.experiment self.log.info(f'Experiment {exp_name} configured') self.gui.exp_preview.setStyleSheet( 'font: 10pt "Consolas"; ' 'color: rgb(255, 255, 255); ' 'background-color: rgb(50, 50, 50);') def clear_data(self): """ Clears all data from curves""" self.log.info("Clearing data") self.dataset.clear_all_data() def run(self): """ Runs/stops the experiment """ # Run experiment if self.gui.run.text() == 'Run': self.gui.run.setStyleSheet('background-color: red') self.gui.run.setText('Stop') self.log.info('Experiment started') # Run update thread self.update_thread = UpdateThread( autosave=self.gui.autosave.isChecked(), save_time=self.gui.autosave_interval.value()) self.update_thread.data_updated.connect(self.dataset.update) self.update_thread.save_flag.connect(self.save) self.gui.autosave.toggled.connect( self.update_thread.update_autosave) self.gui.autosave_interval.valueChanged.connect( self.update_thread.update_autosave_interval) ''' # Step 2: Create a QThread object self.thread = QThread() # Step 3: Create a worker object self.worker = Worker() # Step 4: Move worker to the thread self.worker.moveToThread(self.thread) # Step 5: Connect signals and slots self.thread.started.connect(self.worker.run) self.worker.finished.connect(self.thread.quit) self.worker.finished.connect(self.worker.deleteLater) self.thread.finished.connect(self.thread.deleteLater) self.worker.progress.connect(self.reportProgress) # Step 6: Start the thread self.thread.start() # Final resets self.longRunningBtn.setEnabled(False) self.thread.finished.connect( lambda: self.longRunningBtn.setEnabled(True) ) self.thread.finished.connect( lambda: self.stepLabel.setText("Long-Running Step: 0") ) ''' self.experiment_thread = ExperimentThread(self.experiment, dataset=self.dataset, gui=self.gui, **self.clients) self.experiment_thread.status_flag.connect( self.dataset.interpret_status) self.experiment_thread.finished.connect(self.stop) self.log.update_metadata( exp_start_time=datetime.now().strftime('%d/%m/%Y %H:%M:%S:%f')) # Stop experiment else: self.experiment_thread.running = False def stop(self): """ Stops the experiment""" self.gui.run.setStyleSheet('background-color: green') self.gui.run.setText('Run') self.log.info('Experiment stopped') self.update_thread.running = False self.log.update_metadata( exp_stop_time=datetime.now().strftime('%d/%m/%Y %H:%M:%S:%f')) # Autosave if relevant if self.gui.autosave.isChecked(): self.save() def save(self): """ Saves data """ self.log.update_metadata(notes=self.gui.notes.toPlainText()) filename = self.gui.save_name.text() directory = self.config['save_path'] self.dataset.save(filename=filename, directory=directory, date_dir=True) save_metadata(self.log, filename, directory, True) self.log.info('Data saved') def reload_config(self): """ Loads a new config file """ self.config = load_script_config(script='data_taker', config=self.gui.config.text(), logger=self.log)
class MCS2: # property keys and other constants PKEY_NUM_BUS = int("0x020F0016", 16) PKEY_NUM_CH = int('0x020F0017', 16) PKEY_NUM_BUS_CH = int('0x02030017', 16) PKEY_MOVE_MODE = int('0x03050087', 16) PKEY_STEP_FREQ = int('0x0305002E', 16) PKEY_STEP_AMP = int('0x03050030', 16) PKEY_VOLT = int('0x0305001F', 16) PKEY_DC_VEL = int('0x0305002A', 16) PKEY_CURRENT_STATE = int('0x0305000F', 16) MOVE_MODE_STEP = 4 MOVE_MODE_DC_REL = 3 MOVE_MODE_DC_ABS = 2 SCALE = 65535 DC_VEL_MIN = 100 / 56635 DC_VEL_MAX = 10**8 def __init__(self, logger=None): """ Instantiate Nanopositioners""" self.log = LogHandler(logger) self.dummy = False try: # Loads Nanopositioners DLL and define arguments and result types for c function self._nanopositionersdll = ctypes.windll.LoadLibrary( 'SmarActCTL.dll') self._configure_functions() #Finds devices connected to controller buffer = ctypes.create_string_buffer( 4096) #the way to have a mutable buffer buffersize = ctypes.c_size_t(ctypes.sizeof( buffer)) #size _t gives c_ulonglong, not as in manual result = self._nanopositionersdll.SA_CTL_FindDevices( None, buffer, buffersize) # Handle errors if result: msg_str = 'No MCS2 devices found' self.log.error(msg_str) #Establishes a connection to a device self.dev_name = buffer.value.decode("utf-8") dhandle = ctypes.c_uint32() connect = self._nanopositionersdll.SA_CTL_Open( dhandle, buffer.value, None) if connect == 0: self.dhandle = dhandle.value self.log.info( f'Connected to device {self.dev_name} with handle {self.dhandle}' ) else: msg_str = f'Failed to connect to device {self.dev_name}' self.log.error(msg_str) # Get channel information channel_buffer = ctypes.c_int32() channel_buffer_size = ctypes.c_size_t( ctypes.sizeof(channel_buffer)) channel_result = self._nanopositionersdll.SA_CTL_GetProperty_i32( self.dhandle, 0, self.PKEY_NUM_CH, channel_buffer, channel_buffer_size) self.num_ch = channel_buffer.value if channel_result == 0 and self.num_ch > 0: self.log.info( f'Found {self.num_ch} channels on {self.dev_name}') else: msg_str = f'Failed to find channels on {self.dev_name}' self.log.error(msg_str) except WindowsError: self.log.warn('Did not find MCS2 DLL, entering dummy mode') self.dummy = True def close(self): """ Closes connection to device""" connect = self._nanopositionersdll.SA_CTL_Close(self.dhandle) if connect: msg_str = f'Failed to properly close connection to {self.dev_name}' self.log.warn(msg_str) else: self.log.info(f'Disconnected from {self.dev_name}') def set_parameters(self, channel, mode=None, frequency=None, amplitude=None, dc_vel=None): """ Sets parameters for motion Leave parameter as None in order to leave un-changed :param channel: (int) index of channel from 0 to self.num_ch :param mode: (str) default is 'step', can use 'dc', 'dc_rel' to set abs or rel DC voltage :param freq: (int) frequency in Hz from 1 to 20000 :param amp: (float) amplitude in volts from 0 to 100 :param dc_vel: (float) velocity for DC steps in volts/sec """ # Set movement mode if mode is not None: if mode == 'dc': result_mode = self._nanopositionersdll.SA_CTL_SetProperty_i32( self.dhandle, channel, self.PKEY_MOVE_MODE, self.MOVE_MODE_DC_ABS) if result_mode: self.log.warn( f'Failed to set mode to DC for positioner {self.dev_name},' f' channel {channel}') elif mode == 'dc_rel': result_mode = self._nanopositionersdll.SA_CTL_SetProperty_i32( self.dhandle, channel, self.PKEY_MOVE_MODE, self.MOVE_MODE_DC_REL) if result_mode: self.log.warn( f'Failed to set mode to DC for positioner {self.dev_name},' f' channel {channel}') else: result_mode = self._nanopositionersdll.SA_CTL_SetProperty_i32( self.dhandle, channel, self.PKEY_MOVE_MODE, self.MOVE_MODE_STEP) if result_mode: self.log.warn( f'Failed to set mode to step for positioner {self.dev_name},' f' channel {channel}') # Set frequency if frequency is not None: # Check for reasonable range if 1 <= frequency <= 20000: result_freq = self._nanopositionersdll.SA_CTL_SetProperty_i32( self.dhandle, channel, self.PKEY_STEP_FREQ, int(frequency)) if result_freq: self.log.warn( f'Failed to set step frequency to {frequency} for positioner ' f'{self.dev_name}, channel {channel}') # Handle out of range request else: self.log.warn( 'Warning, can only set frequency within 1 Hz and 20 kHz') # Set amplitude if amplitude is not None: # Check for reasonable range bit_amp = value_to_bitval(amplitude, bits=16, min=0, max=100) if 1 <= bit_amp <= 65535: result_amp = self._nanopositionersdll.SA_CTL_SetProperty_i32( self.dhandle, channel, self.PKEY_STEP_AMP, bit_amp) if result_amp: self.log.warn( f'Failed to set step amplitude to {amplitude} for positioner ' f'{self.dev_name}, channel {channel}') else: self.log.warn( 'Warning, can only set amplitude in the range of 0 to 100 V' ) # Set DC velocity if dc_vel is not None: # Check for reasonable range bit_vel = int(dc_vel * (self.SCALE / 100)) if 1 <= bit_vel <= self.SCALE * 10**6: result_vel = self._nanopositionersdll.SA_CTL_SetProperty_i64( self.dhandle, channel, self.PKEY_DC_VEL, bit_vel) if result_vel: self.log.warn( f'Failed to set DC velocity to {dc_vel} V/s for positioner ' f'{self.dev_name}, channel {channel}') else: self.log.warn('Warning, can only set velocity in the range of ' f'{self.DC_VEL_MIN} to {self.DC_VEL_MAX} V/s') def get_voltage(self, channel): """ Returns the current DC voltage on a piezo :param channel: (int) channel index (from 0) """ voltage_buffer = ctypes.c_int64() voltage_buffer_size = ctypes.c_size_t(ctypes.sizeof(voltage_buffer)) voltage_result = self._nanopositionersdll.SA_CTL_GetProperty_i64( self.dhandle, channel, self.PKEY_VOLT, voltage_buffer, voltage_buffer_size) # Handle error if voltage_result: self.log.warn(f'Could not retrieve voltage for channel {channel}' f'on device {self.dev_name}') return bitval_to_value(voltage_buffer.value, bits=16, min=0, max=100) def set_voltage(self, channel, voltage=50): """ Sets an absolute voltage to the piezo :param channel: (int) channel index (from 0) :param voltage: (float) voltage to set from 0 to 100 V (default is 50) """ # Change the move mode to voltage absolute self.set_parameters(channel, mode='dc') # Move to desired voltage bit_voltage = value_to_bitval(voltage, bits=16, min=0, max=100) move_result = self._nanopositionersdll.SA_CTL_Move( self.dhandle, channel, bit_voltage, 0) # Check success if move_result: self.log.warn( f'Failed to set DC voltage to {voltage} V on channel {channel} of {self.dev_name}' ) def n_steps(self, channel, n=1): """ Takes n steps :param channel: (int) channel index (from 0) :param n: (int) number of steps to take, negative is in opposite direction """ # Take the step self.set_parameters(channel, mode='step') result_step = self._nanopositionersdll.SA_CTL_Move( self.dhandle, channel, n, 0) # Handle error if result_step: self.log.warn( f'Failed to take {n} steps on device {self.dev_name}, channel {channel}' ) def move(self, channel, backward=False): """ Takes the maximum number of steps (quasi continuous) :param channel: (int) channel index (from 0) :param backward: (bool) whether or not to step in backwards direction (default False) """ # Configure move self.set_parameters(channel, mode='step') if backward: MOVE_STEPS = -100000 else: MOVE_STEPS = 100000 # Send move command result_move = self._nanopositionersdll.SA_CTL_Move( self.dhandle, channel, MOVE_STEPS, 0) # Handle error if result_move: self.log.warn( f'Failed to take move on device {self.dev_name}, channel {channel}' ) def stop(self, channel): """ Terminates any ongoing movement :param channel: (int) channel index (from 0) """ result_stop = self._nanopositionersdll.SA_CTL_Stop( self.dhandle, channel, 0) if result_stop: self.log.warn( f'Failed to stop movement on device {self.dev_name}, channel {channel}' ) def is_moving(self, channel): """ Returns whether or not the positioner is moving :param channel: (int) channel index (from 0) :return: (bool) true if moving """ # Get the state bit current_state_buffer = ctypes.c_int32() current_state_buffer_size = ctypes.c_size_t( ctypes.sizeof(current_state_buffer)) state_result = self._nanopositionersdll.SA_CTL_GetProperty_i32( self.dhandle, channel, self.PKEY_CURRENT_STATE, current_state_buffer, current_state_buffer_size) # Handle an error if state_result: self.log.warn( f'Failed to check if positioner {self.dev_name} is moving on' f'Channel {channel}') # Decode state bit if current_state_buffer.value % 2 == 0: return False else: return True # Technical methods def _configure_functions(self): """ Defines arguments and results for c functions """ # Device connection, disconnection self._nanopositionersdll.SA_CTL_GetFullVersionString.restype = ctypes.c_char_p self._nanopositionersdll.SA_CTL_FindDevices.argtypes = [ ctypes.c_char_p, # options for find procedure ctypes.POINTER( ctypes.c_char), # pointer to buffer for writing device locator ctypes.POINTER(ctypes.c_ulonglong ) # pointer to variable holding size of buffer ] self._nanopositionersdll.SA_CTL_FindDevices.restype = ctypes.c_uint32 # result status self._nanopositionersdll.SA_CTL_Open.argtypes = [ ctypes.POINTER( ctypes.c_uint32 ), # pointer to device handle for use in future calls ctypes.c_char_p, # device locator ctypes.c_char_p # config (unused) ] self._nanopositionersdll.SA_CTL_Open.restypes = ctypes.c_uint32 # result status self._nanopositionersdll.SA_CTL_Close.argtype = ctypes.c_uint32 # device handle self._nanopositionersdll.SA_CTL_Close.restype = ctypes.c_uint32 # result status # Getting device properties self._nanopositionersdll.SA_CTL_GetProperty_i32.argtypes = [ ctypes.c_uint32, # device handle ctypes.c_int8, # index of addressed device, module, or channel ctypes.c_uint32, # property key ctypes.POINTER( ctypes.c_int32), # pointer to buffer where result is written ctypes.POINTER( ctypes.c_ulonglong ) # pointer to size of value buffer (number of elements) ] self._nanopositionersdll.SA_CTL_GetProperty_i32.restype = ctypes.c_uint32 # result status self._nanopositionersdll.SA_CTL_GetProperty_i64.argtypes = [ ctypes.c_uint32, # device handle ctypes.c_int8, # index of addressed device, module, or channel ctypes.c_uint32, # property key ctypes.POINTER( ctypes.c_int64), # pointer to buffer where result is written ctypes.POINTER(ctypes.c_ulonglong ) # pointer to size of value buffer (# of elements) ] self._nanopositionersdll.SA_CTL_GetProperty_i64.restype = ctypes.c_uint32 # result status # Settting device properties self._nanopositionersdll.SA_CTL_SetProperty_i32.argtypes = [ ctypes.c_uint32, # device handle ctypes.c_int8, # index of addressed device, module, or channel ctypes.c_uint32, # property key ctypes.c_int32, # value to write ] self._nanopositionersdll.SA_CTL_SetProperty_i32.restype = ctypes.c_uint32 # result status self._nanopositionersdll.SA_CTL_SetProperty_i64.argtypes = [ ctypes.c_uint32, # device handle ctypes.c_int8, # index of addressed device, module, or channel ctypes.c_uint32, # property key ctypes.c_int64 # value to write ] self._nanopositionersdll.SA_CTL_SetProperty_i64.restype = ctypes.c_uint32 # result status # Movement self._nanopositionersdll.SA_CTL_Move.argtypes = [ ctypes.c_uint32, # device handle ctypes.c_int8, # index of addressed device, module, or channel ctypes.c_int64, # move value ctypes.c_uint32 # transmit handle ] self._nanopositionersdll.SA_CTL_Move.restype = ctypes.c_uint32 # result status self._nanopositionersdll.SA_CTL_Stop.argtypes = [ ctypes.c_uint32, # device handle ctypes.c_int8, # index of addressed device, module, or channel ctypes.c_uint32 # transmit handle ] self._nanopositionersdll.SA_CTL_Stop.restype = ctypes.c_uint32 # result status
class Wrap(GatedCtrInterface): def __init__(self, tagger, click_ch, gate_ch, logger=None): """Instantiate gated counter :param tagger: instance of TimeTagger class :param click_ch: (int|list of int) clicks on all specified channels will be summed into one logical channel :param gate_ch: (int) positive/negative channel number - count while gate is high/low """ # Log self.log = LogHandler(logger=logger) # Reference to tagger self._tagger = tagger # Log device ID information to demonstrate that connection indeed works serial = self._tagger.getSerial() model = self._tagger.getModel() self.log.info( 'Got reference to Swabian Instruments TimeTagger device \n' 'Serial number: {0}, Model: {1}' ''.format(serial, model)) # Gated Counter # reference to the TT.CountBetweenMarkers measurement instance self._ctr = None # number of count bins: # length of returned 1D count array, the expected number of gate pulses, # the size of allocated memory buffer. # must be given as argument of init_ctr() call self._bin_number = 0 # Channel assignments self._click_ch = 0 self._gate_ch = 0 # reference to Combiner object # (if _click_ch is a list - then counts on all channels are summed # into virtual channel - self._combiner.getChannel()) self._combiner = None # apply channel assignment self.set_ch_assignment(click_ch=click_ch, gate_ch=gate_ch) # Module status code # -1 "void" # 0 "idle" # 1 "in_progress" # 2 "finished" self._status = -1 self._set_status(-1) # Once __init__() call is complete, # the counter is ready to be initialized by the above-lying logic though init_ctr() call # ---------------- Interface --------------------------- def activate_interface(self): return 0 def init_ctr(self, bin_number, gate_type): # Device-specific fix explanation: # # CountBetweenMarkers measurement configured for n_value bins # indeed fills-up buffer after n_value gate pules, but call of # self._ctr.ready() still gives False. Only after one additional # gate pulse it gives True, such that self.get_status() # gives 2 and self.get_count_ar() returns: # # device always needs an additional pulse to complete # (even for n_values = 1 it needs 2 gate pulses). # # Since this is very counter-intuitive and confuses # above-lying logic, a fix is made here: # # For given bin_number, CountBetweenMarkers measurement # is instantiated with n_values = (bin_number - 1) such # that it completes after receiving bin_number physical # gate pulses as expected. # # As a result, in the returned count_ar # (still of length bin_number, as logic expects), the last # value is just a copy of (bin_number - 1)-st element. # # The last physical bin is not actually measured, what can lead # to confusions when bin_number is on the order of one. # The warning below reminds about it: if bin_number <= 5: self.log.warn( 'init_ctr(): due to strange behaviour of TT.CountBetweenMarkers ' 'measurement, this driver makes a hack: counter is configured to ' 'measure bin_number-1 pulses and the last element of the returned ' 'count_ar is just a copy of the preceding one. \n' 'With bin_number={}, only the first {} gate windows will actually be ' 'measured.' ''.format(bin_number, bin_number - 1)) # Close existing counter, if it was initialized before if self.get_status() != -1: self.close_ctr() # Instantiate counter measurement try: if gate_type == 'RF': self._ctr = TT.CountBetweenMarkers( tagger=self._tagger, click_channel=self._click_ch, begin_channel=self._gate_ch, end_channel=-self._gate_ch, n_values=bin_number - 1) elif gate_type == 'RR': self._ctr = TT.CountBetweenMarkers( tagger=self._tagger, click_channel=self._click_ch, begin_channel=self._gate_ch, n_values=bin_number - 1) else: msg_str = 'init_ctr(): unknown gate type "{}" \n' \ 'Valid types are: \v' \ ' "RR" - Raising-Raising \n' \ ' "RF" - Raising-Falling' self.log.error(msg_str=msg_str) raise CtrError(msg_str) # set status to "idle" self._set_status(0) # save bin_number in internal variable self._bin_number = bin_number # handle NotImplementedError (typical error, produced by TT functions) except NotImplementedError: # remove reference to the counter measurement self._ctr = None # set status to "void" self._set_status(-1) msg_str = 'init_ctr(): instantiation of CountBetweenMarkers measurement failed' self.log.error(msg_str=msg_str) raise CtrError(msg_str) # Prepare counter to be started by start_counting() # (CountBetweenMarkers measurement starts running immediately after instantiation, # so it is necessary to stop it and erase all counts collected between instantiation and stop() call) self._ctr.stop() self._ctr.clear() return 0 def close_ctr(self): # Try to stop and to clear TT.CountBetweenMarkers measurement instance try: self._ctr.stop() self._ctr.clear() except: pass # Remove reference, set status to "void" self._ctr = None self._set_status(-1) return 0 def start_counting(self): current_status = self.get_status() # Sanity check: ensure that counter is not "void" if current_status == -1: msg_str = 'start_counting(): ' \ 'counter is in "void" state - it ether was not initialized or was closed. \n' \ 'Initialize it by calling init_ctr()' self.log.error(msg_str=msg_str) raise CtrError(msg_str) # Terminate counting if it is already running if current_status == 1: self.terminate_counting() # Try stopping and restarting counter measurement try: self._ctr.stop( ) # does not fail even if the measurement is not running self._ctr.clear() self._ctr.start() # set status to "in_progress" self._set_status(1) # Wait until the counter is actually ready to count time.sleep(0.1) return 0 # handle exception in TT function calls [NotImplementedError] except NotImplementedError: # Since stop() and clear() methods are very robust, # this part is only executed if counter is totally broken. # In this case it makes sense to close counter. self.close_ctr() msg_str = 'start_counting(): call failed. Counter was closed. \n'\ 'Re-initialize counter by calling init_ctr() again' self.log.error(msg_str=msg_str) raise CtrError(msg_str) def terminate_counting(self): # Action of this method is non-trivial for "in_progress" state only if self.get_status() != 1: return 0 # Try stopping and clearing counter measurement try: # stop counter, clear count array self._ctr.stop() self._ctr.clear() # set status to "idle" self._set_status(0) return 0 # handle exception in TT.stop()/TT.clear() except NotImplementedError: # Since stop() and clear() methods are very robust, # this part is only executed if counter is totally broken. # In this case it makes sense to close counter. self.close_ctr() msg_str = 'terminate_counting(): call failed. Counter was closed. \n' \ 'Re-initialize it by calling init_ctr()' self.log.error(msg_str=msg_str) raise CtrError(msg_str) def get_status(self): # Check that counter measurement was initialized and that the connection works # by calling isRunning() # -- if self._ctr is None or if connection is broken, call will rise some # exception. In this case "void" status should be set # -- if counter was initialized and connection works, it will return successfully # (True or False, but the result does not matter) # and further choice between "idle", "in_progress", and "finished" should be made try: self._ctr.isRunning() except: # set status to "void" self._status = -1 # No handling of "idle" and "finished" status is needed: # it will be returned by self._status as is # Handle "in_progress" status # This status means that measurement was started before. # Now one needs to check if it is already finished or not. # If measurement is complete, change status to "finished". if self._status == 1: if self._ctr.ready(): self._ctr.stop() self._status = 2 return copy.deepcopy(self._status) def get_count_ar(self, timeout=-1): # If current status is "in_progress", # wait for transition to some other state: # "finished" if measurement completes successfully, # "idle" if measurement is terminated, # "void" if counter breaks start_time = time.time() sleep_time = abs(timeout) / 100 while self.get_status() == 1: # stop waiting if timeout elapses if time.time() - start_time > timeout >= 0: break time.sleep(sleep_time) # Analyze current status and return correspondingly status = self.get_status() # return data only in the case of "finished" state if status == 2: count_array = np.array(self._ctr.getData(), dtype=np.uint32) # Fix of the issue with an additional gate pulse needed to complete # measurement (see comment in init_ctr() for explanation): # the last element of returned array is just a copy # of the last physically measured bin count_array = np.append(count_array, count_array[-1]) return count_array # return empty list for all other states ("in_progress", "idle", and "void") else: if status == 1: self.log.warn( 'get_count_ar(): operation timed out, but counter is still running. \n' 'Try calling get_count_ar() later or terminate process by terminate_counting().' ) elif status == 0: self.log.warn( 'get_count_ar(): counter is "idle" - nothing to read') else: msg_str = 'get_count_ar(): counter broke and was deleted \n' \ 'Re-initialize it by calling init_ctr()' self.log.error(msg_str=msg_str) raise CtrError(msg_str) return [] # ------------------------------------------------------ def _set_status(self, new_status): """Method to set new status in a clean way. This method compares the requested new_status with current status and checks if this transition is possible. If transition is possible, the change is applied to self._status. Otherwise, no status change is applied, -1 is returned, and error message is logged. :param new_status: (int) new status value -1 - "void" 0 - "idle" 1 - "in_progress" 2 - "finished" :return: (int) operation status code: 0 - OK, change was accepted and applied -1 - Error, impossible transition was requested, no state change was applied """ # Transition to "void" is always possible # by calling close_ctr() if new_status == -1: self._status = -1 return 0 # Transition to "idle" is possible from # "void" by calling init_ctr() # "in_progress" by calling terminate_counting() if new_status == 0: if self._status == -1 or self._status == 1: self._status = 0 return 0 else: msg_str = '_set_status(): transition to new_status={0} from self._status={1} is impossible. \n'\ 'Counter status was not changed.'\ ''.format(new_status, self._status) self.log.error(msg_str=msg_str) raise CtrError(msg_str) # Transition to "in_progress" is possible from # "idle" by calling start_counting() # "finished" by calling start_counting() if new_status == 1: if self._status == 0 or self._status == 2: self._status = 1 return 0 else: msg_str = '_set_status(): transition to new_status={0} from self._status={1} is impossible. \n'\ 'Counter status was not changed.'\ ''.format(new_status, self._status) self.log.error(msg_str=msg_str) raise CtrError(msg_str) # Transition to "finished" is only possible from "in_progress" # by successful completion of count_array accumulation if new_status == 2: if self._status == 1: self._status = 2 return 0 else: msg_str = '_set_status(): transition to new_status={0} from self._status={1} is impossible. \n'\ 'Counter status was not changed.'\ ''.format(new_status, self._status) self.log.error(msg_str=msg_str) raise CtrError(msg_str) def get_ch_assignment(self): """Returns dictionary containing current channel assignment: { 'click_ch': (int) click_channel_number_including_edge_sign 'gate_ch': (int) gate_channel_number_including_edge_sign } :return: dict('click_ch': _, 'gate_ch': _) """ click_ch = copy.deepcopy(self._click_ch) gate_ch = copy.deepcopy(self._gate_ch) return dict(click_ch=click_ch, gate_ch=gate_ch) def set_ch_assignment(self, click_ch=None, gate_ch=None): """Sets click channel and and gate channel. This method only changes internal variables self._click_ch and self._gate_ch. To apply the channel update, call init_ctr() again. :param click_ch: (int|list of int) click channel number positive/negative values - rising/falling edge detection if list is given, clicks on all specified channels will be merged into one logic channel :param gate_ch: (int) channel number positive/negative - count during high/low gate level :return: (dict) actually channel assignment: { 'click_channel': (int) click_chnl_num, 'gate_channel': (int) gate_chnl_num } """ if click_ch is not None: # for convenience bring int type of input to list of int if isinstance(click_ch, list): click_ch_list = click_ch elif isinstance(click_ch, int): click_ch_list = [click_ch] else: # unknown input type msg_str = 'set_ch_assignment(click_ch={0}): invalid argument type'\ ''.format(click_ch) self.log.error(msg_str=msg_str) raise CtrError(msg_str) # sanity check: all requested channels are available on the device all_chs = self.get_all_chs() for channel in click_ch_list: if channel not in all_chs: msg_str = 'set_ch_assignment(): '\ 'click_ch={0} - this channel is not available on the device'\ ''.format(click_ch) self.log.error(msg_str=msg_str) raise CtrError(msg_str) # If several channel numbers were passed, create virtual Combiner channel if len(click_ch_list) > 1: self._combiner = TT.Combiner(tagger=self._tagger, channels=click_ch_list) # Obtain int channel number for the virtual channel click_ch_list = [self._combiner.getChannel()] # Set new value for click channel self._click_ch = int(click_ch_list[0]) if gate_ch is not None: # sanity check: channel is available on the device if gate_ch not in self.get_all_chs(): msg_str = 'set_ch_assignment(): '\ 'gate_ch={0} - this channel is not available on the device'\ ''.format(gate_ch) self.log.error(msg_str=msg_str) raise CtrError(msg_str) # Set new value for gate channel self._gate_ch = int(gate_ch) return self.get_ch_assignment() def get_all_chs(self): """Returns list of all channels available on the device, including edge type sign. Positive/negative numbers correspond to detection of rising/falling edges. For example: 1 means 'rising edge on connector 1' -1 means 'falling edge on connector 1 :return: (list of int) list of channel numbers including edge sign. Example: [-8, -7, -6, -5, -4, -3, -2, -1, 1, 2, 3, 4, 5, 6, 7, 8] Empty list is returned in the case of error. """ # Sanity check: check that connection to the device was established if self._tagger is None: msg_str = 'get_all_chs(): not connected to the device yet' self.log.error(msg_str=msg_str) raise CtrError(msg_str) channel_list = list( self._tagger.getChannelList( TT.TT_CHANNEL_RISING_AND_FALLING_EDGES)) return channel_list
class Driver(): """Driver class for GPIB controlled Agilent EE405 Spectrum analyser""" def reset(self): """ Create factory reset""" self.device.write('*RST') self.log.info("Reset to factory settings successfull.") def __init__(self, gpib_address, logger): """Instantiate driver class :gpib_address: GPIB-address of spectrum analyzer, e.g. 'GPIB0::12::INSTR' Can be read out by using rm = pyvisa.ResourceManager() rm.list_resources() :logger: And instance of a LogClient """ # Instantiate log self.log = LogHandler(logger=logger) self.rm = ResourceManager() try: self.device = self.rm.open_resource(gpib_address) device_id = self.device.query('*IDN?') self.log.info(f"Successfully connected to {device_id}.") except VisaIOError: self.log.error(f"Connection to {gpib_address} failed.") # reset to factory settings self.reset() def display_off(self): """ Power off display """ self.device.write(':DISPlay:ENABle OFF') self.log.info("Display off.") def display_on(self): """ Power on display """ self.device.write(':DISPlay:ENABle ON') self.log.info("Display on.") def set_attenuation(self, db): """ Set input attenuation :db: Target attenuation in dB, must be between 0 and 75 """ if not 0 <= db <= 75: self.log.error( f'Invalid attenuation ({db}dB). Attenuation must be between 0dB and 75dB' ) self.device.write(f'POW:ATT {int(db)}dB') self.log.info(f'Input attenuation set to {db}dB.') def set_reference_level(self, db): """Set reference level :db: Target reference level in dB """ self.device.write(f':DISPlay:WINDow:TRACe:Y:RLEVel {int(db)}') self.log.info(f'Reference level set to {db}dB.') def set_center_frequency(self, center_frequency): """ Set center frequency of trace. :center_frequency: Frequency in Hz (from 0 to 13.3 GHz) """ if not 0 <= center_frequency <= 13.2 * 1e9: self.log.error( f'Invalid center frequency ({center_frequency} Hz). Must be within 0 and 13.2 GHz' ) self.device.write(f':SENSe:FREQuency:CENTer {center_frequency}') self.log.info(f'Center frequency set to {center_frequency} Hz') def set_frequency_span(self, frequency_span): """ Set frequency span of trace. :frequency_span: Frequency span in Hz (from 0 to 13.3 GHz) """ if not 0 <= frequency_span <= 13.2 * 1e9: self.log.error( f'Invalid frequency span ({frequency_span} Hz). Must be within 0 and 13.2 GHz' ) self.device.write(f':SENSe:FREQuency:SPAN {frequency_span}') self.log.info(f'Frequency span set {frequency_span} Hz') def toggle_cont(self, target_state): """Switch between single shot and continuous acquisition mode :target_state: Index of targe stat. 1 for continuous mode, 0 for single shot mode. """ self.device.write(f'INIT:CONT {target_state}') def get_frequency_array(self): """Constructs array of frequencies associated with trace points""" # Sweep start frequency. start_freq = float(self.device.query(':SENSe:FREQuency:STARt?')) # Sweep end frequency. end_freq = float(self.device.query(':SENSe:FREQuency:STOP?')) # Number of sweep points. num_sweep_points = int(self.device.query('SENSE:SWEEP:POINTS?')) # Array containing frequencies of each sweep point. frequencies = np.linspace(start_freq, end_freq, num_sweep_points) return frequencies def read_trace(self): """ Read and return trace Retruns array trace contaning frequencies (in Hz) of data points in trace[:,0] and power levels (in dBm) in trace[:,1] """ # Set to single shot mode. self.toggle_cont(0) # Trigger a sweep and wait for sweep to complete. self.device.write('INIT:IMM;*WAI') # Specify units in dBm. self.device.write('UNIT:POW DBM') # Specify data format as ASCII. self.device.write('FORM:DATA ASC') # Trigger a sweep and wait for sweep to complete. self.device.write('INIT:IMM;*WAI') # Query trace data dbm_measurement = self.device.query_ascii_values('TRAC:DATA? TRACE1') # Return to continuos monitoring mode self.toggle_cont(1) # Read frequency axis frequencies = self.get_frequency_array() # Combine trace data trace = np.stack((frequencies, dbm_measurement), axis=-1) return trace def acquire_background_spectrum(self, num_points=100): """Acquires an average background trace. :num_traces: How many traces to sample. :nocheck: Boolean indicating if user confirmation that no input signal is present will be omitted. """ traces = [] self.display_off() for i in range(num_points): traces.append(self.read_trace()) self.display_on() noise_average = np.average(np.asarray(traces)[:, :, 1], 0) noise_trace = np.array([traces[0][:, 0], noise_average]) self.noise_trace = np.transpose(noise_trace) def get_background(self): try: noise_trace = self.noise_trace return noise_trace except AttributeError: error_msg = 'No background acquired. Run acquire_background_spectrum() to acquire background.' self.log.warn(error_msg) return error_msg
class Driver(): POWER_RANGE = [-120, 30] # acceptable power range in dBm POWER_PRECISION = 2 # number of digits of precision for power FREQ_RANGE = [1e7, 2e10] # acceptable frequency range in Hz def __init__(self, gpib_address, logger): """ Instantiate driver class, connects to device :param gpib_address: GPIB-address of the device, can be found with pyvisa.ResourceManager.list_resources() :param logger: instance of LogClient """ self.log = LogHandler(logger=logger) self.rm = ResourceManager() try: self.device = self.rm.open_resource(gpib_address) self.device.read_termination = '\n' self.device_id = self.device.query('*IDN?') self.log.info(f'Successfully connected to {self.device_id}') except VisaIOError: self.log.error(f'Connection to {gpib_address} failed') raise def set_power(self, power): """ Sets power of MW source :param power: (float) output power to set in dBm """ power = round(power, self.POWER_PRECISION) # Check for valid range if power < self.POWER_RANGE[0] or power > self.POWER_RANGE[1]: self.log.warn( f'Warning, power outside acceptable range {self.POWER_RANGE}. ' f'Output power was not updated.') # Set power else: self.device.write(f'POW {power}') self.log.info(f'Set MW power to {power}') def set_freq(self, freq): """ Sets power of MW source :param freq: (float) output frequency in Hz """ freq = int(round(freq)) # Check for valid range if freq < self.FREQ_RANGE[0] or freq > self.FREQ_RANGE[1]: self.log.warn( f'Warning, frequency outside acceptable range {self.FREQ_RANGE}. ' f'Output frequency was not updated.') # Set freq else: self.device.write(f'FREQ {freq}') self.log.info(f'Set MW freq to {freq}') def output_on(self): """ Turn output on """ self.device.write('OUTP ON') self.log.info('MW output turned on') def output_off(self): """ Turn output off """ self.device.write('OUTP OFF') self.log.info('MW output turned off')
class Driver: BOARDS = 8 def __init__(self, address=None, logger=None): """Instantiate driver class. :address: Address of the device, e.g. 'ASRL3::INSTR' Can be read out by using rm = pyvisa.ResourceManager() rm.list_resources() :logger: An instance of a LogClient. """ # Instantiate log self.log = LogHandler(logger=logger) self.addr = address self.rm = ResourceManager() try: self.device = self.rm.open_resource(self.addr) self.log.info(f"Successfully connected to {self.device}.") # Configure device grammar self.device.write_termination = ';' except VisaIOError: self.log.error(f"Connection to {self.addr} failed.") raise def measure_voltage(self, board, channel): """ Measures the current voltage on a particular channel of a particular board :param board: (int) integer between 0 and 7 (assuming 8 boards) :param channel: (int) integer between 0 and 3 :return: (float) voltage in volts (into open-loop) """ # Only proceed if we can correctly set the current board and channel if self._set_board(board) + self._set_channel(channel): self.log.warn( f'Did not measure the voltage for board {board} channel {channel}' ) return float(-777) return bitval_to_value(int(self.device.query('v').split()[-1]), bits=12, min=0, max=10) def set_high_voltage(self, board, channel, voltage): """ Sets a channel's high voltage :param board: (int) integer between 0 and 7 (assuming 8 boards) :param channel: (int) integer between 0 and 3 :param voltage: (float) voltage in V between 0 and 10 (into open-loop) :return: (int) 0 if successful """ # Only proceed if we can correctly set the current board and channel if self._set_board(board) + self._set_channel(channel): self.log.warn( f'Did not measure the voltage for board {board} channel {channel}' ) return float(-777) return self._set_high_voltage(voltage) def set_low_voltage(self, board, channel, voltage): """ Sets a channel's low voltage :param board: (int) integer between 0 and 7 (assuming 8 boards) :param channel: (int) integer between 0 and 3 :param voltage: (float) voltage in V between 0 and 10 (into open-loop) :return: (int) 0 if successful """ # Only proceed if we can correctly set the current board and channel if self._set_board(board) + self._set_channel(channel): self.log.warn( f'Did not measure the voltage for board {board} channel {channel}' ) return float(-777) return self._set_low_voltage(voltage) def get_high_voltage(self, board, channel): """ Gets a channel's high voltage :param board: (int) integer between 0 and 7 (assuming 8 boards) :param channel: (int) integer between 0 and 3 :return: (float) voltage in V from 0 to 10 """ # Only proceed if we can correctly set the current board and channel if self._set_board(board) + self._set_channel(channel): self.log.warn( f'Did not measure the voltage for board {board} channel {channel}' ) return float(-777) return bitval_to_value(int(self.device.query('h').split()[-1]), bits=16, min=0, max=10) def get_low_voltage(self, board, channel): """ Gets a channel's low voltage :param board: (int) integer between 0 and 7 (assuming 8 boards) :param channel: (int) integer between 0 and 3 :return: (float) voltage in V from 0 to 10 """ # Only proceed if we can correctly set the current board and channel if self._set_board(board) + self._set_channel(channel): self.log.warn( f'Did not measure the voltage for board {board} channel {channel}' ) return float(-777) return bitval_to_value(int(self.device.query('l').split()[-1]), bits=16, min=0, max=10) def save(self): """ Saves current state of low/high for all channels to non-volatile memory :return: (int) 0 if successful """ # This fails randomly due to nondeterministic response, which we need to handle read = 0 while read < 10: self.device.write('S') try: self.device.read() read = 11 except VisaIOError: read += 1 except UnicodeDecodeError: read += 1 if read > 10: self.log.info('Saved current DIO breakout settings successfully') return 0 else: self.log.warn('Failed to save DIO breakout settings.' 'Connection may be corrupted.') return 1 def override(self, board, channel, state=True): """ Overrides HDAWG output :param board: (int) integer between 0 and 7 (assuming 8 boards) :param channel: (int) integer between 0 and 3 :param state: (bool) whether or not to force hi or lo :return: (int) 0 if successful """ if self._set_board(board) + self._set_channel(channel): self.log.warn(f'Did not override board {board} channel {channel}') return float(-777) self.device.write(f'F {1 if state else 0}') if int(self.device.query('b').rstrip()[-1]) != board: self.log.warn( f'Error in overriding board {board} channel {channel}') return float(-1) self.log.info(f'Board {board} channel {channel} in override mode') return 0 def disable_override(self, board, channel): """ Disables the override :param board: (int) integer between 0 and 7 (assuming 8 boards) :param channel: (int) integer between 0 and 3 :return: (int) 0 if successful """ if self._set_board(board) + self._set_channel(channel): self.log.warn( f'Did not disable override for board {board} channel {channel}' ) return float(-777) self.device.write('F -1') if int(self.device.query('b').rstrip()[-1]) != board: self.log.warn( f'Error in disabling override for board {board} channel {channel}' ) return float(-1) self.log.info( f'Board {board} channel {channel} override has been disabled') return 0 def close(self): """ Closes the connection to the device """ self.device.close() self.log.info(f'Closed connection to device at {self.addr}') # Technical methods (not to be exposed) def _set_board(self, board): """ Sets the current board (to update) :param board: (int) integer between 0 and 7 (assuming 8 boards) :return: (int) 0 if successful """ board = int(board) # Check within bounds if board < 0 or board > self.BOARDS - 1: self.log.warn( f'Board to set must be an integer between 0 and {self.BOARDS-1}' ) return 1 self.device.write(f'B {board}') # Check successful write (and clear the buffer) if int(self.device.query('b').rstrip()[-1]) != board: self.log.warn(f'Failed to set current board to {board}') return 2 return 0 def _set_channel(self, channel): """ Sets the current channel (to update) :param channel: (int) integer between 0 and 3 :return: (int) 0 if successful """ channel = int(channel) # Check within bounds if channel < 0 or channel > 3: self.log.warn(f'Channel to set must be an integer between 0 and 3') return 1 self.device.write(f'C {channel}') # Check successful write (and clear the buffer) if int(self.device.query('c').rstrip()[-1]) != channel: self.log.warn(f'Failed to set current channel to {channel}') return 2 return 0 def _set_high_voltage(self, voltage): """ Sets the current channel's high voltage :param voltage: (float) voltage in V between 0 and 10 (into open-loop) :return: (int) 0 if successful """ voltage = float(voltage) # Check within bounds if voltage < 0 or voltage > 10: self.log.warn(f'Can only set voltage between 0 and 10 V') return 1 bitval = value_to_bitval(voltage, bits=16, min=0, max=10) self.device.write(f'H {bitval}') # Check successful write (and clear the buffer) if int(self.device.query('h').split()[-1]) != bitval: self.log.warn(f'Failed to set high voltage to {voltage} V') return 2 return 0 def _set_low_voltage(self, voltage): """ Sets the current channel's low voltage :param voltage: (float) voltage in V between 0 and 10 (into open-loop) :return: (int) 0 if successful """ voltage = float(voltage) # Check within bounds if voltage < 0 or voltage > 10: self.log.warn(f'Can only set voltage between 0 and 10 V') return 1 bitval = value_to_bitval(voltage, bits=16, min=0, max=10) self.device.write(f'L {bitval}') # Check successful write (and clear the buffer) if int(self.device.query('l').split()[-1]) != bitval: self.log.warn(f'Failed to set low voltage to {voltage} V') return 2 return 0
class DataTaker: def __init__(self, logger=None, client_tuples=None, config=None, config_name=None): self.log = LogHandler(logger) self.dataset = None # Instantiate GUI window self.gui = Window(gui_template='data_taker', host=get_ip()) # Configure list of experiments self.gui.config.setText(config_name) self.config = config self.exp_path = self.config['exp_path'] if self.exp_path is None: self.exp_path = os.getcwd() sys.path.insert(1, self.exp_path) self.update_experiment_list() # Configure list of clients self.clients = {} # Retrieve Clients for client_entry in self.config['servers']: client_type = client_entry['type'] client_config = client_entry['config'] client = find_client(clients=client_tuples, settings=client_config, client_type=client_type, client_config=client_config, logger=self.log) self.clients[f"{client_type}_{client_config}"] = client for client_name, client_obj in self.clients.items(): client_item = QtWidgets.QListWidgetItem(client_name) client_item.setToolTip(str(client_obj)) self.gui.clients.addItem(client_item) # Configure dataset menu for name, obj in inspect.getmembers(datasets): if inspect.isclass(obj) and issubclass(obj, datasets.Dataset): self.gui.dataset.addItem(name) # Configure button clicks self.gui.configure.clicked.connect(self.configure) self.gui.run.clicked.connect(self.run) self.gui.save.clicked.connect(self.save) self.gui.load_config.clicked.connect(self.reload_config) self.gui.showMaximized() self.gui.apply_stylesheet() def update_experiment_list(self): """ Updates list of experiments """ self.gui.exp.clear() for filename in os.listdir(self.exp_path): if filename.endswith('.py'): self.gui.exp.addItem(filename[:-3]) self.gui.exp.itemClicked.connect(self.display_experiment) def display_experiment(self, item): """ Displays the currently clicked experiment in the text browser :param item: (QlistWidgetItem) with label of name of experiment to display """ with open(os.path.join(self.exp_path, f'{item.text()}.py'), 'r') as exp_file: exp_content = exp_file.read() self.gui.exp_preview.setText(exp_content) self.gui.exp_preview.setStyleSheet('font: 10pt "Consolas"; ' 'color: rgb(255, 255, 255); ' 'background-color: rgb(0, 0, 0);') self.log.update_metadata(experiment_file=exp_content) def configure(self): """ Configures the currently selected experiment + dataset """ # If the experiment is running, do nothing try: if self.experiment_thread.isRunning(): self.log.warn('Did not configure experiment, since it ' 'is still in progress') return except: pass # Load the config self.reload_config() # Set all experiments to normal state and highlight configured expt for item_no in range(self.gui.exp.count()): self.gui.exp.item(item_no).setBackground( QtGui.QBrush(QtGui.QColor('black'))) self.gui.exp.currentItem().setBackground( QtGui.QBrush(QtGui.QColor('darkRed'))) exp_name = self.gui.exp.currentItem().text() self.module = importlib.import_module(exp_name) self.module = importlib.reload(self.module) # Clear graph area and set up new or cleaned up dataset for index in reversed(range(self.gui.graph_layout.count())): try: self.gui.graph_layout.itemAt(index).widget().deleteLater() except AttributeError: try: self.gui.graph_layout.itemAt(index).layout().deleteLater() except AttributeError: pass self.gui.windows = {} # If we're not setting up a new measurement type, just clear the data self.dataset = getattr(datasets, self.gui.dataset.currentText())( gui=self.gui, log=self.log, config=self.config) # Run any pre-experiment configuration try: self.module.configure(dataset=self.dataset, **self.clients) except AttributeError: pass self.experiment = self.module.experiment self.log.info(f'Experiment {exp_name} configured') self.gui.exp_preview.setStyleSheet( 'font: 10pt "Consolas"; ' 'color: rgb(255, 255, 255); ' 'background-color: rgb(50, 50, 50);') def run(self): """ Runs/stops the experiment """ # Run experiment if self.gui.run.text() == 'Run': self.gui.run.setStyleSheet('background-color: red') self.gui.run.setText('Stop') self.log.info('Experiment started') # Run update thread self.update_thread = UpdateThread( autosave=self.gui.autosave.isChecked(), save_time=self.gui.autosave_interval.value()) self.update_thread.data_updated.connect(self.dataset.update) self.update_thread.save_flag.connect(self.save) self.gui.autosave.toggled.connect( self.update_thread.update_autosave) self.gui.autosave_interval.valueChanged.connect( self.update_thread.update_autosave_interval) self.experiment_thread = ExperimentThread(self.experiment, dataset=self.dataset, gui=self.gui, **self.clients) self.experiment_thread.status_flag.connect( self.dataset.interpret_status) self.experiment_thread.finished.connect(self.stop) self.log.update_metadata( exp_start_time=datetime.now().strftime('%d/%m/%Y %H:%M:%S:%f')) # Stop experiment else: self.experiment_thread.running = False def stop(self): """ Stops the experiment""" self.gui.run.setStyleSheet('background-color: green') self.gui.run.setText('Run') self.log.info('Experiment stopped') self.update_thread.running = False self.log.update_metadata( exp_stop_time=datetime.now().strftime('%d/%m/%Y %H:%M:%S:%f')) # Autosave if relevant if self.gui.autosave.isChecked(): self.save() def save(self): """ Saves data """ self.log.update_metadata(notes=self.gui.notes.toPlainText()) filename = self.gui.save_name.text() directory = self.config['save_path'] self.dataset.save(filename=filename, directory=directory, date_dir=True) save_metadata(self.log, filename, directory, True) self.log.info('Data saved') def reload_config(self): """ Loads a new config file """ self.config = load_script_config(script='data_taker', config=self.gui.config.text(), logger=self.log)
class Driver(MWSrcInterface): """Adapted from Qudi <https://github.com/Ulm-IQO/qudi/> """ def __init__(self, addr_str, logger=None): self.log = LogHandler(logger=logger) # Connect to the device self.rm = visa.ResourceManager() try: self._dev = self.rm.open_resource(addr_str) except Exception as exc_obj: self.log.exception( msg_str='Could not connect to the address >>{}<<. \n'.format( addr_str)) raise exc_obj # Log confirmation info message id_str = self._dev.query('*IDN?').replace(',', ' ') id_str = id_str.strip('\n') self.log.info(msg_str='{} initialised and connected.'.format(id_str)) # Reset device self.reset() # Error check self._er_chk() def reset(self): # Reset self._cmd_wait('*RST') # Clear status register self._cmd_wait('*CLS') return 0 def activate_interface(self): # Store hardware settings which are not controlled by logic, # to restore them after reset() # [logic does not know anything about this params, so it should not # introduce any changes to them by calling activate_interface()]. tmp_trig_dict = self.get_trig() # Reset device self.reset() # Restore hardware settings which are not controlled by logic # but were changed by self._dev.reset() self.set_trig(src_str=tmp_trig_dict['src_str'], slope_str=tmp_trig_dict['slope_str']) return 0 # Output control def on(self): if self.get_mode() == 'sweep': self.reset_swp_pos() return self._cmd_wait(cmd_str=':OUTP:STAT ON') def off(self): return self._cmd_wait(cmd_str=':OUTP:STAT OFF') def get_status(self): status = int(self._dev.query('OUTP:STAT?')) return status # Power def get_pwr(self): return float(self._dev.query(':POW?')) def set_pwr(self, pwr): self._cmd_wait(':POW {0:f}'.format(pwr)) return self.get_pwr() # Frequency def get_freq(self): mode = self.get_mode() if mode == 'cw': ret_val = float(self._dev.query(':FREQ?')) elif mode == 'sweep': start = float(self._dev.query(':FREQ:STAR?')) stop = float(self._dev.query(':FREQ:STOP?')) step = float(self._dev.query(':SWE:STEP?')) n_pts = int((stop - start) / step) + 2 ret_val = dict(start=start, stop=stop, n_pts=n_pts) else: raise MWSrcError('get_freq(): got unknown mode {}'.format(mode)) return ret_val def set_freq(self, freq): if self.get_status() == 1: self.off() # Activate CW mode self._cmd_wait(':FREQ:MODE CW') # Set CW frequency self._cmd_wait(':FREQ {0:f}'.format(freq)) return self.get_freq() def set_freq_swp(self, start, stop, n_pts): if self.get_status() == 1: self.off() # Set mode to Sweep self._cmd_wait(':FREQ:MODE SWEEP') # Set frequency sweep step = (stop - start) / (n_pts - 1) self._cmd_wait(':SWE:MODE STEP') self._cmd_wait(':SWE:SPAC LIN') self._cmd_wait(':FREQ:START {0:f}'.format(start)) self._cmd_wait(':FREQ:STOP {0:f}'.format(stop)) self._cmd_wait(':SWE:STEP:LIN {0:f}'.format(step)) return self.get_freq() def reset_swp_pos(self): """Reset of MW sweep mode position to start (start frequency) @return int: error code (0:OK, -1:error) """ self._cmd_wait(':ABOR:SWE') return 0 def get_mode(self): mode_str = self._dev.query(':FREQ:MODE?').strip('\n').lower() if 'cw' in mode_str: return 'cw' elif 'swe' in mode_str: return 'sweep' else: msg_str = 'get_mode(): unknown mode string {} was returned' self.log.error(msg_str=msg_str) raise MWSrcError(msg_str) # Technical methods def _cmd_wait(self, cmd_str): """Writes the command in command_str via resource manager and waits until the device has finished processing it. @param cmd_str: The command to be written """ self._dev.write(cmd_str) # Block command queue until cmd_str execution is complete self._dev.write('*WAI') # Block Python process until cmd_str execution is complete self._dev.query('*OPC?') # Error check self._er_chk() return 0 def _er_chk(self): # Block command queue until all previous commands are complete self._dev.write('*WAI') # Block Python process until all previous commands are complete self._dev.query('*OPC?') # Read all messages out_str = self._dev.query(':SYSTem:ERRor:ALL?') out_str += ',' out_str += self._dev.query('SYST:SERR?') out_str = out_str.replace('\n', '') out_str = out_str.replace('\r', '') out_str = out_str.replace('"', '') out_list = out_str.split(',') # Collect all warns and errors er_list = [] warn_list = [] msg_n = int(len(out_list) / 2) for idx in range(msg_n): msg_code = int(out_list[2 * idx]) if msg_code == 0: # No error continue elif msg_code > 0: # Warning warn_list.append(out_list[2 * idx + 1]) else: # Error er_list.append(out_list[2 * idx + 1]) # Construct Warn message string if len(warn_list) > 0: warn_str = '' for warn in warn_list: warn_str += (warn + ' \n') warn_str = warn_str.rstrip('\n') self.log.warn(msg_str=warn_str) # Construct Error message string if len(er_list) > 0: er_str = '' for er in er_list: er_str += (er + ' \n') er_str = er_str.rstrip('\n') self.log.error(msg_str=er_str) raise MWSrcError(er_str) return 0 def get_trig(self): # # Get trigger source # src_str = self._dev.query('TRIG:FSW:SOUR?') if 'EXT' in src_str: src_str = 'ext' elif 'AUTO' in src_str: src_str = 'int' else: msg_str = 'get_trig(): unknown trigger source was returned "{}" \n' \ ''.format(src_str) self.log.error(msg_str=msg_str) raise MWSrcError(msg_str) # # Get edge slope # slope_str = self._dev.query(':TRIG1:SLOP?') if 'POS' in slope_str: slope_str = 'r' elif 'NEG' in slope_str: slope_str = 'f' else: msg_str = 'get_trig(): unknown slope was returned "{}" \n' \ ''.format(slope_str) self.log.error(msg_str=msg_str) raise MWSrcError(msg_str) return dict(src_str=src_str, slope_str=slope_str) def set_trig(self, src_str='ext', slope_str='r'): if self.get_status() == 1: self.off() # # Set trigger source # if src_str == 'ext': src_str = 'EXT' elif src_str == 'int': src_str = 'AUTO' else: msg_str = 'set_trig(): unknown trigger source "{}" \n' \ 'Valid values are "ext" - external, "int" - internal' \ ''.format(src_str) self.log.error(msg_str=msg_str) raise MWSrcError(msg_str) self._cmd_wait('TRIG:FSW:SOUR {}'.format(src_str)) # # Set trigger edge # if slope_str == 'r': edge = 'POS' elif slope_str == 'f': edge = 'NEG' else: msg_str = 'set_trig(): invalid argument slope_str={} \n' \ 'Valid values are: "r" - raising, "f" - falling' \ ''.format(slope_str) self.log.error(msg_str=msg_str) raise ValueError(msg_str) self._cmd_wait(':TRIG1:SLOP {0}'.format(edge)) return self.get_trig() def force_trig(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. """ self._cmd_wait('*TRG') return 0
class Driver(): def reset(self): """ Create factory reset""" self.device.write('*RST') self.log.info("Reset to factory settings successfull.") def __init__(self, gpib_address, logger): """Instantiate driver class. :gpib_address: GPIB-address of the device, e.g. 'COM8' Can ba found in the Windows device manager. :logger: And instance of a LogClient. """ # Instantiate log. self.log = LogHandler(logger=logger) self.rm = ResourceManager() try: self.device = self.rm.open_resource(gpib_address) self.device_id = self.device.query('*IDN?') self.log.info(f"Successfully connected to {self.device_id}.") except VisaIOError: self.log.error(f"Connection to {gpib_address} failed.") # Reset to factory settings. self.reset() # Read and store min and max power. self.power_min, self.power_max = [ float( self.device.query(f'pow? {string}') ) for string in ['min', 'max'] ] # Read and store min and max frequency. self.freq_min, self.freq_max = [ float( self.device.query(f'freq? {string}') ) for string in ['min', 'max'] ] def output_on(self): """ Turn output on.""" self.device.write('OUTPut ON') self.log.info(f"Output of {self.device_id} turned on.") def output_off(self): """ Turn output off.""" self.device.write('OUTP OFF') self.log.info(f"Output of {self.device_id} turned off.") def check_power_out_of_range(self): """ Returns True if current power is outside of calibration range, False otherwise""" if not self.is_output_on(): warn = "Please enable output to check if calibration out of range" self.log.warn(warn) return warn # Query status register and do bitwise AND to check status of bit 3 if int(self.device.query('Status:Questionable:Condition?')) & 8: power_out_of_range = True else: power_out_of_range = False return power_out_of_range def is_output_on(self): """Returns True if output is enabled, False otherwise.""" return bool(int(self.device.query('OUTPut?'))) def set_freq(self, freq): """ Set frequency (in Hz) :freq: Target frequency in Hz """ if not self.freq_min <= freq <= self.freq_max: self.log.error( f"Frequency must be between {self.freq_min} Hz and {self.freq_max} Hz" ) self.device.write(f'freq {freq}') self.log.info(f"Frequency of {self.device_id} set to {freq} Hz.") def get_freq(self): """Returns current frequency setting.""" return float(self.device.query('freq?')) def set_power(self, power): """Set output power (in dBm) :power: Target power in dBm """ if not self.power_min <= power <= self.power_max: self.log.error( f"Power must be between {self.power_min} dBm and {self.power_max} dBm" ) self.device.write(f'pow {power}') self.log.info(f"Output power of {self.device_id} set to {power} dBm.") def get_power(self): """Returns current output power setting.""" return float(self.device.query('pow?'))