diff --git a/qcodes/instrument_drivers/AlazarTech/ATS.py b/qcodes/instrument_drivers/AlazarTech/ATS.py index 2ea35c71ae43..0ba78a4ddc82 100644 --- a/qcodes/instrument_drivers/AlazarTech/ATS.py +++ b/qcodes/instrument_drivers/AlazarTech/ATS.py @@ -6,6 +6,7 @@ from qcodes.instrument.base import Instrument from qcodes.instrument.parameter import Parameter from qcodes.utils import validators +from qcodes.instrument.parameter import ManualParameter # TODO(damazter) (C) logging @@ -231,6 +232,22 @@ def __init__(self, name, system_id=1, board_id=1, dll_path=None, **kwargs): self.buffer_list = [] + # Some ATS models do not support a bwlimit. This flag defines if the + # ATS supports a bwlimit or not. True by default. + self._bwlimit_support = True + + # get channel info + max_s, bps = self._get_channel_info(self._handle) + self.add_parameter(name='bits_per_sample', + parameter_class=ManualParameter, + initial_value=bps) + self.add_parameter(name='bytes_per_sample', + parameter_class=ManualParameter, + initial_value=int((bps + 7)//8)) + self.add_parameter(name='maximum_samples', + parameter_class=ManualParameter, + initial_value=max_s) + def get_idn(self): """ This methods gets the most relevant information of this instrument @@ -384,15 +401,16 @@ def config(self, clock_source=None, sample_rate=None, clock_edge=None, self._handle, self.clock_source, self.sample_rate, self.clock_edge, self.decimation) - for i in range(1, self.channels + 1): + for i, ch in enumerate(self.channels): self._call_dll('AlazarInputControl', - self._handle, i, - self.parameters['coupling' + str(i)], - self.parameters['channel_range' + str(i)], - self.parameters['impedance' + str(i)]) - self._call_dll('AlazarSetBWLimit', - self._handle, i, - self.parameters['bwlimit' + str(i)]) + self._handle, 2**i, # Channel in binary format + self.parameters['coupling' + ch], + self.parameters['channel_range' + ch], + self.parameters['impedance' + ch]) + if self._bwlimit_support: + self._call_dll('AlazarSetBWLimit', + self._handle, i - 1, + self.parameters['bwlimit' + ch]) self._call_dll('AlazarSetTriggerOperation', self._handle, self.trigger_operation, @@ -472,8 +490,8 @@ def acquire(self, mode=None, samples_per_record=None, # endregion self.mode._set_updated() mode = self.mode.get() - if mode not in ('TS', 'NPT'): - raise Exception("Only the 'TS' and 'NPT' modes are implemented " + if mode not in ('TS', 'NPT', 'CS'): + raise Exception("Only the 'TS', 'CS', 'NPT' modes are implemented " "at this point") # -----set final configurations----- @@ -481,24 +499,15 @@ def acquire(self, mode=None, samples_per_record=None, # Abort any previous measurement self._call_dll('AlazarAbortAsyncRead', self._handle) - # get channel info - max_s, bps = self._get_channel_info(self._handle) - if bps != 8: - raise Exception('Only 8 bits per sample supported at this moment') - # Set record size for NPT mode - if mode == 'NPT': - pretriggersize = 0 # pretriggersize is 0 for NPT always + if mode in ['CS', 'NPT']: + pretriggersize = 0 # pretriggersize is 0 for NPT and CS always post_trigger_size = self.samples_per_record._get_byte() self._call_dll('AlazarSetRecordSize', self._handle, pretriggersize, post_trigger_size) - # set acquisition parameters here for NPT, TS mode - if self.channel_selection._get_byte() == 3: - number_of_channels = 2 - else: - number_of_channels = 1 + number_of_channels = len(self.channel_selection._latest_value) samples_per_buffer = 0 buffers_per_acquisition = self.buffers_per_acquisition._get_byte() samples_per_record = self.samples_per_record._get_byte() @@ -510,6 +519,7 @@ def acquire(self, mode=None, samples_per_record=None, self.interleave_samples._get_byte() | self.get_processed_data._get_byte()) + # set acquisition parameters here for NPT, TS, CS mode if mode == 'NPT': records_per_buffer = self.records_per_buffer._get_byte() records_per_acquisition = ( @@ -542,6 +552,20 @@ def acquire(self, mode=None, samples_per_record=None, self.records_per_buffer, buffers_per_acquisition, acquire_flags) + elif mode == 'CS': + if self.records_per_buffer._get_byte() != 1: + logging.warning('records_per_buffer should be 1 in TS mode, ' + 'defauling to 1') + self.records_per_buffer._set(1) + + samples_per_buffer = samples_per_record + + self._call_dll('AlazarBeforeAsyncRead', + self._handle, self.channel_selection, + self.transfer_offset, samples_per_buffer, + self.records_per_buffer, buffers_per_acquisition, + acquire_flags) + self.samples_per_record._set_updated() self.records_per_buffer._set_updated() self.buffers_per_acquisition._set_updated() @@ -556,12 +580,15 @@ def acquire(self, mode=None, samples_per_record=None, # create buffers for acquisition self.clear_buffers() - # make sure that allocated_buffers <= buffers_per_acquisition - if (self.allocated_buffers._get_byte() > - self.buffers_per_acquisition._get_byte()): - print("'allocated_buffers' should be smaller than or equal to" - "'buffers_per_acquisition'. Defaulting 'allocated_buffers' to" - "" + str(self.buffers_per_acquisition._get_byte())) + # make sure that allocated_buffers <= buffers_per_acquisition and + # buffer acquisition is not in acquire indefinite mode (0x7FFFFFFF) + if (not self.buffers_per_acquisition._get_byte() == 0x7FFFFFFF) and \ + (self.allocated_buffers._get_byte() > + self.buffers_per_acquisition._get_byte()): + logging.warning( + "'allocated_buffers' should be smaller than or equal to" + "'buffers_per_acquisition'. Defaulting 'allocated_buffers' to'" + "" + str(self.buffers_per_acquisition._get_byte())) self.allocated_buffers._set( self.buffers_per_acquisition._get_byte()) @@ -569,7 +596,8 @@ def acquire(self, mode=None, samples_per_record=None, for k in range(allocated_buffers): try: - self.buffer_list.append(Buffer(bps, samples_per_buffer, + self.buffer_list.append(Buffer(self.bits_per_sample(), + samples_per_buffer, number_of_channels)) except: self.clear_buffers() @@ -592,10 +620,14 @@ def acquire(self, mode=None, samples_per_record=None, buffer_timeout = self.buffer_timeout._get_byte() self.buffer_timeout._set_updated() - buffer_recycling = (self.buffers_per_acquisition._get_byte() > - self.allocated_buffers._get_byte()) + # Recycle buffers either if using continuous streaming mode or if + # more buffers are needed than the number of allocated buffers + buffer_recycling = \ + (self.buffers_per_acquisition._get_byte() == 0x7FFFFFFF) or \ + (self.buffers_per_acquisition._get_byte() > + self.allocated_buffers._get_byte()) - while buffers_completed < self.buffers_per_acquisition._get_byte(): + while acquisition_controller.requires_buffer(buffers_completed): buf = self.buffer_list[buffers_completed % allocated_buffers] self._call_dll('AlazarWaitAsyncBufferComplete', @@ -608,7 +640,6 @@ def acquire(self, mode=None, samples_per_record=None, # if buffers must be recycled, extract data and repost them # otherwise continue to next buffer - if buffer_recycling: acquisition_controller.handle_buffer(buf.buffer) self._call_dll('AlazarPostAsyncBuffer', @@ -634,16 +665,38 @@ def acquire(self, mode=None, samples_per_record=None, # return result return acquisition_controller.post_acquire() + def triggered(self): + """ + Checks if the ATS has received at least one trigger. + Returns: + 1 if there has been a trigger, 0 otherwise + """ + return self._call_dll('AlazarTriggered', self._handle, + error_check=False) + + def get_status(self): + """ + Returns t + Returns: + + """ + return self._call_dll('AlazarGetStatus', self._handle, + error_check=False) + def _set_if_present(self, param_name, value): if value is not None: self.parameters[param_name]._set(value) - def _set_list_if_present(self, param_base, value): - if value is not None: - for i, v in enumerate(value): - self.parameters[param_base + str(i + 1)]._set(v) + def _set_list_if_present(self, param_base, values): + if values is not None: + # Create list of identical values if a single value is given + if not isinstance(values, list): + values = [values] * len(self.channels) + for val, ch in zip(values, self.channels): + if param_base + ch in self.parameters.keys(): + self.parameters[param_base + ch]._set(val) - def _call_dll(self, func_name, *args): + def _call_dll(self, func_name, *args, error_check=True): """ Execute a dll function `func_name`, passing it the given arguments @@ -669,7 +722,7 @@ def _call_dll(self, func_name, *args): return_code = func(*args_out) # check for errors - if (return_code != self._success) and (return_code !=518): + if error_check and (return_code not in [self._success, 518]): # TODO(damazter) (C) log error argrepr = repr(args_out) @@ -684,6 +737,8 @@ def _call_dll(self, func_name, *args): 'error {}: {} from function {} with args: {}'.format( return_code, self._error_codes[return_code], func_name, argrepr)) + elif not error_check: + return return_code # mark parameters updated (only after we've checked for errors) for param in update_params: @@ -846,17 +901,20 @@ class Buffer: """ def __init__(self, bits_per_sample, samples_per_buffer, number_of_channels): - if bits_per_sample != 8: - raise Exception("Buffer: only 8 bit per sample supported") if os.name != 'nt': raise Exception("Buffer: only Windows supported at this moment") self._allocated = True + bytes_per_sample = int((bits_per_sample + 7)//8) + np_sample_type = {1: np.uint8, + 2: np.uint16}[bytes_per_sample] + # try to allocate memory mem_commit = 0x1000 page_readwrite = 0x4 - self.size_bytes = samples_per_buffer * number_of_channels + self.size_bytes = bytes_per_sample * samples_per_buffer * \ + number_of_channels # for documentation please see: # https://msdn.microsoft.com/en-us/library/windows/desktop/aa366887(v=vs.85).aspx @@ -872,7 +930,7 @@ def __init__(self, bits_per_sample, samples_per_buffer, ctypes_array = (ctypes.c_uint8 * self.size_bytes).from_address(self.addr) - self.buffer = np.frombuffer(ctypes_array, dtype=np.uint8) + self.buffer = np.frombuffer(ctypes_array, dtype=np_sample_type) pointer, read_only_flag = self.buffer.__array_interface__['data'] def free_mem(self): @@ -937,6 +995,23 @@ def __init__(self, name, alazar_name, **kwargs): self._alazar = self.find_instrument(alazar_name, instrument_class=AlazarTech_ATS) + self._acquisition_settings = {} + self._fixed_acquisition_settings = {} + self.add_parameter(name="acquisition_settings", + get_cmd=lambda: self._acquisition_settings) + + # Names and shapes must have initial value, even through they will be + # overwritten in set_acquisition_settings. If we don't do this, the + # remoteInstrument will not recognize that it returns multiple values. + self.add_parameter(name="acquisition", + names=['channel_signal'], + get_cmd=self.do_acquisition, + shapes=((),), + snapshot_value=False) + + # Save bytes_per_sample received from ATS digitizer + self._bytes_per_sample = self._alazar.bytes_per_sample() * 8 + def _get_alazar(self): """ returns a reference to the alazar instrument. A call to self._alazar is @@ -945,6 +1020,116 @@ def _get_alazar(self): """ return self._alazar + def verify_acquisition_settings(self, **kwargs): + """ + Ensure that none of the fixed acquisition settings are overwritten + Args: + **kwargs: List of acquisition settings + + Returns: + acquisition settings wwith fixed settings + """ + for key, val in self._fixed_acquisition_settings.items(): + if kwargs.get(key, val) != val: + logging.warning('Cannot set {} to {}. Defaulting to {}'.format( + key, kwargs[key], val)) + kwargs[key] = val + return kwargs + + def get_acquisition_setting(self, setting): + """ + Obtain an acquisition setting for the ATS. + It checks if the setting is in ATS_controller._acquisition_settings + If not, it will retrieve the ATS latest parameter value + + Args: + setting: acquisition setting to look for + + Returns: + Value of the acquisition setting + """ + if setting in self._acquisition_settings.keys(): + return self._acquisition_settings[setting] + else: + # Must get latest value, since it may not be updated in ATS + return self._alazar.parameters[setting].get_latest() + + def update_acquisition_settings(self, **kwargs): + """ + Updates acquisition settings after first verifying that none of the + fixed acquisition settings are overwritten. Any pre-existing settings + that are not overwritten remain. + + Args: + **kwargs: acquisition settings + + Returns: + None + """ + kwargs = self.verify_acquisition_settings(**kwargs) + self._acquisition_settings.update(**kwargs) + + def set_acquisition_settings(self, **kwargs): + """ + Sets acquisition settings after first verifying that none of the + fixed acquisition settings are overwritten. Any pre-existing settings + that are not overwritten are removed. + + Args: + **kwargs: acquisition settings + + Returns: + None + """ + kwargs = self.verify_acquisition_settings(**kwargs) + self._acquisition_settings = kwargs + + def do_acquisition(self): + """ + Performs an acquisition using the acquisition settings + Returns: + None + """ + records = self._alazar.acquire(acquisition_controller=self, + **self._acquisition_settings) + return records + + def requires_buffer(self): + """ + Check if enough buffers are acquired + Returns: + True if more buffers are needed, False otherwise + """ + raise NotImplementedError( + 'This method should be implemented in a subclass') + + def segment_buffer(self, buffer, scale_voltages=True): + """ + Segments buffers into the distinct channels + Args: + buffer: 1D buffer array containing all channels + scale_voltages: Whether or not to scale data to actual volts + Returns: + buffer_segments: Dictionary with items channel_idx: channel_buffer + """ + + buffer_segments = {} + for ch, ch_idx in enumerate(self.channel_selection): + buffer_slice = slice(ch * self.samples_per_record, + (ch + 1) * self.samples_per_record) + # TODO int16 conversion necessary but should be done earlier + buffer_segment = buffer[buffer_slice] + + if scale_voltages: + # Convert data points from an uint16 to volts + ch_range = self._alazar.parameters['channel_range'+ch_idx]() + # Determine value corresponding to zero for unsigned int + mid_val = 2.**(self._bytes_per_sample-1) + buffer_segment = (buffer_segment - mid_val) / mid_val * ch_range + + buffer_segments[ch_idx] = buffer_segment + return buffer_segments + def pre_start_capture(self): """ Use this method to prepare yourself for the data acquisition diff --git a/qcodes/instrument_drivers/AlazarTech/ATS9440.py b/qcodes/instrument_drivers/AlazarTech/ATS9440.py new file mode 100644 index 000000000000..9b4083a8d0d4 --- /dev/null +++ b/qcodes/instrument_drivers/AlazarTech/ATS9440.py @@ -0,0 +1,260 @@ +from .ATS import AlazarTech_ATS, AlazarParameter +from qcodes.utils import validators + + +class ATS9440(AlazarTech_ATS): + def __init__(self, name, **kwargs): + dll_path = 'C:\\WINDOWS\\System32\\ATSApi.dll' + super().__init__(name, dll_path=dll_path, **kwargs) + + self._bwlimit_support = False + + # add parameters + self.channels = ['A', 'B', 'C', 'D'] + + # ----- Parameters for the configuration of the board ----- + self.add_parameter(name='clock_source', + parameter_class=AlazarParameter, + label='Clock Source', + unit=None, + value='internal_clock', + byte_to_value_dict={1: 'internal_clock', + 4: 'slow_external_clock', + 5: 'external_clock_AC', + 7: 'external_clock_10_MHz_ref'}) + self.add_parameter(name='sample_rate', + parameter_class=AlazarParameter, + label='Sample Rate', + unit='S/s', + value=100000, + byte_to_value_dict={ + 0x1: 1000, 0x2: 2000, 0x4: 5000, 0x8: 10000, + 0xA: 20000, 0xC: 50000, 0xE: 100000, + 0x10: 200000, 0x12: 500000, 0x14: 1000000, + 0x18: 2000000, 0x1A: 5000000, 0x1C: 10000000, + 0x1E: 20000000, 0x22: 50000000, 0x24: 100000000, + 0x25: 125000000, 0x40: 'external_clock', + 1000000000: '1GHz_reference_clock'}) + self.add_parameter(name='clock_edge', + parameter_class=AlazarParameter, + label='Clock Edge', + unit=None, + value='rising', + byte_to_value_dict={0: 'rising', + 1: 'falling'}) + + self.add_parameter(name='decimation', + parameter_class=AlazarParameter, + label='Decimation', + unit=None, + value=0, + vals=validators.Ints(0, 100000)) + + # Acquisition channel parameters + for ch in self.channels: + self.add_parameter(name='coupling' + ch, + parameter_class=AlazarParameter, + label='Coupling channel ' + ch, + unit=None, + value='DC', + byte_to_value_dict={1: 'AC', 2: 'DC'}) + self.add_parameter(name='channel_range' + ch, + parameter_class=AlazarParameter, + label='Range channel ' + ch, + unit='V', + value=1, + byte_to_value_dict={ + 5: 0.1, 6: 0.2, 7: 0.4, + 10: 1., 11: 2., 12: 4.}) + self.add_parameter(name='impedance' + ch, + parameter_class=AlazarParameter, + label='Impedance channel ' + ch, + unit='Ohm', + value=50, + byte_to_value_dict={2: 50}) + + # Trigger parameters + self.add_parameter(name='trigger_operation', + parameter_class=AlazarParameter, + label='Trigger Operation', + unit=None, + value='J', + byte_to_value_dict={ + 0: 'J', + 1: 'K', + 2: 'J_or_K', + 3: 'J_and_K', + 4: 'J_xor_K', + 5: 'J_and_not_K', + 6: 'not_J_and_K'}) + for i in ['1', '2']: + self.add_parameter(name='trigger_engine' + i, + parameter_class=AlazarParameter, + label='Trigger Engine ' + i, + unit=None, + value=('J' if i == '1' else 'K'), + byte_to_value_dict={0: 'J', + 1: 'K'}) + self.add_parameter(name='trigger_source' + i, + parameter_class=AlazarParameter, + label='Trigger Source ' + i, + unit=None, + value='disable', + byte_to_value_dict={0: 'A', + 1: 'B', + 2: 'trig_in', + 3: 'disable', + 4: 'C', + 5: 'D'}) + self.add_parameter(name='trigger_slope' + i, + parameter_class=AlazarParameter, + label='Trigger Slope ' + i, + unit=None, + value='positive', + byte_to_value_dict={1: 'positive', + 2: 'negative'}) + self.add_parameter(name='trigger_level' + i, + parameter_class=AlazarParameter, + label='Trigger Level ' + i, + unit=None, + value=150, + vals=validators.Ints(0, 255)) + + self.add_parameter(name='external_trigger_coupling', + parameter_class=AlazarParameter, + label='External Trigger Coupling', + unit=None, + value='AC', + byte_to_value_dict={1: 'AC', 2: 'DC'}) + self.add_parameter(name='external_trigger_range', + parameter_class=AlazarParameter, + label='External Trigger Range', + unit='V', + value=5, + byte_to_value_dict={0: 5, 1: 1}) + self.add_parameter(name='trigger_delay', + parameter_class=AlazarParameter, + label='Trigger Delay', + unit='Sample clock cycles', + value=0, + vals=validators.Ints(min_value=0)) + + # NOTE: The board will wait for a for this amount of time for a + # trigger event. If a trigger event does not arrive, then the + # board will automatically trigger. Set the trigger timeout value + # to 0 to force the board to wait forever for a trigger event. + # + # IMPORTANT: The trigger timeout value should be set to zero after + # appropriate trigger parameters have been determined, otherwise + # the board may trigger if the timeout interval expires before a + # hardware trigger event arrives. + self.add_parameter(name='timeout_ticks', + parameter_class=AlazarParameter, + label='Timeout Ticks', + unit='10 us', + value=0, + vals=validators.Ints(min_value=0)) + + # ----- Parameters for the acquire function ----- + self.add_parameter(name='mode', + parameter_class=AlazarParameter, + label='Acquisition mode', + unit=None, + value='NPT', + byte_to_value_dict={0x100: 'CS', 0x200: 'NPT', + 0x400: 'TS'}) + + # samples_per_record must be a multiple of 32, and 256 minimum! + # TODO check if it is 32 or 16, manual is unclear + self.add_parameter(name='samples_per_record', + parameter_class=AlazarParameter, + label='Samples per Record', + unit=None, + value=1024, + vals=validators.Multiples(divisor=16, min_value=16)) + + self.add_parameter(name='records_per_buffer', + parameter_class=AlazarParameter, + label='Records per Buffer', + unit=None, + value=10, + vals=validators.Ints(min_value=0)) + self.add_parameter(name='buffers_per_acquisition', + parameter_class=AlazarParameter, + label='Buffers per Acquisition', + unit=None, + value=10, + vals=validators.Ints(min_value=0)) + self.add_parameter(name='channel_selection', + parameter_class=AlazarParameter, + label='Channel Selection', + unit=None, + value='AB', + byte_to_value_dict={1: 'A', 2: 'B', 3: 'AB', 4:'C', + 5: 'AC', 6: 'BC',8:'D', 9: 'AD', + 10: 'BD', 12: 'CD', 15: 'ABCD'}) + self.add_parameter(name='transfer_offset', + parameter_class=AlazarParameter, + label='Transer Offset', + unit='Samples', + value=0, + vals=validators.Ints(min_value=0)) + self.add_parameter(name='external_startcapture', + parameter_class=AlazarParameter, + label='External Startcapture', + unit=None, + value='enabled', + byte_to_value_dict={0x0: 'disabled', + 0x1: 'enabled'}) + self.add_parameter(name='enable_record_headers', + parameter_class=AlazarParameter, + label='Enable Record Headers', + unit=None, + value='disabled', + byte_to_value_dict={0x0: 'disabled', + 0x8: 'enabled'}) + self.add_parameter(name='alloc_buffers', + parameter_class=AlazarParameter, + label='Alloc Buffers', + unit=None, + value='disabled', + byte_to_value_dict={0x0: 'disabled', + 0x20: 'enabled'}) + self.add_parameter(name='fifo_only_streaming', + parameter_class=AlazarParameter, + label='Fifo Only Streaming', + unit=None, + value='disabled', + byte_to_value_dict={0x0: 'disabled', + 0x800: 'enabled'}) + self.add_parameter(name='interleave_samples', + parameter_class=AlazarParameter, + label='Interleave Samples', + unit=None, + value='disabled', + byte_to_value_dict={0x0: 'disabled', + 0x1000: 'enabled'}) + self.add_parameter(name='get_processed_data', + parameter_class=AlazarParameter, + label='Get Processed Data', + unit=None, + value='disabled', + byte_to_value_dict={0x0: 'disabled', + 0x2000: 'enabled'}) + + self.add_parameter(name='allocated_buffers', + parameter_class=AlazarParameter, + label='Allocated Buffers', + unit=None, + value=2, + vals=validators.Ints(min_value=0)) + self.add_parameter(name='buffer_timeout', + parameter_class=AlazarParameter, + label='Buffer Timeout', + unit='ms', + value=1000, + vals=validators.Ints(min_value=0)) + + # TODO (M) make parameter for board type + + # TODO (M) check board kind diff --git a/qcodes/instrument_drivers/AlazarTech/ATS9870.py b/qcodes/instrument_drivers/AlazarTech/ATS9870.py index eebda1985496..567d84747dee 100644 --- a/qcodes/instrument_drivers/AlazarTech/ATS9870.py +++ b/qcodes/instrument_drivers/AlazarTech/ATS9870.py @@ -12,7 +12,9 @@ class AlazarTech_ATS9870(AlazarTech_ATS): def __init__(self, name, **kwargs): dll_path = 'C:\\WINDOWS\\System32\\ATSApi.dll' super().__init__(name, dll_path=dll_path, **kwargs) + # add parameters + self.channels = ['A', 'B', 'C', 'D'] # ----- Parameters for the configuration of the board ----- self.add_parameter(name='clock_source', @@ -53,30 +55,30 @@ def __init__(self, name, **kwargs): value=0, vals=validators.Ints(0, 100000)) - for i in ['1', '2']: - self.add_parameter(name='coupling' + i, + for ch in self.channels: + self.add_parameter(name='coupling' + ch, parameter_class=AlazarParameter, - label='Coupling channel ' + i, + label='Coupling channel ' + ch, unit=None, value='AC', byte_to_value_dict={1: 'AC', 2: 'DC'}) - self.add_parameter(name='channel_range' + i, + self.add_parameter(name='channel_range' + ch, parameter_class=AlazarParameter, - label='Range channel ' + i, + label='Range channel ' + ch, unit='V', value=4, byte_to_value_dict={ 2: 0.04, 5: 0.1, 6: 0.2, 7: 0.4, 10: 1., 11: 2., 12: 4.}) - self.add_parameter(name='impedance' + i, + self.add_parameter(name='impedance' + ch, parameter_class=AlazarParameter, - label='Impedance channel ' + i, + label='Impedance channel ' + ch, unit='Ohm', value=50, byte_to_value_dict={1: 1000000, 2: 50}) - self.add_parameter(name='bwlimit' + i, + self.add_parameter(name='bwlimit' + ch, parameter_class=AlazarParameter, - label='Bandwidth limit channel ' + i, + label='Bandwidth limit channel ' + ch, unit=None, value='DISABLED', byte_to_value_dict={0: 'DISABLED', @@ -100,7 +102,7 @@ def __init__(self, name, **kwargs): parameter_class=AlazarParameter, label='Trigger Engine ' + i, unit=None, - value='TRIG_ENGINE_' + ('J' if i == 0 else 'K'), + value='TRIG_ENGINE_'+('J' if i == '1' else 'K'), byte_to_value_dict={0: 'TRIG_ENGINE_J', 1: 'TRIG_ENGINE_K'}) self.add_parameter(name='trigger_source' + i, diff --git a/qcodes/instrument_drivers/AlazarTech/ATS_acquisition_controllers.py b/qcodes/instrument_drivers/AlazarTech/ATS_acquisition_controllers.py index 919e777cab43..b9b73b2020d7 100644 --- a/qcodes/instrument_drivers/AlazarTech/ATS_acquisition_controllers.py +++ b/qcodes/instrument_drivers/AlazarTech/ATS_acquisition_controllers.py @@ -1,6 +1,307 @@ -from .ATS import AcquisitionController import math import numpy as np +import logging +import time + +from qcodes.instrument.parameter import ManualParameter +from qcodes.utils import validators as vals +from .ATS import AcquisitionController + + +class Triggered_AcquisitionController(AcquisitionController): + """ + Acquisition controller that acquires a record after each trigger. + + The resulting data is a list, where each element corresponds to an ATS + acquisition channel, and whose shape depends on post-processing. + The parameter average_mode sets the data averaging during post-processing. + Possible modes are: + 'none': Return full traces, each output data element has shape + (records_per_buffer * buffers_per_acquisition, + samples_per_record) + 'trace': Average over all traces (records), with data element shape + (samples_per_record) + 'point': Average over all traces and over time, returning the average + signal as a single value. + """ + def __init__(self, name, alazar_name, **kwargs): + super().__init__(name, alazar_name, **kwargs) + self.samples_per_record = None + self.records_per_buffer = None + self.buffers_per_acquisition = None + self.buffer = None + self.buffer_idx = 0 + + self._fixed_acquisition_settings = { + 'mode': 'NPT' + } + + self.add_parameter(name='average_mode', + parameter_class=ManualParameter, + initial_value='trace', + vals=vals.Enum('none', 'trace', 'point')) + + def setup(self, **kwargs): + """ + Setup the ATS controller by updating most current ATS values and setting + the acquisition parameter metadata + + Returns: + None + """ + # Update acquisition parameter values. These depend on the average mode + for attr in ['channel_selection', 'samples_per_record', + 'records_per_buffer', 'buffers_per_acquisition']: + setattr(self, attr, self.get_acquisition_setting(attr)) + self.samples_per_buffer = self.samples_per_record * \ + self.records_per_buffer + self.number_of_channels = len(self.channel_selection) + self.traces_per_acquisition = self.buffers_per_acquisition * \ + self.records_per_buffer + + if self.samples_per_record % 16: + raise SyntaxError('Samples per record {} is not multiple of ' + '16'.format(self.samples_per_record)) + + # Set acquisition parameter metadata + self.acquisition.names = tuple(['ch{}_signal'.format(ch) for ch in + self.channel_selection]) + self.acquisition.labels = self.acquisition.names + self.acquisition.units = ['V'] * self.number_of_channels + + if self.average_mode() == 'point': + shape = () + elif self.average_mode() == 'trace': + shape = (self.samples_per_record,) + else: + shape = (self.traces_per_acquisition, self.samples_per_record) + self.acquisition.shapes = tuple([shape] * self.number_of_channels) + + def requires_buffer(self, buffers_completed): + # Using buffers_completed instead of self.buffer_idx because the + # buffers may all be passed on to handle_buffer after all are filled + return buffers_completed < self.buffers_per_acquisition + + def pre_start_capture(self): + """ + Initializes buffers before capturing + """ + self.buffer_idx = 0 + self.buffers = [np.zeros((self.traces_per_acquisition, + self.samples_per_record)) + for ch in self.channel_selection] + + def pre_acquire(self): + # gets called after 'AlazarStartCapture' + pass + + def handle_buffer(self, buffer): + if self.buffer_idx < self.buffers_per_acquisition: + # Segment the buffer into buffers for each channel + segmented_buffer = self.segment_buffer(buffer, scale_voltages=True) + + # Save buffer components into each channel dataset + for ch, ch_name in enumerate(self.channel_selection): + buffer_slice = slice( + self.buffer_idx * self.records_per_buffer, + (self.buffer_idx + 1) * self.records_per_buffer) + self.buffers[ch][buffer_slice] = segmented_buffer.pop(ch_name) + else: + logging.warning('Ignoring extra ATS buffer') + + self.buffer_idx += 1 + + def post_acquire(self): + # average over records in buffer: + # for ATS9360 samples are arranged in the buffer as follows: + # S0A, S0B, ..., S1A, S1B, ... + # with SXY the sample number X of channel Y. + + if self.average_mode() == 'none': + data = self.buffers + elif self.average_mode() == 'trace': + data = [np.mean(buffer, axis=0) for buffer in self.buffers] + elif self.average_mode() == 'point': + data = [np.mean(buffer) for buffer in self.buffers] + + return data + + +class Continuous_AcquisitionController(AcquisitionController): + """ + Acquisition controller that continuously acquires data without needing a + trigger. In contrast to triggered mode, here a trace may be composed of + multiple buffers. Therefore, the acquisition settings 'records_per_buffer' + and 'buffers_per_acquisition' are fixed, and instead the parameters + 'samples_per_trace' and 'traces_per_acquisition' must be set. + + The resulting data is a list, where each element corresponds to an ATS + acquisition channel, and whose shape depends on post-processing. + The parameter average_mode sets the data averaging during post-processing. + Possible modes are: + 'none': Return full traces, each output data element has shape + (traces_per_acquisition, + samples_per_trace) + 'trace': Average over all traces (records), with data element shape + (samples_per_trace) + 'point': Average over all traces and over time, returning the average + signal as a single value. + """ + def __init__(self, name, alazar_name, **kwargs): + super().__init__(name, alazar_name, **kwargs) + + # buffers_per_acquisition=0x7FFFFFFF results in buffers being collected + # indefinitely until aborted. + # Records_per_buffer must equal 1 for CS or TS + self._fixed_acquisition_settings = { + 'mode': 'CS', + 'buffers_per_acquisition': 0x7FFFFFFF, + 'records_per_buffer': 1} + self._acquisition_settings = self._fixed_acquisition_settings.copy() + + self.add_parameter(name='average_mode', + parameter_class=ManualParameter, + initial_value='trace', + vals=vals.Enum('none', 'trace', 'point')) + self.add_parameter(name='samples_per_trace', + parameter_class=ManualParameter, + vals=vals.Multiples(divisor=16)) + self.add_parameter(name='traces_per_acquisition', + parameter_class=ManualParameter, + vals=vals.Ints()) + + self.buffer_idx = None + self.trace_idx = None + self.buffer_start_idx = None + + def setup(self, **kwargs): + """ + Setup the ATS controller by updating most current ATS values and setting + the acquisition parameter metadata + + Returns: + None + """ + + # Update acquisition parameter values. These depend on the average mode + for attr in ['channel_selection', 'samples_per_record']: + setattr(self, attr, self.get_acquisition_setting(attr)) + self.number_of_channels = len(self.channel_selection) + self.buffers_per_trace = round(self.samples_per_trace() / \ + self.samples_per_record) + + if self.samples_per_record % 16: + raise SyntaxError('Samples per record {} is not multiple of ' + '16'.format(self.samples_per_record)) + + # Set acquisition parameter metadata + self.acquisition.names = tuple(['ch{}_signal'.format(ch) for ch in + self.channel_selection]) + self.acquisition.labels = self.acquisition.names + self.acquisition.units = ['V'] * self.number_of_channels + + if self.average_mode() == 'point': + shape = () + elif self.average_mode() == 'trace': + shape = (self.samples_per_trace(),) + else: + shape = (self.traces_per_acquisition(), self.samples_per_record) + self.acquisition.shapes = tuple([shape] * self.number_of_channels) + + def requires_buffer(self, buffers_completed): + return self.trace_idx < self.traces_per_acquisition() + + def pre_start_capture(self): + """ + Initializes buffers before capturing + """ + self.buffer_idx = 0 + self.trace_idx = 0 + self.buffer_start_idx = 0 + self.buffers = [np.zeros((self.traces_per_acquisition(), + self.samples_per_trace())) + for ch in self.channel_selection] + + def pre_acquire(self): + # gets called after 'AlazarStartCapture' + pass + + def handle_buffer(self, buffer): + """ + Adds buffers to fill up the data traces. + Because a trace can consist of multiple buffers, the 'trace_idx' and + 'buffer_idx' determine the relevant trace/buffer within a trace, + respectively. + Note that 'buffer_start_idx' set to nonzero if the first buffer should + be added from a nonzero starting idx. In this case, the first buffer + must have 'buffer_idx = -1'. + Args: + buffer: Buffer to add to trace + + Returns: + None + """ + if self.buffer_idx >= self.buffers_per_trace * \ + self.traces_per_acquisition(): + print('Ignoring extra ATS buffer {}'.format(self.buffer_idx)) + return None + + # Segment the buffer into buffers for each channel + segmented_buffer = self.segment_buffer(buffer, scale_voltages=True) + + # Determine the slice where the buffer segments should go in the traces, + # and possibly cut the segments if they start at nonzero idx, + # or if they don't fit completely in a trace + if self.buffer_idx == -1: + # This is the first (incomplete) buffer, whose starting idx is + # buffer_start_idx, add it to the beginning of the segmented buffer + buffer_slice = slice(None, self.samples_per_record - + self.buffer_start_idx) + for ch_name, buffer_segment in segmented_buffer.items(): + # Shorten each of the segments to start from buffer_start_idx + segmented_buffer[ch_name] = \ + buffer_segment[self.buffer_start_idx:] + else: + # Determine the buffer idx offset in the trace + trace_offset = self.samples_per_record - self.buffer_start_idx + \ + self.samples_per_record * \ + (self.buffer_idx % self.buffers_per_trace) + + if trace_offset+self.samples_per_record > self.samples_per_trace(): + # Buffer does not fit completely in a trace, cutting buffer + buffer_slice = slice(trace_offset, None) + for ch_name, buffer_segment in segmented_buffer.items(): + # Shorten each of the segments to stop at samples_per_trace + max_idx = self.samples_per_trace() - trace_offset + segmented_buffer[ch_name] = buffer_segment[:max_idx] + else: + buffer_slice = slice(trace_offset, + trace_offset + self.samples_per_record) + + # Save buffer components into each channel dataset + for ch, ch_name in enumerate(self.channel_selection): + self.buffers[ch][self.trace_idx, buffer_slice] = \ + segmented_buffer[ch_name] + + self.buffer_idx += 1 + if self.buffer_idx and not self.buffer_idx % self.buffers_per_trace: + # Filled a trace + self.trace_idx += 1 + + def post_acquire(self): + # average over records in buffer: + # for ATS9360 samples are arranged in the buffer as follows: + # S0A, S0B, ..., S1A, S1B, ... + # with SXY the sample number X of channel Y. + + if self.average_mode() == 'none': + data = self.buffers + elif self.average_mode() == 'trace': + data = [np.mean(buffer, axis=0) for buffer in self.buffers] + elif self.average_mode() == 'point': + data = [np.mean(buffer) for buffer in self.buffers] + + return data # DFT AcquisitionController