def __init__(self, config, **kwargs): super().__init__(config=config, **kwargs) if 'pulsed_file_dir' in config.keys(): self.pulsed_file_dir = config['pulsed_file_dir'] if not os.path.exists(self.pulsed_file_dir): homedir = get_home_dir() self.pulsed_file_dir = os.path.join(homedir, 'pulsed_files') self.log.warning( 'The directory defined in parameter ' '"pulsed_file_dir" in the config for ' 'PulseStreamer does not exist!\n' 'The default home directory\n{0}\n will be taken ' 'instead.'.format(self.pulsed_file_dir)) else: homedir = get_home_dir() self.pulsed_file_dir = os.path.join(homedir, 'pulsed_files') self.log.warning( 'No parameter "pulsed_file_dir" was specified in the config for ' 'PulseStreamer as directory for the pulsed files!\nThe default home ' 'directory\n{0}\nwill be taken instead.'.format( self.pulsed_file_dir)) self.host_waveform_directory = self._get_dir_for_name( 'sampled_hardware_files') self.current_status = -1 self.sample_rate = 1e9 self.current_loaded_asset = None self._channel = grpc.insecure_channel(self._pulsestreamer_ip + ':50051')
def __init__(self, config, **kwargs): super().__init__(config=config, **kwargs) if 'pulsed_file_dir' in config.keys(): self.pulsed_file_dir = config['pulsed_file_dir'] if not os.path.exists(self.pulsed_file_dir): homedir = get_home_dir() self.pulsed_file_dir = os.path.join(homedir, 'pulsed_files') self.log.warning('The directory defined in parameter ' '"pulsed_file_dir" in the config for ' 'PulseStreamer does not exist!\n' 'The default home directory\n{0}\n will be taken ' 'instead.'.format(self.pulsed_file_dir)) else: homedir = get_home_dir() self.pulsed_file_dir = os.path.join(homedir, 'pulsed_files') self.log.warning('No parameter "pulsed_file_dir" was specified in the config for ' 'PulseStreamer as directory for the pulsed files!\nThe default home ' 'directory\n{0}\nwill be taken instead.'.format(self.pulsed_file_dir)) self.host_waveform_directory = self._get_dir_for_name('sampled_hardware_files') self.current_status = -1 self.sample_rate = 1e9 self.current_loaded_asset = None self._channel = grpc.insecure_channel(self._pulsestreamer_ip + ':50051')
def __init__(self, config, **kwargs): super().__init__(config=config, **kwargs) if 'pulsed_file_dir' in config.keys(): self.pulsed_file_dir = config['pulsed_file_dir'] if not os.path.exists(self.pulsed_file_dir): homedir = get_home_dir() self.pulsed_file_dir = os.path.join(homedir, 'pulsed_files') self.log.warning('The directory defined in parameter ' '"pulsed_file_dir" in the config for ' 'SequenceGeneratorLogic class does not exist!\n' 'The default home directory\n{0}\n will be taken ' 'instead.'.format(self.pulsed_file_dir)) else: homedir = get_home_dir() self.pulsed_file_dir = os.path.join(homedir, 'pulsed_files') self.log.warning('No parameter "pulsed_file_dir" was specified in the config for ' 'OkFpgaPulser as directory for the pulsed files!\nThe default home ' 'directory\n{0}\nwill be taken instead.'.format(self.pulsed_file_dir)) self.log.warning( 'No parameter "fpga_type" specified in the config!\n' 'Possible types are "XEM6310_LX150" or "XEM6310_LX45".\n' 'Taking the type "{0}" as default.'.format(self._fpga_type)) self.host_waveform_directory = self._get_dir_for_name('sampled_hardware_files') self.current_status = -1 self.sample_rate = 950e6 self.current_loaded_asset = ''
def on_activate(self): """ Activate module """ config = self.getConfiguration() if 'pulsed_file_dir' in config.keys(): self.pulsed_file_dir = config['pulsed_file_dir'] if not os.path.exists(self.pulsed_file_dir): homedir = get_home_dir() self.pulsed_file_dir = os.path.join(homedir, 'pulsed_files') self.log.warning( 'The directory defined in parameter "pulsed_file_dir" in the config for ' 'SequenceGeneratorLogic class does not exist!\nThe default home directory\n' '{0}\n will be taken instead.'.format( self.pulsed_file_dir)) else: homedir = get_home_dir() self.pulsed_file_dir = os.path.join(homedir, 'pulsed_files') self.log.warning( 'No parameter "pulsed_file_dir" was specified in the config for NIPulser ' 'as directory for the pulsed files!\nThe default home directory\n{0}\n' 'will be taken instead.'.format(self.pulsed_file_dir)) self.host_waveform_directory = self._get_dir_for_name( 'sampled_hardware_files') self.pulser_task = daq.TaskHandle() daq.DAQmxCreateTask('NI Pulser', daq.byref(self.pulser_task)) self.current_status = -1 self.current_loaded_asset = None self.init_constraints() # analog voltage self.min_volts = -10 self.max_volts = 10 self.sample_rate = 1000 self.a_names = [] self.d_names = [] self.set_active_channels({ k: True for k in self.constraints['activation_config']['analog_only'] })
def on_activate(self): """ Activate module """ config = self.getConfiguration() if 'pulsed_file_dir' in config.keys(): self.pulsed_file_dir = config['pulsed_file_dir'] if not os.path.exists(self.pulsed_file_dir): homedir = get_home_dir() self.pulsed_file_dir = os.path.join(homedir, 'pulsed_files') self.log.warning( 'The directory defined in parameter "pulsed_file_dir" in the config for ' 'SequenceGeneratorLogic class does not exist!\nThe default home directory\n' '{0}\n will be taken instead.'.format(self.pulsed_file_dir)) else: homedir = get_home_dir() self.pulsed_file_dir = os.path.join(homedir, 'pulsed_files') self.log.warning( 'No parameter "pulsed_file_dir" was specified in the config for NIPulser ' 'as directory for the pulsed files!\nThe default home directory\n{0}\n' 'will be taken instead.'.format(self.pulsed_file_dir)) self.host_waveform_directory = self._get_dir_for_name('sampled_hardware_files') self.pulser_task = daq.TaskHandle() daq.DAQmxCreateTask('NI Pulser', daq.byref(self.pulser_task)) self.current_status = -1 self.current_loaded_asset = None self.init_constraints() # analog voltage self.min_volts = -10 self.max_volts = 10 self.sample_rate = 1000 self.a_names = [] self.d_names = [] self.set_active_channels({ k: True for k in self.constraints['activation_config']['analog_only']})
class AWG663(Base, PulserInterface): """ A hardware module for the Spectrum AWG for generating waveforms and sequences thereof. """ _modclass = 'awg663' _modtype = 'hardware' # config options awg_ip_address = ConfigOption(name='awg_ip_address', missing='error') waveform_folder = ConfigOption(name="waveform_folder", default=os.path.join(get_home_dir(), 'saved_pulsed_assets', 'waveform'), missing="warn") sequence_folder = ConfigOption(name="sequence_folder", default=os.path.join(get_home_dir(), 'saved_pulsed_assets', 'sequence'), missing="warn") def __init__(self, config, **kwargs): super().__init__(config=config, **kwargs) self.cards = [0, 1] self.hub = 0 self.channel_setup = [ { 'name': 'mw', 'ch': 0, 'type': 'sin', 'amp': 1.0, 'phase': 0.0, 'freq': 30e6, 'freq2': 0.0 }, { 'name': 'mwy', 'ch': 1, 'type': 'sin', 'amp': 1.0, 'phase': np.pi / 2., 'freq': 30e6, 'freq2': 0.0 }, { 'name': 'rf', 'ch': 2, 'type': 'DC', 'amp': 1.0, 'phase': 0.0, # np.pi/2., 'freq': 0.0, # 'rf_freq' 'freq2': 0.0 }, { 'name': 'mw3', 'ch': 3, 'type': 'DC', 'amp': 1.0, 'phase': 0.0, 'freq': 0.0, 'freq2': 0.0 }, { 'name': 'laser', 'ch': 4, 'type': 'DC', 'amp': 1.0, 'phase': 0.0, 'freq': 0.0, 'freq2': 0.0 }, { 'name': 'aom', 'ch': 5, 'type': 'DC', 'amp': 1.0, 'phase': 0.0, 'freq': 0.0, 'freq2': 0.0 }, { 'name': 'trig', 'ch': 6, 'type': 'DC', 'amp': 1.0, 'phase': 0.0, 'freq': 0.0, 'freq2': 0.0 }, { 'name': 'mw_gate', 'ch': 7, 'type': 'DC', 'amp': 1.0, 'phase': 0.0, 'freq': 0.0, 'freq2': 0.0 } ] # [card, channel, binary channel for later use] self.channels = [[0, 0, 0b1], [0, 1, 0b10], [1, 0, 0b100], [1, 1, 0b1000]] self.loaded_assets = {} self.CurrentUpload = os.path.join(os.getcwd(), 'hardware', 'awg', 'CurrentUpload.pkl') self.typeloaded = None def on_activate(self): try: self.instance = SpectrumAWG35.AWG(self.awg_ip_address, self.cards, self.hub, self.channel_setup) except ValueError: self.log.error("Unable to connect to Spectrum AWG. It may be locked by another program.") return -1 self.instance.init_all_channels() # Set the amplitude of a channel in mV (into 50 Ohms) self.instance.cards[0].set_amplitude(0, 500) self.instance.cards[0].set_amplitude(1, 500) self.instance.cards[1].set_amplitude(0, 100) self.instance.cards[1].set_amplitude(1, 100) active_chan = self.get_constraints().activation_config['hira_config'] self.loaded_assets = dict.fromkeys(active_chan) def on_deactivate(self): """ Method called when module is deactivated. If not overridden this method returns an error. """ self.instance.reset() self.instance.stop() self.instance.close() del self.instance @staticmethod def __mV_to_V(voltage_in_mV): return voltage_in_mV / 1e3 @staticmethod def __V_to_mV(voltage_in_V): return voltage_in_V * 1e3 def get_constraints(self): """ Retrieve the hardware constrains from the Pulsing device. @return constraints object: object with pulser constraints as attributes. """ constraints = PulserConstraints() # sample rate, i.e. the time base of the pulser constraints.sample_rate.min = 50e6 constraints.sample_rate.max = 1.25e9 constraints.sample_rate.step = 1e6 constraints.sample_rate.default = 1.25e9 # The peak-to-peak amplitude and voltage offset of the analog channels # This should be in Volts for qudi constraints.a_ch_amplitude.min = self.__mV_to_V(80) constraints.a_ch_amplitude.max = self.__mV_to_V(2000) constraints.a_ch_amplitude.step = self.__mV_to_V(1) constraints.a_ch_amplitude.default = self.__mV_to_V(500) constraints.a_ch_offset.min = 0 constraints.a_ch_offset.max = 0 constraints.a_ch_offset.default = 0 # length of the created waveform in samples constraints.waveform_length.step = 1 constraints.waveform_length.default = 1 # Low and high voltage level of the digital channels constraints.d_ch_low.default = 0.0 constraints.d_ch_low.min = 0.0 constraints.d_ch_low.max = 0.0 constraints.d_ch_high.default = 3.3 constraints.d_ch_high.min = 3.3 constraints.d_ch_high.max = 3.3 activation_config = OrderedDict() activation_config['hira_config'] = {'a_ch0', 'a_ch1', 'd_ch0', 'd_ch1', 'd_ch2', 'a_ch2', 'a_ch3', 'd_ch3', 'd_ch4', 'd_ch5'} constraints.activation_config = activation_config return constraints def get_limits(self): limits = MicrowaveLimits() limits.supported_modes = (MicrowaveMode.SWEEP) limits.min_frequency = 1 limits.max_frequency = 400e6 limits.min_power = 100 limits.max_power = 2000 limits.sweep_minstep = 1 limits.sweep_maxstep = 400e6 return limits def pulser_on(self): """ Switches the pulsing device on. @return int: error code (0:OK, -1:error) """ ERR = self.instance.start() return ERR def pulser_off(self): """ Switches the pulsing device off. @return int: error code (0:OK, -1:error) """ ERR = self.instance.stop() return ERR def prepare_ch(self, name, type=None, amp=None, phase=None, freq=None, freq2=None, reset_phase=True): chan = self.instance.ch(name, type, amp, phase, freq, freq2, reset_phase) return chan def load_waveform(self, load_dict): """ Loads a waveform from the waveform folder on disk to all specified channels of the pulsing device. The file on disk has to have the corresponding channel name in the file name. eg. hahn_echo_a_ch0.pkl hahn_echo_d_ch1.pkl will be loaded into analog ch0 and digital ch1 respectively. @param dict|list load_dict: a dictionary with keys being one of the available channel index and values being the name of the already written waveform to load into the channel. Examples: {1: rabi_ch1, 2: rabi_ch2} or {1: rabi_ch2, 2: rabi_ch1} If just a list of waveform names if given, the channel association will be invoked from the channel suffix '_ch1', '_ch2' etc. {1: rabi_ch1, 2: rabi_ch2} or {1: rabi_ch2, 2: rabi_ch1} If just a list of waveform names if given, the channel association will be invoked from the channel suffix '_ch1', '_ch2' etc. A possible configuration can be e.g. ['rabi_ch1', 'rabi_ch2', 'rabi_ch3'] @return dict: Dictionary containing the actually loaded waveforms per channel. For devices that have a workspace (i.e. AWG) this will load the waveform from the device workspace into the channel. For a device without mass memory, this will make the waveform/pattern that has been previously written with self.write_waveform ready to play. Please note that the channel index used here is not to be confused with the number suffix in the generic channel descriptors (i.e. 'd_ch1', 'a_ch1'). The channel index used here is highly hardware specific and corresponds to a collection of digital and analog channels being associated to a SINGLE waveform asset. """ # create new dictionary with keys = num_of_ch and item = waveform max_chunk_size = self.instance.max_chunk_size empty_chunk_array_factor = self.instance.empty_chunk_array_factor if isinstance(load_dict, list): new_dict = dict() self.log.info('Using waveform set "{}" from "{}"'.format(load_dict[0], self.waveform_folder)) for waveform in load_dict: wave_name = waveform.rsplit('.pkl')[0] channel_num = int(wave_name.rsplit('_ch', 1)[1]) # Map channel numbers to HW channel numbers if '_a_ch' not in waveform: channel = channel_num + 4 else: channel = channel_num new_dict[channel] = wave_name load_dict = new_dict if not load_dict: self.log.error('No data to send to AWG') return -1 # Get a list of all pkl waveforms in waveform folder path = self.waveform_folder wave_form_files = self.get_waveform_names() # TODO: get_waveform_names() already removes .pkl extension? wave_form_list = [file.rsplit('.pkl')[0] for file in wave_form_files] data_list = list() # this looks like 4 analog channels and 6 digital for i in range(4): data_list.append(np.zeros(int(max_chunk_size * empty_chunk_array_factor), np.int16)) for i in range(6): data_list.append(np.zeros(int(max_chunk_size * empty_chunk_array_factor), np.bool)) for ch, value in load_dict.items(): if value in wave_form_list: wavefile = '{0}.pkl'.format(value) filepath = os.path.join(path, wavefile) data = self.my_load_dict(filepath) data_list[ch][0:len(data)] = data data_size = len(data) if '_a_ch' in value: chan_name = 'a_ch{0}'.format(value.rsplit('a_ch')[1]) self.loaded_assets[chan_name] = value else: chan_name = 'd_ch{0}'.format(value.rsplit('d_ch')[1]) self.loaded_assets[chan_name] = value else: self.log.warn('Waveform {} not found in {}'.format(value, self.waveform_folder)) data_size = 0 # If data sizes don't match the first file, append empty rows # TODO: This will only work for the last size loaded as data_size is a variable, not a list? new_list = list() if data_size < len(data_list[0]): for row in data_list: new_row = row[0:data_size] new_list.append(new_row) data_list = new_list # See pg 80 in manual count = 0 while not data_size % 32 == 0: data_size += 1 count += 1 if not count == 1: extra = np.zeros(count, np.int16) new_list = list() for row in data_list: new_row = np.concatenate((row, extra), axis=0) new_list.append(new_row) data_list = new_list self.instance.set_memory_size(int(data_size)) # Runs a threaded method to upload to both cards simultaneously # data_list is a list of analog and digital values # data_list[0, 1, 4, 5, 6] goes to card 0 # data_list[2, 3, 7, 8, 9] goes to card 1 self.log.info('Uploading waveform to AWG...') if not data_size == 0: self.instance.upload(data_list, data_size, mem_offset=0) self.typeloaded = 'waveform' # print(data_list[0][0:5]) self.log.info('Upload to AWG complete') return load_dict def load_sequence(self, sequence_name): """ TODO: Does not appear to work as SequenceFile is left upmapped Loads a sequence to the channels of the device in order to be ready for playback. For devices that have a workspace (i.e. AWG) this will load the sequence from the device workspace into the channels. For a device without mass memory this will make the waveform/pattern that has been previously written with self.write_waveform ready to play. @param dict|list sequence_name: a dictionary with keys being one of the available channel index and values being the name of the already written waveform to load into the channel. Examples: {1: rabi_ch1, 2: rabi_ch2} or {1: rabi_ch2, 2: rabi_ch1} If just a list of waveform names if given, the channel association will be invoked from the channel suffix '_ch1', '_ch2' etc. @return dict: Dictionary containing the actually loaded waveforms per channel. """ # create new dictionary with keys = num_of_ch and item = waveform max_chunk_size = self.instance.max_chunk_size empty_chunk_array_factor = self.instance.empty_chunk_array_factor if isinstance(sequence_name, list): new_dict = dict() for waveform in sequence_name: channel = int(waveform.rsplit('_ch', 1)[1]) new_dict[channel] = waveform load_dict = new_dict # load possible sequences path = self.SequenceFile wave_form_dict = self.my_load_dict(path) # with open(path,'w') as json_file: # wave_form_dict = json.load(json_file) # dict_path = os.path.join('awg', 'SequenceDict.pkl') # pkl_file = open(dict_path, 'rb') # wave_form_dict = pickle.load(pkl_file) # pkl_file.close() data_list = list() # this looks like 4 analog channels and 6 digital for i in range(4): data_list.append(np.zeros(int(max_chunk_size * empty_chunk_array_factor), np.int16)) for i in range(6): data_list.append(np.zeros(int(max_chunk_size * empty_chunk_array_factor), np.bool)) # key_list = list() # for key in wave_form_dict.keys(): # key_list.append(key.rsplit('_a',1)[0]) # find the given sequence in the dictionary and load to the wanted channel for chan, wave_name in load_dict.items(): wave_form = wave_form_dict.get(wave_name) if wave_form is not None: # prepare_ch(name=) # self.instance.upload_wave_from_list(wave_form) data = np.asarray(wave_form * (2 ** 15 - 1), dtype=np.int16) data_list[chan][0:len(wave_form)] = data # plt.plot(data) # plt.show() data_size = len(wave_form) else: self.log.error(wave_name + ' not in dictionary') self.instance.upload(data_list, data_size, mem_offset=0) return load_dict def get_loaded_assets(self): """ Retrieve the currently loaded asset names for each active channel of the device. The returned dictionary will have the channel numbers as keys. In case of loaded waveforms the dictionary values will be the waveform names. In case of a loaded sequence the values will be the sequence name appended by a suffix representing the track loaded to the respective channel (i.e. '<sequence_name>_1'). @return (dict, str): Dictionary with keys being the channel number and values being the respective asset loaded into the channel, string describing the asset type ('waveform' or 'sequence') """ self.log.info("Assets have been retrieved from memory, not the AWG.") return self.loaded_assets, self.typeloaded def clear_all(self): """ Clears all loaded waveforms from the pulse generators RAM/workspace. @return int: error code (0:OK, -1:error) """ self.instance.reset() def get_status(self): """ Retrieves the status of the pulsing hardware @return (int, dict): tuple with an integer value of the current status and a corresponding dictionary containing status description for all the possible status variables of the pulse generator hardware. """ state_card1 = self.instance.cards[0].get_state() state_card2 = self.instance.cards[1].get_state() if state_card1 == "Ready" and state_card2 == "Ready": num = 0 else: num = -1 status_dic = {-1: 'no communication with device', 0: 'device is active and ready'} self.status_dic = status_dic return num, status_dic def get_sample_rate(self): """ Get the sample rate of the pulse generator hardware @return float: The current sample rate of the device (in Hz) Do not return a saved sample rate from an attribute, but instead retrieve the current sample rate directly from the device. """ rate = self.instance.get_samplerate() return float(rate) def set_sample_rate(self, sample_rate): """ Set the sample rate of the pulse generator hardware. @param float sample_rate: The sampling rate to be set (in Hz) @return float: the sample rate returned from the device (in Hz). Note: After setting the sampling rate of the device, use the actually set return value for further processing. """ # Integer conversion required by AWG self.instance.set_samplerate(int(sample_rate)) actual_rate = self.get_sample_rate() return actual_rate def get_analog_level(self, amplitude=None, offset=None): """ Retrieve the analog amplitude and offset of the provided channels. @param list amplitude: optional, if the amplitude value (in Volt peak to peak, i.e. the full amplitude) of a specific channel is desired. @param list offset: optional, if the offset value (in Volt) of a specific channel is desired. @return: (dict, dict): tuple of two dicts, with keys being the channel descriptor string (i.e. 'a_ch1') and items being the values for those channels. Amplitude is always denoted in Volt-peak-to-peak and Offset in volts. Note: Do not return a saved amplitude and/or offset value but instead retrieve the current amplitude and/or offset directly from the device. If nothing (or None) is passed then the levels of all channels will be returned. If no analog channels are present in the device, return just empty dicts. Example of a possible input: amplitude = ['a_ch1', 'a_ch4'], offset = None to obtain the amplitude of channel 1 and 4 and the offset of all channels {'a_ch1': -0.5, 'a_ch4': 2.0} {'a_ch1': 0.0, 'a_ch2': 0.0, 'a_ch3': 1.0, 'a_ch4': 0.0} ls for each channel Card[id].get_filter(ch_num) Card[id].get_amplitude(ch_num) """ channels = self.get_constraints().activation_config['hira_config'] a_ch = [ch for ch in channels if 'a' in ch] AllAmp = dict() if amplitude is not None: for chan in amplitude: channel = int(chan.rsplit('_ch', 1)[1]) chInd = self.channels[channel] state = self.instance.cards[chInd[0]].get_amplitude(chInd[1]) AllAmp[chan] = self.__mV_to_V(state) else: for channel in range(4): chInd = self.channels[channel] state = self.instance.cards[chInd[0]].get_amplitude(chInd[1]) chan = "a_ch{0}".format(channel) AllAmp[chan] = self.__mV_to_V(state) AllOffset = dict() if offset is not None: for chan in offset: channel = int(chan.rsplit('_ch', 1)[1]) chInd = self.channels[channel] state = self.instance.cards[chInd[0]].get_offset(chInd[1]) AllOffset[chan] = self.__mV_to_V(state) else: for ch in a_ch: ch_num = int(ch.rsplit('_ch')[1]) chInd = self.channels[ch_num] state = self.instance.cards[chInd[0]].get_offset(chInd[1]) AllOffset[ch] = self.__mV_to_V(state) return AllAmp, AllOffset def set_analog_level(self, amplitude=None, offset=None): """ Set amplitude and/or offset value of the provided analog channel(s). @param dict amplitude: dictionary, with key being the channel descriptor string (i.e. 'a_ch1', 'a_ch2') and items being the amplitude values (in Volt peak to peak, i.e. the full amplitude) for the desired channel. @param dict offset: dictionary, with key being the channel descriptor string (i.e. 'a_ch1', 'a_ch2') and items being the offset values (in absolute volt) for the desired channel. @return (dict, dict): tuple of two dicts with the actual set values for amplitude and offset for ALL channels. If nothing is passed then the command will return the current amplitudes/offsets. Note: After setting the amplitude and/or offset values of the device, use the actual set return values for further processing. """ if amplitude is not None: for chan in amplitude: channel = int(chan.rsplit('_ch', 1)[1]) chInd = self.channels[channel] amp = self.__V_to_mV(amplitude[chan]) state = self.instance.cards[chInd[0]].set_amplitude(chInd[1], int(amp)) if offset is not None: for chan in offset: channel = int(chan.rsplit('_ch', 1)[1]) chInd = self.channels[channel] off = self.__V_to_mV(offset[chan]) state = self.instance.cards[chInd[0]].set_offset(chInd[1], int(off)) AllAmp, AllOffset = self.get_analog_level() return AllAmp, AllOffset def get_digital_level(self, low=None, high=None): """ Retrieve the digital low and high level of the provided/all channels. @param list low: optional, if the low value (in Volt) of a specific channel is desired. @param list high: optional, if the high value (in Volt) of a specific channel is desired. @return: (dict, dict): tuple of two dicts, with keys being the channel descriptor strings (i.e. 'd_ch1', 'd_ch2') and items being the values for those channels. Both low and high value of a channel is denoted in volts. Note: Do not return a saved low and/or high value but instead retrieve the current low and/or high value directly from the device. If nothing (or None) is passed then the levels of all channels are being returned. If no digital channels are present, return just an empty dict. Example of a possible input: low = ['d_ch1', 'd_ch4'] to obtain the low voltage values of digital channel 1 an 4. A possible answer might be {'d_ch1': -0.5, 'd_ch4': 2.0} {'d_ch1': 1.0, 'd_ch2': 1.0, 'd_ch3': 1.0, 'd_ch4': 4.0} Since no high request was performed, the high values for ALL channels are returned (here 4). """ channels = self.get_constraints().activation_config['hira_config'] d_ch = [ch for ch in channels if 'd' in ch] if low is None: low = [] if high is None: high = [] low_val = {} high_val = {} if (low == []) and (high == []): for ch in d_ch: low_val[ch] = 0.0 high_val[ch] = 3.3 for d_ch in low: low_val[d_ch] = 0 for d_ch in low: high_val[d_ch] = 1 return low_val, high_val def set_digital_level(self, low=None, high=None): """ Set low and/or high value of the provided digital channel. @param dict low: dictionary, with key being the channel descriptor string (i.e. 'd_ch1', 'd_ch2') and items being the low values (in volt) for the desired channel. @param dict high: dictionary, with key being the channel descriptor string (i.e. 'd_ch1', 'd_ch2') and items being the high values (in volt) for the desired channel. @return (dict, dict): tuple of two dicts where first dict denotes the current low value and the second dict the high value for ALL digital channels. Keys are the channel descriptor strings (i.e. 'd_ch1', 'd_ch2') If nothing is passed then the command will return the current voltage levels. Note: After setting the high and/or low values of the device, use the actual set return values for further processing. """ return self.get_digital_level() def get_active_channels(self, ch=None): """ Get the active channels of the pulse generator hardware. @param list ch: optional, if specific analog or digital channels are needed to be asked without obtaining all the channels. @return dict: where keys denoting the channel string and items boolean expressions whether channel are active or not. Example for an possible input (order is not important): ch = ['a_ch2', 'd_ch2', 'a_ch1', 'd_ch5', 'd_ch1'] then the output might look like {'a_ch2': True, 'd_ch2': False, 'a_ch1': False, 'd_ch5': True, 'd_ch1': False} If no parameter (or None) is passed to this method all channel states will be returned. DONT KNOW HOW TO CHECK IF DIGITAL IS ENABLED, JUST ASSUME IT IS SINCE THIS IS PART OF THE CARD INITIALIZATION """ all_state = dict() all_constraints = self.get_constraints() all_options = all_constraints.activation_config['hira_config'] # ['a_ch0', 'a_ch1', 'a_ch2', 'a_ch3', 'd_ch0', 'd_ch1', 'd_ch2', 'd_ch3', 'd_ch4', 'd_ch5'] count = 0 # # index = 0 # for card in self.instance.cards: # active = card.get_selected_channels() # state[index] = index & bin(active) # index =+1 for c in all_options: if c.startswith('a'): num = int(c.rsplit('ch')[1]) ind = self.channels[num] card = self.instance.cards[ind[0]] # state = card.get_channel_output(ind[1]) state = card.get_selected_channels() all_state[c] = bool(state) elif c.startswith('d'): all_state[c] = True # for c in self.instance.cards: # for chan in range(2): # state = c.get_channel_output(chan) # all_state[all_options[count]] = state # count +=1 if ch is not None: for chan in all_options: if chan is not ch: all_state.pop(chan, 'None') return all_state def set_active_channels(self, ch=None): """ Set the active/inactive channels for the pulse generator hardware. The state of ALL available analog and digital channels will be returned (True: active, False: inactive). The actually set and returned channel activation must be part of the available activation_configs in the constraints. You can also activate/deactivate subsets of available channels but the resulting activation_config must still be valid according to the constraints. If the resulting set of active channels can not be found in the available activation_configs, the channel states must remain unchanged. @param dict ch: dictionary with keys being the analog or digital string generic names for the channels (i.e. 'd_ch1', 'a_ch2') with items being a boolean value. True: Activate channel, False: Deactivate channel @return dict: with the actual set values for ALL active analog and digital channels If nothing is passed then the command will simply return the unchanged current state. Note: After setting the active channels of the device, use the returned dict for further processing. Example for possible input: ch={'a_ch2': True, 'd_ch1': False, 'd_ch3': True, 'd_ch4': True} to activate analog channel 2 digital channel 3 and 4 and to deactivate digital channel 1. All other available channels will remain unchanged. THE ANALOG CHANNELS ARE DONE, THE DIGITAL IS NOT CLEAR """ binary = 0b0 status = self.get_active_channels() status.update(ch) if ch is not None: for chan, value in status.items(): if chan.startswith('a'): num = int(chan.rsplit('ch')[1]) if value: binary = self.channels[num][2] | binary else: binary = 0b1111 self.instance.set_selected_channels(binary) status = self.get_active_channels() return status def write_waveform(self, name, analog_samples, digital_samples, is_first_chunk, is_last_chunk, total_number_of_samples): """ Write a new waveform or append samples to an already existing waveform on the device memory. The flags is_first_chunk and is_last_chunk can be used as indicator if a new waveform should be created or if the write process to a waveform should be terminated. NOTE: All sample arrays in analog_samples and digital_samples must be of equal length! @param str name: the name of the waveform to be created/append to @param dict analog_samples: keys are the generic analog channel names (i.e. 'a_ch1') and values are 1D numpy arrays of type float32 containing the voltage samples. @param dict digital_samples: keys are the generic digital channel names (i.e. 'd_ch1') and values are 1D numpy arrays of type bool containing the marker states. @param bool is_first_chunk: Flag indicating if it is the first chunk to write. If True this method will create a new empty waveform. If False the samples are appended to the existing waveform. @param bool is_last_chunk: Flag indicating if it is the last chunk to write. Some devices may need to know when to close the appending wfm. @param int total_number_of_samples: The number of sample points for the entire waveform (not only the currently written chunk) @return (int, list): Number of samples written (-1 indicates failed process) and list of created waveform names """ waveforms = list() # # # & is_last_chunk: # self.log.error('sample is either first of last, not both') # return -1, waveforms if len(analog_samples) == 0: self.log.error('No analog samples passed to write_waveform method') return -1, waveforms activation_dict = self.get_active_channels() active_channels = {chnl for chnl in activation_dict if activation_dict[chnl]} active_analog = sorted(chnl for chnl in active_channels if chnl.startswith('a')) # Sanity check of channel numbers # First need to undersatnd how to enable / disable digital channel # if active_channels != set(analog_samples.keys()).union(set(digital_samples.keys())): # self.log.error('Mismatch of channel activation and sample array dimensions for ' # 'waveform creation.\nChannel activation is: {0}\nSample arrays have: ' # ''.format(active_channels, # set(analog_samples.keys()).union(set(digital_samples.keys())))) # return -1, waveforms # file with all the waveforms saved # wave_dict = self.get_waveform_names() total_length = 0 # data is converted from float64 to int16 # if name not in wave_dict: for chan, value in analog_samples.items(): full_name = '{0}_{1}'.format(name, chan) wavename = '{0}.pkl'.format(full_name) path = os.path.join(self.waveform_folder, wavename) if not value.dtype == 'float64': convert = np.zeros(len(value), dtype='float64') ch_amp = self.get_analog_level(amplitude=[chan]) convert[0:len(value)] = value * ch_amp[0][chan] value = convert if is_first_chunk: full_signal = np.asarray(value * (2 ** 15 - 1), dtype=np.int16) # print(value[0:5]) else: old_part = self.my_load_dict(path) new_part = np.asarray(value * (2 ** 15 - 1), dtype=np.int16) full_signal = np.concatenate((old_part, new_part)) self.my_save_dict(full_signal, path) waveforms.append(full_name) total_length = len(full_signal) for chan, value in digital_samples.items(): # maybe need to convert to boolian full_name = '{0}_{1}'.format(name, chan) wavename = '{0}.pkl'.format(full_name) path = os.path.join(self.waveform_folder, wavename) self.my_save_dict(value, path) waveforms.append(full_name) total_length = len(full_signal) return total_length, waveforms def prepare_data_via_channel(self, channel, duration, sample_rate, start_position): # TODO: This function used to pass self as the first argument, for whatever reason data = self.instance.get_data_from_channel(channel, duration, sample_rate, start_position) return data def write_sequence(self, name, sequence_parameters): """ Write a new sequence on the device memory. @param str name: the name of the waveform to be created/append to @param dict sequence_parameters: dictionary containing the parameters for a sequence @return: int, number of sequence steps written (-1 indicates failed process) In our AWG we will have a dictionary with sequences, this is saved locally (not on the AWG) sequence parameters are the waveforms and lengths? repetitions? The specific channel is set at uploading """ sequencename = '{0}.pkl'.format(name) path = os.path.join(self.sequence_folder, sequencename) sequence_list = self.get_sequence_names() if name not in sequence_list: self.my_save_dict(sequence_parameters, path) count = len(sequence_parameters) # with open(path, 'w') as json_file: # json.dump(sequence_dict, json_file) # output = open(path, 'wb') # pickle.dump(sequence_dict, output) # output.close() return count def get_waveform_names(self): """ Retrieve the names of all uploaded waveforms on the device. @return list: List of all uploaded waveform name strings in the device workspace. """ path = self.waveform_folder names = [f.rsplit('.pkl')[0] for f in os.listdir(path) if f.endswith(".pkl")] return names def get_sequence_names(self): """ Retrieve the names of all uploaded sequence on the device. @return list: List of all uploaded sequence name strings in the device workspace. """ path = self.sequence_folder names = [f for f in os.listdir(path) if f.endswith(".pkl")] return names def delete_waveform(self, waveform_name): """ Delete the waveform with name "waveform_name" from the device memory. @param str waveform_name: The name of the waveform to be deleted Optionally a list of waveform names can be passed. @return list: a list of deleted waveform names. """ # TODO: Seems to only delete waveforms from the awg folder, not the device itself self.log.warn("Seems to only delete waveforms from the awg folder, not the device itself") path = os.path.join(os.getcwd(), 'awg', 'WaveFormDict.pkl') pkl_file = open(path, 'rb') wave_dict = pickle.load(pkl_file) pkl_file.close() names = list() for wave in waveform_name: if wave in wave_dict.keys(): wave_dict.pop(wave, "None") names.append = wave else: self.log.error("Unable to delete {}. Waveform not in list. ".format(wave)) path = os.path.join(os.getcwd(), 'awg', 'WaveFormDict.pkl') output = open(path, 'wb') pickle.dump(wave_dict, output) output.close() return names def delete_sequence(self, sequence_name): """ Delete the sequence with name "sequence_name" from the device memory. @param str sequence_name: The name of the sequence to be deleted Optionally a list of sequence names can be passed. @return list: a list of deleted sequence names. """ path = os.path.join(os.getcwd(), 'awg', 'SequenceDict.pkl') pkl_file = open(path, 'rb') wave_dict = pickle.load(pkl_file) pkl_file.close() names = list() for wave in sequence_name: if wave in wave_dict.keys(): wave_dict.pop(wave, "None") names.append = wave else: self.log.error("Unable to delete {}. Waveform not in list. ".format(wave)) output = open(path, 'wb') pickle.dump(wave_dict, output) output.close() return names def get_interleave(self): """ Check whether Interleave is ON or OFF in AWG. @return bool: True: ON, False: OFF Will always return False for pulse generator hardware without interleave. """ return False def set_interleave(self, state=False): """ Turns the interleave of an AWG on or off. @param bool state: The state the interleave should be set to (True: ON, False: OFF) @return bool: actual interleave status (True: ON, False: OFF) Note: After setting the interleave of the device, retrieve the interleave again and use that information for further processing. Unused for pulse generator hardware other than an AWG. """ return False def reset(self): """ Reset the device. @return int: error code (0:OK, -1:error) """ err = self.instance.reset() return err def has_sequence_mode(self): """ Asks the pulse generator whether sequence mode exists. @return: bool, True for yes, False for no. """ return True def my_load_dict(self, filename): """ Load a waveform from disk into memory. @param filename: Full path + name + extension of file to be loaded @return: loaded pkl file dict """ # if file doesn't exist it create a file with this name # TODO: Why use rb+ (both reading and writing in binary), looks like rb (read binary) will suffice? with open(filename, 'rb+') as file: my_dictionary = pickle.load(file) return my_dictionary def my_save_dict(self, my_dictionary, filename): """ save dictionary to pkl file, will overwrite existing data""" with open(filename, 'wb+') as file: pickle.dump(my_dictionary, file) def set_reps(self, reps): self.instance.set_loops(reps) ############# # FOR TESTING ############# def generate_sine(self): # For TESTING sample_rate = 1.25e9 # Sample set to default - 1.25 GSa/sec freq_duration = 2e-3 # Duration of each frequency set to 3 μsec average_factor = 100000 # How many sweeps will be conducted at each ODMR line scan samples_per_freq = int(sample_rate * freq_duration) # Number of samples required for each frequency x = np.linspace(start=0, stop=int(samples_per_freq) - 1, num=samples_per_freq) analog_samples = {'a_ch0': [], 'a_ch1': []} digital_samples = {} freq = 100e6 norm_freq = freq / sample_rate analog_samples['a_ch0'] = np.append(analog_samples['a_ch0'], np.sin(2 * np.pi * norm_freq * x)) freq = 200e6 norm_freq = freq / sample_rate analog_samples['a_ch1'] = np.append(analog_samples['a_ch1'], np.sin(2 * np.pi * norm_freq * x)) self.set_analog_level(amplitude={'a_ch0': 500, 'a_ch1': 500}) num_of_samples, waveform_names = self.write_waveform(name='testing_sine', analog_samples=analog_samples, digital_samples=digital_samples, is_first_chunk=True, is_last_chunk=True, total_number_of_samples=len(analog_samples['a_ch0'])) self.load_waveform(load_dict=waveform_names) self.set_reps(average_factor) def testing_generate_sine(self, ch='a_ch2', power=500, freq=100e6): # For TESTING sample_rate = 1.25e9 # Sample set to default - 1.25 GSa/sec freq_duration = 2e-3 # Duration of each frequency set to 3 μsec average_factor = 100000 # How many sweeps will be conducted at each ODMR line scan samples_per_freq = int(sample_rate * freq_duration) # Number of samples required for each frequency x = np.linspace(start=0, stop=int(samples_per_freq) - 1, num=samples_per_freq) analog_samples = {ch: []} digital_samples = {} norm_freq = freq / sample_rate analog_samples[ch] = np.append(analog_samples[ch], np.sin(2 * np.pi * norm_freq * x)) self.set_analog_level(amplitude={ch: power}) num_of_samples, waveform_names = self.write_waveform(name='testing_sine', analog_samples=analog_samples, digital_samples=digital_samples, is_first_chunk=True, is_last_chunk=True, total_number_of_samples=len(analog_samples[ch])) self.load_waveform(load_dict=waveform_names) self.set_reps(average_factor) def generate_rect(self, freq1=100e3, freq2=50e3, pow1=250, pow2=260): """ Default values set to generate 1 V p-p square waves on analog channels 0 and 1. @param freq1: @param freq2: @param pow1: @param pow2: @return: """ # import numpy as np # import matplotlib.pyplot as plt # For TESTING sample_rate = 1.25e9 # Sample set to default - 1.25 GSa/sec freq_duration = 2e-3 # Duration of each frequency set to 3 μsec average_factor = 100000 # How many sweeps will be conducted at each ODMR line scan samples_per_freq = int(sample_rate * freq_duration) # Number of samples required for each frequency x = np.linspace(start=0, stop=int(samples_per_freq) - 1, num=samples_per_freq) analog_samples = {'a_ch0': [], 'a_ch1': []} digital_samples = {} norm_freq = freq1 / sample_rate analog_samples['a_ch0'] = np.append(analog_samples['a_ch0'], sp.square(2 * np.pi * norm_freq * x, duty=0.5)) # plt.plot(x, analog_samples['a_ch0']) norm_freq = freq2 / sample_rate analog_samples['a_ch1'] = np.append(analog_samples['a_ch1'], sp.square(2 * np.pi * norm_freq * x, duty=0.5)) self.set_analog_level(amplitude={'a_ch0': pow1, 'a_ch1': pow2}) num_of_samples, waveform_names = self.write_waveform(name='testing_rect', analog_samples=analog_samples, digital_samples=digital_samples, is_first_chunk=True, is_last_chunk=True, total_number_of_samples=len(analog_samples['a_ch0'])) self.load_waveform(load_dict=waveform_names) self.set_reps(average_factor) def generate_sine_ch0(self, freq=100e6, power=500): # For TESTING sample_rate = 1.25e9 # Sample set to default - 1.25 GSa/sec freq_duration = 2e-3 # Duration of each frequency set to 3 μsec average_factor = 100000 # How many sweeps will be conducted at each ODMR line scan samples_per_freq = int(sample_rate * freq_duration) # Number of samples required for each frequency x = np.linspace(start=0, stop=int(samples_per_freq) - 1, num=samples_per_freq) analog_samples = {'a_ch0': []} digital_samples = {} norm_freq = freq / sample_rate analog_samples['a_ch0'] = np.append(analog_samples['a_ch0'], np.sin(2 * np.pi * norm_freq * x)) self.set_analog_level(amplitude={'a_ch0': power}) num_of_samples, waveform_names = self.write_waveform(name='testing_sine', analog_samples=analog_samples, digital_samples=digital_samples, is_first_chunk=True, is_last_chunk=True, total_number_of_samples=len(analog_samples['a_ch0'])) self.load_waveform(load_dict=waveform_names) self.set_reps(average_factor)
def __init__(self, config, **kwargs): super().__init__(config=config, **kwargs) self.log.info('Dummy Pulser: I will simulate an AWG :) !') self.awg_waveform_directory = '/waves' if 'pulsed_file_dir' in config.keys(): self.pulsed_file_dir = config['pulsed_file_dir'] if not os.path.exists(self.pulsed_file_dir): homedir = get_home_dir() self.pulsed_file_dir = os.path.join(homedir, 'pulsed_files') self.log.warning('The directory defined in parameter ' '"pulsed_file_dir" in the config for ' 'SequenceGeneratorLogic class does not exist!\n' 'The default home directory\n{0}\n will be taken ' 'instead.'.format(self.pulsed_file_dir)) else: homedir = get_home_dir() self.pulsed_file_dir = os.path.join(homedir, 'pulsed_files') self.log.warning('No parameter "pulsed_file_dir" was specified ' 'in the config for SequenceGeneratorLogic as directory ' 'for the pulsed files!\nThe default home directory\n' '{0}\n' 'will be taken instead.'.format(self.pulsed_file_dir)) self.host_waveform_directory = self._get_dir_for_name('sampled_hardware_files') self.compatible_waveform_format = 'wfm' # choose one of 'wfm', 'wfmx' or 'fpga' self.compatible_sequence_format = 'seq' # choose one of 'seq' or 'seqx' self.connected = False self.sample_rate = 25e9 # Deactivate all channels at first: ch = {'a_ch1': False, 'a_ch2': False, 'a_ch3': False, 'd_ch1': False, 'd_ch2': False, 'd_ch3': False, 'd_ch4': False, 'd_ch5': False, 'd_ch6': False, 'd_ch7': False, 'd_ch8': False} self.active_channel = ch # for each analog channel one value self.amplitude_list = {'a_ch1': 1, 'a_ch2': 1, 'a_ch3': 1} self.offset_list = {'a_ch1': 0, 'a_ch2': 0, 'a_ch3': 0} # for each digital channel one value self.digital_high_list = {'d_ch1': 5, 'd_ch2': 5, 'd_ch3': 5, 'd_ch4': 5, 'd_ch5': 5, 'd_ch6': 5, 'd_ch7': 5, 'd_ch8': 5} self.digital_low_list = {'d_ch1': 0, 'd_ch2': 0, 'd_ch3': 0, 'd_ch4': 0, 'd_ch5': 0, 'd_ch6': 0, 'd_ch7': 0, 'd_ch8': 0} self.uploaded_assets_list = [] self.uploaded_files_list = [] self.current_loaded_asset = '' self.is_output_enabled = True # settings for remote access on the AWG PC self.asset_directory = 'waves' self.use_sequencer = True self.interleave = False self.current_status = 0 # that means off, not running.