From c65296577fd826f56e64710e83e9ff93463ffc13 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Anders=20Ku=CC=88hle?= Date: Mon, 24 Jun 2019 11:33:26 +0200 Subject: [PATCH 01/32] feat: Make driver for the QDevil QDAC Make new driver for the QDevil QDAC based on the existing QDac_channels driver, originally made for the QDAC prototypes assembled at the Niels Bohr Institute --- .../drivers/QDevil/QDevil_QDac_channels.py | 840 ++++++++++++++++++ 1 file changed, 840 insertions(+) create mode 100644 qcodes_contrib_drivers/drivers/QDevil/QDevil_QDac_channels.py diff --git a/qcodes_contrib_drivers/drivers/QDevil/QDevil_QDac_channels.py b/qcodes_contrib_drivers/drivers/QDevil/QDevil_QDac_channels.py new file mode 100644 index 000000000..69d0d26f4 --- /dev/null +++ b/qcodes_contrib_drivers/drivers/QDevil/QDevil_QDac_channels.py @@ -0,0 +1,840 @@ +# QCoDeS driver for the QDevil QDAC using channels +# Adapted by QDevil from "qdev\QDac_channels.py" in the instrument drivers package - kudos to QDEV and the Microsoft quantum computing consortium for making that +# AK QDevil 2019-06-24 + +import time +import visa +import logging +import numpy as np + +from datetime import datetime +from functools import partial +from operator import xor +from collections import OrderedDict + +from qcodes.instrument.channel import InstrumentChannel, ChannelList +from qcodes.instrument.channel import MultiChannelInstrumentParameter +from qcodes.instrument.visa import VisaInstrument +from qcodes.utils import validators as vals + +log = logging.getLogger(__name__) + +class QDacChannel(InstrumentChannel): + """ + A single output channel of the QDac. + + Exposes chan.v, chan.vrange, chan.vmin, chan.vmax, chan.slope, chan.i, chan.irange, chan.slope + Always set v to zero before changing vrange or irange + """ + + _CHANNEL_VALIDATION = vals.Numbers(1, 48) + + def __init__(self, parent, name, channum): + """ + Args: + parent (Instrument): The instrument to which the channel is + attached. + name (str): The name of the channel + channum (int): The number of the channel in question (1-48) + """ + super().__init__(parent, name) + + # Validate the channel + self._CHANNEL_VALIDATION.validate(channum) + + # Add the parameters + + self.add_parameter('v', + label='Channel {} voltage'.format(channum), + unit='V', + set_cmd=partial(self._parent._set_voltage, channum), + get_cmd='set {}'.format(channum), + get_parser=float, + vals=vals.Numbers(-9.99, 9.99) # Initial values. Are updated at initialisation and when vrange is changed + ) + + self.add_parameter('vrange', + label='Channel {} range.'.format(channum), + set_cmd=partial(self._parent._set_vrange, channum), + get_cmd='vol {}'.format(channum), + get_parser=int, + vals=vals.Enum(0, 1) + ) + + self.add_parameter('vmin', + label='Channel {} min voltage'.format(channum), + unit='V', + set_cmd=None, + get_cmd=partial(self._parent._get_vmin, channum), + get_parser=float, + ) + + self.add_parameter('vmax', + label='Channel {} max voltage'.format(channum), + unit='V', + set_cmd=None, + get_cmd=partial(self._parent._get_vmax, channum), + get_parser=float, + ) + + self.add_parameter('i', + label='Channel {} current'.format(channum), + get_cmd='get {}'.format(channum), + unit='A', + get_parser=self._parent._current_parser + ) + + self.add_parameter('irange', + label='Channel {} irange'.format(channum), + set_cmd=partial(self._parent._set_irange, channum), + get_cmd='cur {}'.format(channum), + get_parser=int + ) + + self.add_parameter('slope', + label='Channel {} slope'.format(channum), + unit='V/s', + set_cmd=partial(self._parent._setslope, channum), + get_cmd=partial(self._parent._getslope, channum), + vals=vals.MultiType(vals.Enum('Inf'), + vals.Numbers(1e-3, 100)) + ) + + self.add_parameter('sync', + label='Channel {} sync output'.format(channum), + set_cmd=partial(self._parent._setsync, channum), + get_cmd=partial(self._parent._getsync, channum), + vals=vals.Ints(0, 5) + ) + + self.add_parameter(name='sync_delay', + label='Channel {} sync pulse delay'.format(channum), + unit='s', + get_cmd=None, set_cmd=None, + initial_value=0 + ) + + self.add_parameter(name='sync_duration', + label='Channel {} sync pulse duration'.format(channum), + unit='s', + get_cmd=None, set_cmd=None, + initial_value=0.01 + ) + + def snapshot_base(self, update=False, params_to_skip_update=None): + update_currents = self._parent._update_currents and update + if update and not self._parent._get_status_performed: + self._parent._get_status(readcurrents=update_currents) + # call get_status rather than getting the status individually for + # each parameter. This is only done if _get_status_performed is False + # this is used to signal that the parent has already called it and + # no need to repeat. + if params_to_skip_update is None: + params_to_skip_update = ('v', 'i', 'irange', 'vrange') + snap = super().snapshot_base(update=update, + params_to_skip_update=params_to_skip_update) + return snap + + +class QDacMultiChannelParameter(MultiChannelInstrumentParameter): + """ + The class to be returned by __getattr__ of the ChannelList. Here customised + for fast multi-readout of voltages. + """ + def __init__(self, channels, param_name, *args, **kwargs): + super().__init__(channels, param_name, *args, **kwargs) + + def get_raw(self): + """ + Return a tuple containing the data from each of the channels in the + list. + """ + # For voltages, we can do something slightly faster than the naive + # approach + + if self._param_name == 'v': + qdac = self._channels[0]._parent + qdac._get_status(readcurrents=False) + output = tuple(chan.parameters[self._param_name].get_latest() + for chan in self._channels) + else: + output = tuple(chan.parameters[self._param_name].get() + for chan in self._channels) + + return output + + +class QDac(VisaInstrument): + """ + Channelised driver for the QDevil QDAC voltage source + + Based on "PM10091-7 QDAC SW1.07.pdf" + Tested with Firmware Version: 1.07 + + The driver assumes that the instrument is ALWAYS in verbose mode OFF + and sets this as part of the initialization. + """ + + # Range factors for QDevil QDAC 1.0 + voltage_range_status = {'X 1': 0, 'X 0.1': 1} + + # set nonzero value (seconds) to accept older status when reading settings + max_status_age = 1 + + def __init__(self, + name, + address, + num_chans=48, + update_currents=True, + **kwargs): + """ + Instantiates the instrument. + + Args: + name (str): The instrument name used by qcodes + address (str): The VISA name of the resource + num_chans (int): Number of channels to assign. Default: 48 (Legacy, not required) + update_currents (bool): Whether to query all channels for their + current current value on startup. Default: True. + + Returns: + QDac object + """ + + super().__init__(name, address, **kwargs) + handle = self.visa_handle + self._get_status_performed = False + + # This is the baud rate on power-up. It can be changed later but + # you must start out with this value. + handle.baud_rate = 480600 + handle.parity = visa.constants.Parity(0) + handle.data_bits = 8 + self.set_terminator('\n') + # TODO: do we want a method for write termination too? + handle.write_termination = '\n' + # TODO: do we need a query delay for robust operation? + self._write_response = '' + + # isQDevilQDAC = self._check_if_qdevilqdac() + firmware_version = self._get_firmware_version() + if firmware_version < 1.07: + log.warning("Firmware version: {}".format(firmware_version)) + raise RuntimeError( ''' + No QDevil QDAC detected or the firmware version is obsolete. + QCoDeS only supports version 1.07 or newer. In that case, + please Contact info@qdevil.com for an update. + ''') + + self.num_chans = self._get_number_of_channels() + num_boards = int(self.num_chans/8) + self._output_n_lines = self.num_chans + 2 + + # Assigned slopes. Entries will eventually be [chan, slope] + self._slopes = [] + # Function generators (used in _set_voltage) + self._fgs = set(range(1, 9)) + self._assigned_fgs = {} # {chan: fg} + # Sync channels + self._syncoutputs = [] # Entries: [chan, syncchannel] + + self.chan_range = range(1, 1 + self.num_chans) + self.channel_validator = vals.Ints(1, self.num_chans) + + channels = ChannelList(self, "Channels", QDacChannel, + snapshotable=False, + multichan_paramclass=QDacMultiChannelParameter) + + for i in self.chan_range: + channel = QDacChannel(self, 'chan{:02}'.format(i), i) + channels.append(channel) + # Should raise valueerror if name is invalid (silently fails now) + self.add_submodule('ch{:02}'.format(i), channel) + channels.lock() + self.add_submodule('channels', channels) + + for board in range(num_boards): + for sensor in range(3): + label = 'Board {}, Temperature {}'.format(board, sensor) + self.add_parameter(name='temp{}_{}'.format(board, sensor), + label=label, + unit='C', + get_cmd='tem {} {}'.format(board, sensor), + get_parser=self._num_verbose) + + self.add_parameter(name='cal', + set_cmd='cal {}', + vals=self.channel_validator) + # TO-DO: maybe it's too dangerous to have this settable. + # And perhaps ON is a better verbose mode default? + self.add_parameter(name='verbose', + set_cmd='ver {}', + val_mapping={True: 1, False: 0}) + + # Initialise the instrument, all channels DC (unbind func. generators) + for chan in self.chan_range: + # Note: this call does NOT change the voltage on the channel + self.write('wav {} 0 1 0'.format(chan)) + + # Get all calibrated min/max output values, before swithcing to verbose off mode + self.vranges = {} + for chan in self.chan_range: + self.vranges.update({chan: {0:self.get_MinMax_OutputVoltage(chan, 0), 1:self.get_MinMax_OutputVoltage(chan, 1)}}) + + self.verbose.set(False) + self.connect_message() + log.info('[*] Querying all channels for voltages and currents...') + self._get_status(readcurrents=update_currents) + self._update_currents = update_currents + # Set "v" parameter limits to actual calibrated limits + self._set_vvals_to_current_range() + log.info('[+] Done') + + def snapshot_base(self, update=False, params_to_skip_update=None): + update_currents = self._update_currents and update + if update: + self._get_status(readcurrents=update_currents) + self._get_status_performed = True + # call get_status rather than getting the status individually for + # each parameter. We set _get_status_performed to True + # to indicate that each update channel does not need to call this + # function as opposed to when snapshot is called on an individual + # channel + snap = super().snapshot_base(update=update, + params_to_skip_update=params_to_skip_update) + self._get_status_performed = False + return snap + + ######################### + # Channel gets/sets + ######################### + + def _set_voltage(self, chan, v_set): + """ + set_cmd for the chXX_v parameter + + Args: + chan (int): The 1-indexed channel number + v_set (float): The target voltage + + If a finite slope has been assigned, we assign a function generator to + ramp the voltage. + """ + # validation + atten = self.channels[chan-1].vrange.get_latest() + + outofrange = False + if v_set > self.vranges[chan][atten]['Max']: + v_set = self.vranges[chan][atten]['Max'] + outofrange = True + elif v_set < self.vranges[chan][atten]['Min']: + v_set = self.vranges[chan][atten]['Min'] + outofrange = True + + if outofrange: + log.warning('Requested voltage outside reachable range.' + + ' Setting voltage on channel ' + + '{} to {} V. '.format(chan, v_set) + + 'Note: _get_latest not up to date') + + slopechans = [sl[0] for sl in self._slopes] + if chan in slopechans: + slope = [sl[1] for sl in self._slopes if sl[0] == chan][0] + # find and assign fg + fg = min(self._fgs.difference(set(self._assigned_fgs.values()))) + self._assigned_fgs[chan] = fg + # We need .get and not get_latest in case a ramp was interrupted + v_start = self.channels[chan-1].v.get() + time = abs(v_set-v_start)/slope + log.info('Slope: {}, time: {}'.format(slope, time)) + # Attenuation compensation and syncing + # happen inside _rampvoltage + self._rampvoltage(chan, fg, v_start, v_set, time) + else: + self.write('wav {} 0 0 0;set {} {:.6f}'.format(chan, chan, v_set)) + + def _set_vrange(self, chan, switchint): + """ + set_cmd for changing the voltage range: chXX_vrange + + The switchint is an integer. 0: high range, 1: low range. + + The output voltage should be set to zero before changin the range, + eventhough we try to keep the voltage output steady if within range. + A spike will occur if the voltage is not zero. + """ + + old = self.channels[chan-1].vrange.get_latest() + old_irange = self.channels[chan-1].irange.get_latest() + + if xor(old, switchint): + # In the QDevil QDAC version 1.0 we can not have the high current range and the low voltage range at the same time + if (old_irange == 1) and (switchint == 1): + self.channels[chan-1].irange.set(0) + log.warning('QDAC can not be in low voltage range and high current range at the same time. Setting current range to low.') + voltageparam = self.channels[chan-1].v + volmessage = 'vol {} {}'.format(chan, switchint) + + # Avoid making coltage jumps when switching range by compensating for design flaws FW1.07. Spikes are unavoidable. + # Even if a channel is a slow channel it might not yet have be assigned to a generator and therefore + # Therefore it is not enough to test if the channel is a slope channel. That is why we ask the channel + self.write('wav {}'.format(chan)) + FW_str = self._write_response + gen,_,_ = FW_str.split(',') + if int(gen) > 0: + volmessage +=';wav {} {} {:.6f}'.format(chan, int(gen), 0) #The amplitude must be set to zero to avoid potential overflow. Assuming that vrange is not changed during a ramp + else: + volmessage +=';set {}'.format(chan) + + oldvoltage = voltageparam.get() # The only way to be sure about the actual output is to read it, as the buffered value is not updated after ramping + if (switchint == 0): + newvoltage = oldvoltage + else: + if oldvoltage > self.vranges[chan][switchint]['Max']: + newvoltage = self.vranges[chan][switchint]['Max'] + log.warning('Voltage is outside the bounds of the low range and is therefore clipped.') + elif oldvoltage < self.vranges[chan][switchint]['Min']: + newvoltage = self.vranges[chan][switchint]['Min'] + log.warning('Voltage is outside the bounds of the low range and is therefore clipped.') + else: + newvoltage = oldvoltage + volmessage += ' {:.6f}'.format(newvoltage) + self.write(volmessage) + voltageparam.vals = vals.Numbers(self.vranges[chan][switchint]['Min'],self.vranges[chan][switchint]['Max']) + voltageparam._save_val(newvoltage) + + def _set_vvals_to_current_range(self): + """ + Command for setting all "v" limits ("vals") of all channels to the + actual calibrated output limits for the range each individual channel is currently in. + """ + for chan in range(1,self.num_chans+1): + vrange = self.channels[chan-1].vrange.get_latest() + self.channels[chan-1].v.vals = vals.Numbers(self.vranges[chan][vrange]['Min'],self.vranges[chan][vrange]['Max']) + + def _set_irange(self, chan, switchint): + """ + set_cmd for changing the current measurement range: chXX_irange + + The switchint is an integer. 1: high range, 0: low range. + + The output voltage should be set to zero before changing the range, + otherwise a spike will occur on the output. When switching from high + current range to low current range the volage (if not zero) may drop if + the current draw is exceeding the max current in the low current range. + """ + old = self.channels[chan-1].irange.get_latest() + old_vrange = self.channels[chan-1].vrange.get_latest() + + if xor(old, switchint): + if (old_vrange == 1) and (switchint == 1): + log.warning(''' + QDAC can not be in high current range and low voltage range and at the same time. + Current range has NOT been changed and _get_latest is NOT up to date + ''') + self.channels[chan-1].irange._save_val(0) + else: + self.write('cur {} {}'.format(chan, switchint)) + + def _num_verbose(self, s): + """ + Turns a return value from the QDac into a number. + If the QDac is in verbose mode, this involves stripping off the + value descriptor. + """ + if self.verbose.get_latest(): + s = s.split[': '][-1] + return float(s) + + def _current_parser(self, s): + """ + parser for chXX_i parameter (converts from uA to A) + """ + return 1e-6*self._num_verbose(s) + + + def read_state(self, chan, param): + """ + specific routine for reading items out of status response + + Args: + chan (int): The 1-indexed channel number + param (str): The parameter in question, e.g. 'v' or 'vrange' + """ + if chan not in self.chan_range: + raise ValueError('valid channels are {}'.format(self.chan_range)) + valid_params = ('v', 'vrange', 'irange') + if param not in valid_params: + raise ValueError( + 'read_state valid params are {}'.format(valid_params)) + self._get_status(readcurrents=False) + + value = getattr(self.channels[chan-1], param).get_latest() + + returnmap = {'vrange': {1: 1, 10: 0}, + 'irange': {0: '1 muA', 1: '100 muA'}} + + if 'range' in param: + value = returnmap[param][value] + + return value + + def _get_status(self, readcurrents=False): + """ + Function to query the instrument and get the status of all channels. + Takes a while to finish. + + The `status` call generates 27 or 51 lines of output. Send the command and + read the first one, which is the software version line + the full output looks like: + Software Version: 1.07\r\n + Channel\tOut V\t\tVoltage range\tCurrent range\n + \n + 8\t 0.000000\t\tX 1\t\tpA\n + 7\t 0.000000\t\tX 1\t\tpA\n + ... (all 24/48 channels like this in a somewhat peculiar order) + (no termination afterward besides the \n ending the last channel) + returns a list of dicts [{v, vrange, irange}] + NOTE - channels are 1-based, but the return is a list, so of course + 0-based, ie chan1 is out[0] + """ + + # Status call + version_line = self.ask('status') + + if version_line.startswith('Software Version: '): + self.version = version_line.strip().split(': ')[1] + else: + self._wait_and_clear() + raise ValueError('unrecognized version line: ' + version_line) + + header_line = self.read() + headers = header_line.lower().strip('\r\n').split('\t') + expected_headers = ['channel', 'out v', '', 'voltage range', + 'current range'] + if headers != expected_headers: + raise ValueError('unrecognized header line: ' + header_line) + + chans = [{} for _ in self.chan_range] + chans_left = set(self.chan_range) + while chans_left: + line = self.read().strip() + if not line: + continue + chanstr, v, _, vrange, _, irange = line.split('\t') + chan = int(chanstr) + + irange_trans = {'hi cur': 1, 'lo cur': 0} + + # In the original driver from QDEV it was necessary to have vrange before v in the dict + vals_dict = OrderedDict() + vals_dict.update({'vrange': ('vrange', + self.voltage_range_status[vrange.strip()])}) + vals_dict.update({'irange': ('irange', irange_trans[irange])}) + vals_dict.update({'v': ('v', float(v))}) + chans[chan - 1] = vals_dict + + for param in vals_dict: + value = vals_dict[param][1] + getattr(self.channels[chan-1], param)._save_val(value) + chans_left.remove(chan) + + if readcurrents: + for chan in range(1, self.num_chans+1): + param = self.channels[chan-1].i + param._save_val(param.get()) + + self._status = chans + self._status_ts = datetime.now() + return chans + + def _setsync(self, chan, sync): + """ + set_cmd for the chXX_sync parameter. + + Args: + chan (int): The channel number (1-48 or 1-24) + sync (int): The associated sync output (1-3 on 24 ch units or 1-5 on 48 ch units). 0 means 'unassign' + """ + + if chan not in range(1, self.num_chans+1): + raise ValueError('Channel number must be 1-{}.'.format(self.num_chans)) + + if sync == 0: + # try to remove the sync from internal bookkeeping + try: + sc = self._syncoutputs + to_remove = [sc.index(syn) for syn in sc if syn[0] == chan][0] + self._syncoutputs.remove(sc[to_remove]) + except IndexError: + pass + # free the previously assigned sync + oldsync = self.channels[chan-1].sync.get_latest() + if oldsync is not None: + self.write('syn {} 0 0 0'.format(oldsync)) + return + + if sync in [syn[1] for syn in self._syncoutputs]: + oldchan = [syn[0] for syn in self._syncoutputs if syn[1] == sync][0] + self._syncoutputs.remove([oldchan, sync]) + + if chan in [syn[0] for syn in self._syncoutputs]: + oldsyn = [syn[1] for syn in self._syncoutputs if syn[0] == chan][0] + self._syncoutputs[self._syncoutputs.index([chan, oldsyn])] = [chan, + sync] + return + + self._syncoutputs.append([chan, sync]) + return + + def _getsync(self, chan): + """ + get_cmd of the chXX_sync parameter + """ + if chan in [syn[0] for syn in self._syncoutputs]: + sync = [syn[1] for syn in self._syncoutputs if syn[0] == chan][0] + return sync + else: + return 0 + + def _setslope(self, chan, slope): + """ + set_cmd for the chXX_slope parameter, the maximum slope of a channel. + + Args: + chan (int): The channel number (1-24 or 1-48) + slope (Union[float, str]): The slope in V/s. Write 'Inf' to allow + arbitraryly small rise times. + """ + if chan not in range(1, self.num_chans+1): + raise ValueError('Channel number must be 1-{}.'.format(self.num_chans)) + + if slope == 'Inf': + self.write('wav {} 0 0 0'.format(chan)) + + # Now clear the assigned slope and function generator (if possible) + try: + self._assigned_fgs.pop(chan) + except KeyError: + pass + # Remove a sync output, if one was assigned + syncchans = [syn[0] for syn in self._syncoutputs] + if chan in syncchans: + self.channels[chan-1].sync.set(0) + try: + sls = self._slopes + to_remove = [sls.index(sl) for sl in sls if sl[0] == chan][0] + self._slopes.remove(sls[to_remove]) + return + # If the value was already 'Inf', the channel was not + # in the list and nothing happens + except IndexError: + return + + if chan in [sl[0] for sl in self._slopes]: + oldslope = [sl[1] for sl in self._slopes if sl[0] == chan][0] + self._slopes[self._slopes.index([chan, oldslope])] = [chan, slope] + return + + if len(self._slopes) >= 8: + rampchans = ', '.join([str(c[0]) for c in self._slopes]) + raise ValueError('Can not assign finite slope to more than ' + + "8 channels. Assign 'Inf' to at least one of " + + 'the following channels: {}'.format(rampchans)) + + self._slopes.append([chan, slope]) + return + + def _getslope(self, chan): + """ + get_cmd of the chXX_slope parameter + """ + if chan in [sl[0] for sl in self._slopes]: + slope = [sl[1] for sl in self._slopes if sl[0] == chan][0] + return slope + else: + return 'Inf' + + def printslopes(self): + """ + Print the finite slopes assigned to channels + """ + for sl in self._slopes: + print('Channel {}, slope: {} (V/s)'.format(sl[0], sl[1])) + + def _rampvoltage(self, chan, fg, v_start, setvoltage, ramptime): + """ + Smoothly ramp the voltage of a channel by the means of a function + generator. Helper function used by _set_voltage. + + Args: + chan (int): The channel number (counting from 1) + fg (int): The function generator (counting from 1) + setvoltage (float): The voltage to ramp to + ramptime (float): The ramp time in seconds. + """ + + # Crazy stuff happens if the period is too small, e.g. the channel + # can jump to its max voltage + if ramptime <= 0.002: + ramptime = 0 + log.warning('Cancelled a ramp with a ramptime of ' + '{} s'.format(ramptime) + '. Voltage not changed.') + + offset = v_start + amplitude = setvoltage-v_start + + chanmssg = 'wav {} {} {} {}'.format(chan, fg, + amplitude, + offset) + + if chan in [syn[0] for syn in self._syncoutputs]: + sync = [syn[1] for syn in self._syncoutputs if syn[0] == chan][0] + sync_duration = 1000*self.channels[chan-1].sync_duration.get() + sync_delay = 1000*self.channels[chan-1].sync_delay.get() + self.write('syn {} {} {} {}'.format(sync, fg, + sync_delay, + sync_duration)) + + typedict = {'SINE': 1, 'SQUARE': 2, 'RAMP': 3} + typeval = typedict['RAMP'] + dutyval = 100 + # s -> ms + periodval = ramptime*1e3 + repval = 1 + funmssg = 'fun {} {} {} {} {}'.format(fg, + typeval, periodval, + dutyval, repval) + self.write(chanmssg) + self.write(funmssg) + + def get_MinMax_OutputVoltage(self, channel, vrange_int): + """ + This command returns a dictionary of the calibrated Min and Max output + voltages of 'channel' for the voltage given range (0,1) given by 'vrange_int' + """ + # For firmware 1.07 verbose mode and nn verbose mode give verbose result. So this is designed for verbose mode + if channel not in range(1, self.num_chans+1): + raise ValueError('Channel number must be 1-{}.'.format(self.num_chans)) + if vrange_int not in range(0, 2): + raise ValueError('Range must be 0 or 1.') + + self.write('rang {} {}'.format(channel, vrange_int)) + FW_str = self._write_response + return {'Min': float(FW_str.split('MIN:')[1].split('MAX')[0].strip()), + 'Max': float(FW_str.split('MAX:')[1].strip())} + + def write(self, cmd): + """ + QDac always returns something even from set commands, even when + verbose mode is off, so we'll override write to take this out + if you want to use this response, we put it in self._write_response + (but only for the very last write call) + + In this method we expect to read one termination char per command. As + commands are concatenated by `;` we count the number of concatenated + commands as count(';') + 1 e.g. 'wav 1 1 1 0;fun 2 1 100 1 1' is two + commands. Note that only the response of the last command will be + available in `_write_response` + + """ + + log.debug("Writing to instrument {}: {}".format(self.name, cmd)) + nr_bytes_written, ret_code = self.visa_handle.write(cmd) + self.check_error(ret_code) + for _ in range(cmd.count(';')+1): + self._write_response = self.visa_handle.read() + + def read(self): + return self.visa_handle.read() + + def _wait_and_clear(self, delay=0.5): + time.sleep(delay) + self.visa_handle.clear() + + def connect_message(self): + """ + Override of the standard Instrument class connect_message. + Usually, the response to `*IDN?` is printed. Here, the + software version is printed. + """ + self.visa_handle.write('status') + + log.info('Connected to QDac on {}, {}'.format(self._address, + self.visa_handle.read())) + + # take care of the rest of the output + for _ in range(self._output_n_lines): + self.visa_handle.read() + + def _get_firmware_version(self): + """ + Check if the "version" command reponds. If so we probbaly have a QDevil QDAC, + and the version number is returned. Otherwise 0.0 is returned. + """ + self.write('version') + FW_str = self._write_response + if (not ("Unrecognized command" in FW_str)) and ("Software Version: " in FW_str): + FW_version = float(self._write_response.replace("Software Version: ", "")) + else: + FW_version= 0.0 + return FW_version + + def _get_number_of_channels(self): + """ + Returns the number of channels for the instrument + """ + self.write('boardNum') + FW_str = self._write_response + return 8*int(FW_str.strip("numberOfBoards:")) + + def _get_vmin(self,chan): + """ + Returns the calibrated minimum output voltage for the channel, + for the current voltage output range + chan: 1-24 or 1-48 + """ + range_int = self.channels[chan-1].vrange.get_latest() + return self.vranges[chan][range_int]['Min'] + + def _get_vmax(self,chan): + """ + Returns the calibrated maximum output voltage for the channel, + for the current voltage output range + chan: 1-24 or 1-48 + """ + range_int = self.channels[chan-1].vrange.get_latest() + return self.vranges[chan][range_int]['Max'] + + def print_overview(self, update_currents=False): + """ + Pretty-prints the status of the QDac + """ + + self._get_status(readcurrents=update_currents) + + paramstoget = [['i', 'v'], ['irange', 'vrange']] + printdict = {'i': 'Current', 'v': 'Voltage', 'vrange': 'Voltage range', + 'irange': 'Current range'} + + returnmap = {'vrange': {1: '-1 V to 1 V', 0: '-10 V to 10 V'}, + 'irange': {0: '0 to 1 muA', 1: '0 to 100 muA'}} + + # Print the channels + for ii in range(self.num_chans): + line = 'Channel {} \n'.format(ii+1) + line += ' ' + for pp in paramstoget[0]: + param = getattr(self.channels[ii], pp) + line += printdict[pp] + line += ': {}'.format(param.get_latest()) + line += ' ({})'.format(param.unit) + line += '. ' + line += '\n ' + for pp in paramstoget[1]: + param = getattr(self.channels[ii], pp) + line += printdict[pp] + value = param.get_latest() + line += ': {}'.format(returnmap[pp][value]) + line += '. ' + print(line) From 977fb04f863a4ea8d4420119b4a7d14871892cbe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Anders=20Ku=CC=88hle?= Date: Mon, 24 Jun 2019 13:36:10 +0200 Subject: [PATCH 02/32] Fix: Fix bugs discovered by automatic review --- .../drivers/QDevil/QDevil_QDac_channels.py | 57 ++++++++----------- 1 file changed, 25 insertions(+), 32 deletions(-) diff --git a/qcodes_contrib_drivers/drivers/QDevil/QDevil_QDac_channels.py b/qcodes_contrib_drivers/drivers/QDevil/QDevil_QDac_channels.py index 69d0d26f4..e786bc894 100644 --- a/qcodes_contrib_drivers/drivers/QDevil/QDevil_QDac_channels.py +++ b/qcodes_contrib_drivers/drivers/QDevil/QDevil_QDac_channels.py @@ -5,7 +5,6 @@ import time import visa import logging -import numpy as np from datetime import datetime from functools import partial @@ -46,7 +45,7 @@ def __init__(self, parent, name, channum): self.add_parameter('v', label='Channel {} voltage'.format(channum), - unit='V', + unit='V', set_cmd=partial(self._parent._set_voltage, channum), get_cmd='set {}'.format(channum), get_parser=float, @@ -224,11 +223,11 @@ def __init__(self, No QDevil QDAC detected or the firmware version is obsolete. QCoDeS only supports version 1.07 or newer. In that case, please Contact info@qdevil.com for an update. - ''') + ''') self.num_chans = self._get_number_of_channels() - num_boards = int(self.num_chans/8) - self._output_n_lines = self.num_chans + 2 + num_boards = int(self.num_chans/8) + self._output_n_lines = self.num_chans + 2 # Assigned slopes. Entries will eventually be [chan, slope] self._slopes = [] @@ -277,7 +276,7 @@ def __init__(self, self.write('wav {} 0 1 0'.format(chan)) # Get all calibrated min/max output values, before swithcing to verbose off mode - self.vranges = {} + self.vranges = {} for chan in self.chan_range: self.vranges.update({chan: {0:self.get_MinMax_OutputVoltage(chan, 0), 1:self.get_MinMax_OutputVoltage(chan, 1)}}) @@ -355,12 +354,12 @@ def _set_voltage(self, chan, v_set): def _set_vrange(self, chan, switchint): """ - set_cmd for changing the voltage range: chXX_vrange + set_cmd for changing the voltage range: chXX_vrange The switchint is an integer. 0: high range, 1: low range. The output voltage should be set to zero before changin the range, - eventhough we try to keep the voltage output steady if within range. + eventhough we try to keep the voltage output steady if within range. A spike will occur if the voltage is not zero. """ @@ -368,26 +367,26 @@ def _set_vrange(self, chan, switchint): old_irange = self.channels[chan-1].irange.get_latest() if xor(old, switchint): - # In the QDevil QDAC version 1.0 we can not have the high current range and the low voltage range at the same time + # In the QDevil QDAC version 1.0 we can not have the high current range and the low voltage range at the same time if (old_irange == 1) and (switchint == 1): self.channels[chan-1].irange.set(0) log.warning('QDAC can not be in low voltage range and high current range at the same time. Setting current range to low.') voltageparam = self.channels[chan-1].v volmessage = 'vol {} {}'.format(chan, switchint) - # Avoid making coltage jumps when switching range by compensating for design flaws FW1.07. Spikes are unavoidable. + # Avoid making coltage jumps when switching range by compensating for design flaws FW1.07. Spikes are unavoidable. # Even if a channel is a slow channel it might not yet have be assigned to a generator and therefore - # Therefore it is not enough to test if the channel is a slope channel. That is why we ask the channel + # Therefore it is not enough to test if the channel is a slope channel. That is why we ask the channel self.write('wav {}'.format(chan)) FW_str = self._write_response gen,_,_ = FW_str.split(',') - if int(gen) > 0: + if int(gen) > 0: volmessage +=';wav {} {} {:.6f}'.format(chan, int(gen), 0) #The amplitude must be set to zero to avoid potential overflow. Assuming that vrange is not changed during a ramp else: volmessage +=';set {}'.format(chan) - oldvoltage = voltageparam.get() # The only way to be sure about the actual output is to read it, as the buffered value is not updated after ramping - if (switchint == 0): + oldvoltage = voltageparam.get() # The only way to be sure about the actual output is to read it, as the buffered value is not updated after ramping + if (switchint == 0): newvoltage = oldvoltage else: if oldvoltage > self.vranges[chan][switchint]['Max']: @@ -398,14 +397,14 @@ def _set_vrange(self, chan, switchint): log.warning('Voltage is outside the bounds of the low range and is therefore clipped.') else: newvoltage = oldvoltage - volmessage += ' {:.6f}'.format(newvoltage) + volmessage += ' {:.6f}'.format(newvoltage) self.write(volmessage) voltageparam.vals = vals.Numbers(self.vranges[chan][switchint]['Min'],self.vranges[chan][switchint]['Max']) voltageparam._save_val(newvoltage) def _set_vvals_to_current_range(self): """ - Command for setting all "v" limits ("vals") of all channels to the + Command for setting all "v" limits ("vals") of all channels to the actual calibrated output limits for the range each individual channel is currently in. """ for chan in range(1,self.num_chans+1): @@ -414,7 +413,7 @@ def _set_vvals_to_current_range(self): def _set_irange(self, chan, switchint): """ - set_cmd for changing the current measurement range: chXX_irange + set_cmd for changing the current measurement range: chXX_irange The switchint is an integer. 1: high range, 0: low range. @@ -526,7 +525,7 @@ def _get_status(self, readcurrents=False): irange_trans = {'hi cur': 1, 'lo cur': 0} - # In the original driver from QDEV it was necessary to have vrange before v in the dict + # In the original driver from QDEV it was necessary to have vrange before v in the dict vals_dict = OrderedDict() vals_dict.update({'vrange': ('vrange', self.voltage_range_status[vrange.strip()])}) @@ -711,7 +710,7 @@ def _rampvoltage(self, chan, fg, v_start, setvoltage, ramptime): def get_MinMax_OutputVoltage(self, channel, vrange_int): """ This command returns a dictionary of the calibrated Min and Max output - voltages of 'channel' for the voltage given range (0,1) given by 'vrange_int' + voltages of 'channel' for the voltage given range (0,1) given by 'vrange_int' """ # For firmware 1.07 verbose mode and nn verbose mode give verbose result. So this is designed for verbose mode if channel not in range(1, self.num_chans+1): @@ -740,7 +739,7 @@ def write(self, cmd): """ log.debug("Writing to instrument {}: {}".format(self.name, cmd)) - nr_bytes_written, ret_code = self.visa_handle.write(cmd) + _, ret_code = self.visa_handle.write(cmd) self.check_error(ret_code) for _ in range(cmd.count(';')+1): self._write_response = self.visa_handle.read() @@ -752,20 +751,14 @@ def _wait_and_clear(self, delay=0.5): time.sleep(delay) self.visa_handle.clear() - def connect_message(self): + def connect_message(self, idn_param='IDN', begin_time=None): """ Override of the standard Instrument class connect_message. Usually, the response to `*IDN?` is printed. Here, the software version is printed. """ - self.visa_handle.write('status') - - log.info('Connected to QDac on {}, {}'.format(self._address, - self.visa_handle.read())) - - # take care of the rest of the output - for _ in range(self._output_n_lines): - self.visa_handle.read() + self.visa_handle.write('version') + print('Connected to QDac on {}, {}'.format(self._address,self.visa_handle.read())) def _get_firmware_version(self): """ @@ -790,10 +783,10 @@ def _get_number_of_channels(self): def _get_vmin(self,chan): """ - Returns the calibrated minimum output voltage for the channel, - for the current voltage output range + Returns the calibrated minimum output voltage for the channel, + for the current voltage output range chan: 1-24 or 1-48 - """ + """ range_int = self.channels[chan-1].vrange.get_latest() return self.vranges[chan][range_int]['Min'] From 67303a18912504a1ab6eb7bfd04302a3ff431eca Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Anders=20Ku=CC=88hle?= Date: Mon, 24 Jun 2019 14:10:05 +0200 Subject: [PATCH 03/32] Fix: Remove trailing white space --- .../drivers/QDevil/QDevil_QDac_channels.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/qcodes_contrib_drivers/drivers/QDevil/QDevil_QDac_channels.py b/qcodes_contrib_drivers/drivers/QDevil/QDevil_QDac_channels.py index e786bc894..e608c4033 100644 --- a/qcodes_contrib_drivers/drivers/QDevil/QDevil_QDac_channels.py +++ b/qcodes_contrib_drivers/drivers/QDevil/QDevil_QDac_channels.py @@ -404,7 +404,7 @@ def _set_vrange(self, chan, switchint): def _set_vvals_to_current_range(self): """ - Command for setting all "v" limits ("vals") of all channels to the + Command for setting all 'v' limits ('vals') of all channels to the actual calibrated output limits for the range each individual channel is currently in. """ for chan in range(1,self.num_chans+1): @@ -447,14 +447,14 @@ def _num_verbose(self, s): def _current_parser(self, s): """ - parser for chXX_i parameter (converts from uA to A) + Parser for chXX_i parameter (converts from uA to A) """ return 1e-6*self._num_verbose(s) def read_state(self, chan, param): """ - specific routine for reading items out of status response + Specific routine for reading items out of status response Args: chan (int): The 1-indexed channel number @@ -717,7 +717,7 @@ def get_MinMax_OutputVoltage(self, channel, vrange_int): raise ValueError('Channel number must be 1-{}.'.format(self.num_chans)) if vrange_int not in range(0, 2): raise ValueError('Range must be 0 or 1.') - + self.write('rang {} {}'.format(channel, vrange_int)) FW_str = self._write_response return {'Min': float(FW_str.split('MIN:')[1].split('MAX')[0].strip()), @@ -758,7 +758,7 @@ def connect_message(self, idn_param='IDN', begin_time=None): software version is printed. """ self.visa_handle.write('version') - print('Connected to QDac on {}, {}'.format(self._address,self.visa_handle.read())) + self.log('Connected to QDac on {}, {}'.format(self._address,self.visa_handle.read())) def _get_firmware_version(self): """ @@ -792,10 +792,10 @@ def _get_vmin(self,chan): def _get_vmax(self,chan): """ - Returns the calibrated maximum output voltage for the channel, - for the current voltage output range + Returns the calibrated maximum output voltage for the channel, + for the current voltage output range chan: 1-24 or 1-48 - """ + """ range_int = self.channels[chan-1].vrange.get_latest() return self.vranges[chan][range_int]['Max'] From 9365a747a7933bff4b587e460f95d8a00bd1d03d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Anders=20Ku=CC=88hle?= Date: Mon, 24 Jun 2019 14:23:12 +0200 Subject: [PATCH 04/32] Fix: Remove more trailing whitespace and fix typo --- .../drivers/QDevil/QDevil_QDac_channels.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/qcodes_contrib_drivers/drivers/QDevil/QDevil_QDac_channels.py b/qcodes_contrib_drivers/drivers/QDevil/QDevil_QDac_channels.py index e608c4033..8b1c64a83 100644 --- a/qcodes_contrib_drivers/drivers/QDevil/QDevil_QDac_channels.py +++ b/qcodes_contrib_drivers/drivers/QDevil/QDevil_QDac_channels.py @@ -424,7 +424,7 @@ def _set_irange(self, chan, switchint): """ old = self.channels[chan-1].irange.get_latest() old_vrange = self.channels[chan-1].vrange.get_latest() - + if xor(old, switchint): if (old_vrange == 1) and (switchint == 1): log.warning(''' @@ -758,7 +758,7 @@ def connect_message(self, idn_param='IDN', begin_time=None): software version is printed. """ self.visa_handle.write('version') - self.log('Connected to QDac on {}, {}'.format(self._address,self.visa_handle.read())) + log.info('Connected to QDac on {}, {}'.format(self._address,self.visa_handle.read())) def _get_firmware_version(self): """ @@ -776,7 +776,7 @@ def _get_firmware_version(self): def _get_number_of_channels(self): """ Returns the number of channels for the instrument - """ + """ self.write('boardNum') FW_str = self._write_response return 8*int(FW_str.strip("numberOfBoards:")) From d3e7924a92c60d4d72ae4994f25eb614ba165abd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Anders=20Ku=CC=88hle?= Date: Mon, 24 Jun 2019 14:32:49 +0200 Subject: [PATCH 05/32] Fix: Remove more trailing white space --- .../drivers/QDevil/QDevil_QDac_channels.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/qcodes_contrib_drivers/drivers/QDevil/QDevil_QDac_channels.py b/qcodes_contrib_drivers/drivers/QDevil/QDevil_QDac_channels.py index 8b1c64a83..d3b28131a 100644 --- a/qcodes_contrib_drivers/drivers/QDevil/QDevil_QDac_channels.py +++ b/qcodes_contrib_drivers/drivers/QDevil/QDevil_QDac_channels.py @@ -224,7 +224,7 @@ def __init__(self, QCoDeS only supports version 1.07 or newer. In that case, please Contact info@qdevil.com for an update. ''') - + self.num_chans = self._get_number_of_channels() num_boards = int(self.num_chans/8) self._output_n_lines = self.num_chans + 2 @@ -329,7 +329,7 @@ def _set_voltage(self, chan, v_set): elif v_set < self.vranges[chan][atten]['Min']: v_set = self.vranges[chan][atten]['Min'] outofrange = True - + if outofrange: log.warning('Requested voltage outside reachable range.' + ' Setting voltage on channel ' + @@ -362,7 +362,7 @@ def _set_vrange(self, chan, switchint): eventhough we try to keep the voltage output steady if within range. A spike will occur if the voltage is not zero. """ - + old = self.channels[chan-1].vrange.get_latest() old_irange = self.channels[chan-1].irange.get_latest() @@ -401,7 +401,7 @@ def _set_vrange(self, chan, switchint): self.write(volmessage) voltageparam.vals = vals.Numbers(self.vranges[chan][switchint]['Min'],self.vranges[chan][switchint]['Max']) voltageparam._save_val(newvoltage) - + def _set_vvals_to_current_range(self): """ Command for setting all 'v' limits ('vals') of all channels to the @@ -410,7 +410,7 @@ def _set_vvals_to_current_range(self): for chan in range(1,self.num_chans+1): vrange = self.channels[chan-1].vrange.get_latest() self.channels[chan-1].v.vals = vals.Numbers(self.vranges[chan][vrange]['Min'],self.vranges[chan][vrange]['Max']) - + def _set_irange(self, chan, switchint): """ set_cmd for changing the current measurement range: chXX_irange From b5ebd99d771241a8c99659e62061cc3436c59721 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Anders=20Ku=CC=88hle?= Date: Tue, 19 Nov 2019 14:49:20 +0100 Subject: [PATCH 06/32] feat: Driver for the QDevil QDAC MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Driver for the QDevil QDAC. It is based on the “QDev\Qdac_channels.py” driver but is different in many ways as it leverages several of the latest developments from QDevil, and requires a QDAC from QDevil with at least firmware 1.07. The improvements include auto configuration, output calibration, and synchronised multi-channel ramping. --- .../drivers/QDevil/QDAC1.py | 919 ++++++++++++++++++ .../drivers/QDevil/QDevil_QDac_channels.py | 833 ---------------- 2 files changed, 919 insertions(+), 833 deletions(-) create mode 100644 qcodes_contrib_drivers/drivers/QDevil/QDAC1.py delete mode 100644 qcodes_contrib_drivers/drivers/QDevil/QDevil_QDac_channels.py diff --git a/qcodes_contrib_drivers/drivers/QDevil/QDAC1.py b/qcodes_contrib_drivers/drivers/QDevil/QDAC1.py new file mode 100644 index 000000000..2f8366ddd --- /dev/null +++ b/qcodes_contrib_drivers/drivers/QDevil/QDAC1.py @@ -0,0 +1,919 @@ +# QCoDeS driver for the QDevil QDAC using channels +# Adapted by QDevil from "qdev\QDac_channels.py" in +# the instrument drivers package +# Version 2.0 QDevil 2019-11-19 + +import time +import visa +import logging + +from datetime import datetime +from functools import partial +from operator import xor +from collections import OrderedDict + +from qcodes.instrument.channel import InstrumentChannel, ChannelList +from qcodes.instrument.channel import MultiChannelInstrumentParameter +from qcodes.instrument.visa import VisaInstrument +from qcodes.utils import validators as vals + +log = logging.getLogger(__name__) + +mode_dict = {0: "V-high / I-high", 1: "V-high / I-low", 2: "V-low / I-low"} + + +class Mode: + vhigh_ihigh = 0 + vhigh_ilow = 1 + vlow_ilow = 2 + + +class Waveform: + # Enum-like class defining the built-in waveform types + sine = 1 + square = 2 + triangle = 3 + staircase = 4 + all = [sine, square, triangle, staircase] + + +class Generator(): + fg = 0 + t_end = 9.9e9 + + def __init__(self, generator_number): + self.fg = generator_number + + +class QDacChannel(InstrumentChannel): + """ + A single output channel of the QDac. + + Exposes chan.v, chan.i, chan.mode, chan.slope, + chan.sync, chan.sync_delay, chan.sync_duration + Always set v to zero before changing range + """ + + _CHANNEL_VALIDATION = vals.Numbers(1, 48) + + def __init__(self, parent, name, channum): + """ + Args: + parent (Instrument): The instrument to which the channel belongs. + name (str): The name of the channel + channum (int): The number of the channel (1-48) + """ + super().__init__(parent, name) + + # Validate the channel + self._CHANNEL_VALIDATION.validate(channum) + + # Add the parameters + self.add_parameter(name='v', + label='Channel {} voltage'.format(channum), + unit='V', + set_cmd=partial(self._parent._set_voltage, channum), + get_cmd='set {}'.format(channum), + get_parser=float, + # Initial values. Updated on init and during + # operation: + vals=vals.Numbers(-9.99, 9.99) + ) + + self.add_parameter(name='mode', + label='Channel {} mode.'.format(channum), + set_cmd=partial(self._parent._set_mode, channum), + vals=vals.Enum(0, 1, 2) + ) + + self.add_parameter(name='i', + label='Channel {} current'.format(channum), + get_cmd='get {}'.format(channum), + unit='A', + get_parser=self._parent._current_parser + ) + + self.add_parameter(name='slope', + label='Channel {} slope'.format(channum), + unit='V/s', + set_cmd=partial(self._parent._setslope, channum), + get_cmd=partial(self._parent._getslope, channum), + vals=vals.MultiType(vals.Enum('Inf'), + vals.Numbers(1e-3, 1000)) + ) + + self.add_parameter(name='sync', + label='Channel {} sync output'.format(channum), + set_cmd=partial(self._parent._setsync, channum), + get_cmd=partial(self._parent._getsync, channum), + vals=vals.Ints(0, 5) + ) + + self.add_parameter(name='sync_delay', + label='Channel {} sync pulse delay'.format(channum), + unit='s', + get_cmd=None, set_cmd=None, + initial_value=0 + ) + + self.add_parameter( + name='sync_duration', + label='Channel {} sync pulse duration'.format(channum), + unit='s', + get_cmd=None, set_cmd=None, + initial_value=0.01 + ) + + def snapshot_base(self, update=False, params_to_skip_update=None): + update_currents = self._parent._update_currents and update + if update and not self._parent._get_status_performed: + self._parent._get_status(readcurrents=update_currents) + # call get_status rather than getting the status individually for + # each parameter. This is only done if _get_status_performed is False + # this is used to signal that the parent has already called it and + # no need to repeat. + if params_to_skip_update is None: + params_to_skip_update = ('v', 'i', 'mode') + snap = super().snapshot_base( + update=update, + params_to_skip_update=params_to_skip_update) + return snap + + +class QDacMultiChannelParameter(MultiChannelInstrumentParameter): + """ + The class to be returned by __getattr__ of the ChannelList. Here customised + for fast multi-readout of voltages. + """ + def __init__(self, channels, param_name, *args, **kwargs): + super().__init__(channels, param_name, *args, **kwargs) + + def get_raw(self): + """ + Return a tuple containing the data from each of the channels in the + list. + """ + # For voltages, we can do something slightly faster than the naive + # approach + + if self._param_name == 'v': + qdac = self._channels[0]._parent + qdac._get_status(readcurrents=False) + output = tuple(chan.parameters[self._param_name].get_latest() + for chan in self._channels) + else: + output = tuple(chan.parameters[self._param_name].get() + for chan in self._channels) + + return output + + +class QDac(VisaInstrument): + """ + Channelised driver for the QDevil QDAC voltage source + Exposes channels, temperature sensors and calibration output, + and 'ramp_voltages' for mutil channel ramping. + Tested with Firmware Version: 1.07 + + The driver assumes that the instrument is ALWAYS in verbose mode OFF + and sets this as part of the initialization, so please do not change this. + """ + + # set nonzero value (seconds) to accept older status when reading settings + max_status_age = 1 + + def __init__(self, + name, + address, + update_currents=False, + **kwargs): + """ + Instantiates the instrument. + + Args: + name (str): The instrument name used by qcodes + address (str): The VISA name of the resource + update_currents (bool): Whether to query all channels for their + current sensor value on startup, which takes about 0.5 sec + per channel. Default: False. + + Returns: + QDac object + """ + + super().__init__(name, address, **kwargs) + handle = self.visa_handle + self._get_status_performed = False + + # This is the baud rate on power-up. It can be changed later but + # you must start out with this value. + handle.baud_rate = 480600 + handle.parity = visa.constants.Parity(0) + handle.data_bits = 8 + self.set_terminator('\n') + # TODO: do we want a method for write termination too? + handle.write_termination = '\n' + # TODO: do we need a query delay for robust operation? + self._write_response = '' + firmware_version = self._get_firmware_version() + if firmware_version < 1.07: + log.warning("Firmware version: {}".format(firmware_version)) + raise RuntimeError(''' + No QDevil QDAC detected or the firmware version is obsolete. + This driver only supports version 1.07 or newer. Please + contact info@qdevil.com for a firmware update. + ''') + + self.num_chans = self._get_number_of_channels() + num_boards = int(self.num_chans/8) + self._output_n_lines = self.num_chans + 2 + + # Assigned slopes. Entries will eventually be {chan: slope} + self._slopes = {} + # Function generators and triggers (used in ramping) + self._fgs = set(range(1, 9)) + self._assigned_fgs = {} # {chan: fg} + self._trigs = set(range(1, 10)) + self._assigned_triggers = {} # {fg: trigger} + # Sync channels + self._syncoutputs = {} # {chan: syncoutput} + + self._chan_range = range(1, 1 + self.num_chans) + self.channel_validator = vals.Ints(1, self.num_chans) + + channels = ChannelList(self, "Channels", QDacChannel, + snapshotable=False, + multichan_paramclass=QDacMultiChannelParameter) + + for i in self._chan_range: + channel = QDacChannel(self, 'chan{:02}'.format(i), i) + channels.append(channel) + self.add_submodule('ch{:02}'.format(i), channel) + channels.lock() + self.add_submodule('channels', channels) + + for board in range(num_boards): + for sensor in range(3): + label = 'Board {}, Temperature {}'.format(board, sensor) + self.add_parameter(name='temp{}_{}'.format(board, sensor), + label=label, + unit='C', + get_cmd='tem {} {}'.format(board, sensor), + get_parser=self._num_verbose) + + self.add_parameter(name='cal', + set_cmd='cal {}', + vals=self.channel_validator) + + self.add_parameter(name='verbose', + label=label, + val_mapping={True: 1, False: 0}) + + # Initialise the instrument, all channels DC (unbind func. generators) + for chan in self._chan_range: + # Note: this call may change the voltage on the channel + self.write('wav {} 0 1 0'.format(chan)) + + # Get all calibrated min/max output values, before switching to + # verbose off mode + self.vranges = {} + for chan in self._chan_range: + self.vranges.update( + {chan: {0: self._get_minmax_outputvoltage(chan, 0), + 1: self._get_minmax_outputvoltage(chan, 1)}}) + + self.write('ver 0') + self.verbose._save_val(False) + self.connect_message() + log.info('[*] Querying all channels for voltages and currents...') + self._get_status(readcurrents=update_currents) + self._update_currents = update_currents + # Set "v" parameter limits to actual calibrated limits + self._set_vvals_to_current_range() + log.info('[+] Done') + + def snapshot_base(self, update=False, params_to_skip_update=None): + update_currents = self._update_currents and update + if update: + self._get_status(readcurrents=update_currents) + self._get_status_performed = True + # call get_status rather than getting the status individually for + # each parameter. We set _get_status_performed to True + # to indicate that each update channel does not need to call this + # function as opposed to when snapshot is called on an individual + # channel + snap = super().snapshot_base( + update=update, + params_to_skip_update=params_to_skip_update) + self._get_status_performed = False + return snap + + ######################### + # Channel gets/sets + ######################### + + def _set_voltage(self, chan, v_set): + """ + set_cmd for the chXX_v parameter + + Args: + chan (int): The 1-indexed channel number + v_set (float): The target voltage + + If a finite slope has been assigned, a function generator will + ramp the voltage. + """ + + slope = self._slopes.get(chan, None) + if slope: + # We need .get and not get_latest in case a ramp was interrupted + v_start = self.channels[chan-1].v.get() + duration = abs(v_set-v_start)/slope + log.info('Slope: {}, time: {}'.format(slope, duration)) + # SYNCing happens inside ramp_voltages + self.ramp_voltages([chan], [v_start], [v_set], duration) + else: + self.write('wav {} 0 0 0;set {} {:.6f}'.format(chan, chan, v_set)) + + # Helper for _set_mode + def _clipto(self, value, min, max, errmsg=""): + if value > max: + log.warning(errmsg) + return max + elif value < min: + log.warning(errmsg) + return min + else: + return value + + # Helper for _set_mode. It is not possible ot say if the channel is + # connected to a generator, so we need to ask. + def _wav_or_set_msg(self, chan, new_voltage): + self.write('wav {}'.format(chan)) + FW_str = self._write_response + gen, _, _ = FW_str.split(',') + if int(gen) > 0: + # The amplitude must be set to zero to avoid potential overflow. + # Assuming that voltage range is not changed during a ramp + return 'wav {} {} {:.6f} {:.6f}'\ + .format(chan, int(gen), 0, new_voltage) + else: + return 'set {} {:.6f}'.format(chan, new_voltage) + + def _set_mode(self, chan, new_mode): + """ + set_cmd for the QDAC's mode (combined voltage and current sense range) + """ + # Mode 0: (10V, 100uA), Mode 1: (10V, 1uA), Mode 2: (1V, 1uA) + ivrangedict = { + 0: {"vol": 0, "cur": 1}, + 1: {"vol": 0, "cur": 0}, + 2: {"vol": 1, "cur": 0}} + # switchint = (0,1,2, + # 3,4,5, + # 6,7,8) + + old_mode = self.channels[chan-1].mode.get_latest() + new_vrange = ivrangedict[new_mode]["vol"] + new_irange = ivrangedict[new_mode]["cur"] + switchint = int(3*old_mode+new_mode) + message = '' + + if old_mode == new_mode: + return + + # If the voltage range is going to change we have to take care of + # setting the voltage after the switch, and therefore read them first + # Only the current range has to change + if switchint in [1, 3]: + message = 'cur {} {}'.format(chan, new_irange) + # The voltage range has to change + else: # switchint in [2,5,6,7] (7 is max) + if switchint == 2: + message = 'cur {} {};'.format(chan, new_irange) + old_voltage = self.channels[chan-1].v.get() + new_voltage = self._clipto( + # Actually, for 6,7 new_voltage = old_voltage, always. + old_voltage, self.vranges[chan][new_vrange]['Min'], + self.vranges[chan][new_vrange]['Max'], + "Voltage is outside the bounds of the new voltage range" + " and is therefore clipped.") + message += 'vol {} {};'.format(chan, new_vrange) + message += self._wav_or_set_msg(chan, new_voltage) + if switchint == 6: + message += ';cur {} {}'.format(chan, new_irange) + self.channels[chan-1].v.vals = vals.Numbers( + self.vranges[chan][new_vrange]['Min'], + self.vranges[chan][new_vrange]['Max']) + self.channels[chan-1].v._save_val(new_voltage) + self.write(message) + + def _vrange(self, range): + if range == Mode.vlow_ilow: + return 1 + else: + return 0 + + def _irange(self, range): + if range == Mode.vhigh_ihigh: + return 1 + else: + return 0 + + def _set_vvals_to_current_range(self): + """ + Command for setting all 'v' limits ('vals') of all channels to the + actual calibrated output limits for the range each individual channel + is currently in. + """ + for chan in range(1, self.num_chans+1): + vrange = self._vrange(self.channels[chan-1].mode()) + self.channels[chan-1].v.vals = vals.Numbers( + self.vranges[chan][vrange]['Min'], + self.vranges[chan][vrange]['Max']) + + def _num_verbose(self, s): + """ + Turns a return value from the QDac into a number. + If the QDac is in verbose mode, this involves stripping off the + value descriptor. + """ + if self.verbose.get_latest(): + s = s.split[': '][-1] + return float(s) + + def _current_parser(self, s): + """ + Parser for chXX_i parameter (converts from uA to A) + """ + return 1e-6*self._num_verbose(s) + + def _get_status(self, readcurrents=False): + """ + Function to query the instrument and get the status of all channels. + Takes a while to finish. + + The `status` call generates 27 or 51 lines of output. Send the command + and read the first one, which is the software version line + the full output looks like: + Software Version: 1.07\r\n + Channel\tOut V\t\tVoltage range\tCurrent range\n + \n + 8\t 0.000000\t\tX 1\t\tpA\n + 7\t 0.000000\t\tX 1\t\tpA\n + ... (all 24/48 channels like this) + (no termination afterward besides the \n ending the last channel) + """ + irange_trans = {'hi cur': 1, 'lo cur': 0} + vrange_trans = {'X 1': 0, 'X 0.1': 1} + vi_range_dict = {0: {0: 1, 1: 0}, 1: {0: 2, 1: 3}} + # Status call + version_line = self.ask('status') + + if version_line.startswith('Software Version: '): + self.version = version_line.strip().split(': ')[1] + else: + self._wait_and_clear() + raise ValueError('unrecognized version line: ' + version_line) + + header_line = self.read() + headers = header_line.lower().strip('\r\n').split('\t') + expected_headers = ['channel', 'out v', '', 'voltage range', + 'current range'] + if headers != expected_headers: + raise ValueError('unrecognized header line: ' + header_line) + + chans_left = set(self._chan_range) + while chans_left: + line = self.read().strip() + if not line: + continue + chanstr, v, _, vrange, _, irange = line.split('\t') + chan = int(chanstr) + vrange_int = int(vrange_trans[vrange.strip()]) + irange_int = int(irange_trans[irange.strip()]) + mode = vi_range_dict[vrange_int][irange_int] + self.channels[chan-1].mode._save_val(mode) + self.channels[chan-1].v._save_val(float(v)) + chans_left.remove(chan) + + if readcurrents: + for chan in self._chan_range: + self.channels[chan-1].i.get() + + def _setsync(self, chan, sync): + """ + set_cmd for the chXX_sync parameter. + + Args: + chan (int): The channel number (1-48 or 1-24) + sync (int): The associated sync output (1-3 on 24 ch units + or 1-5 on 48 ch units). 0 means 'unassign' + """ + + if chan not in range(1, self.num_chans+1): + raise ValueError( + 'Channel number must be 1-{}.'.format(self.num_chans)) + + if sync == 0: + oldsync = self.channels[chan-1].sync.get_latest() + # try to remove the sync from internal bookkeeping + self._syncoutputs.pop(chan, None) + # free the previously assigned sync + if oldsync is not None: + self.write('syn {} 0 0 0'.format(oldsync)) + return + + if sync in self._syncoutputs.values(): + oldchan = [ch for ch, sy in self._syncoutputs.items() + if sy == sync] + self._syncoutputs.pop(oldchan[0], None) + self._syncoutputs[chan] = sync + return + + def _getsync(self, chan): + """ + get_cmd of the chXX_sync parameter + """ + return self._syncoutputs.get(chan, 0) + + def _setslope(self, chan, slope): + """ + set_cmd for the chXX_slope parameter, the maximum slope of a channel. + With a finite slope the channel will be ramped using a generator. + + Args: + chan (int): The channel number (1-24 or 1-48) + slope (Union[float, str]): The slope in V/s. + Write 'Inf' to release the channelas slope channel and to release + the associated function generator. The output rise time will now + only depend on the analog electronics. + """ + if chan not in range(1, self.num_chans+1): + raise ValueError( + 'Channel number must be 1-{}.'.format(self.num_chans)) + + if slope == 'Inf': + # Set the channel in DC mode + v_set = self.channels[chan-1].v.get() + self.write('set {} {:.6f};wav {} 0 0 0'.format(chan, v_set, chan)) + + # Now release the function generator and fg trigger (if possible) + try: + fg = self._assigned_fgs[chan] + self._assigned_fgs[chan].t_end = 0 + self._assigned_triggers.pop(fg) + except KeyError: + pass + + # Remove a sync output, if one was assigned + if chan in self._syncoutputs: + self.channels[chan-1].sync.set(0) + # Now clear the assigned slope + self._slopes.pop(chan, None) + else: + self._slopes[chan] = slope + return + + def _getslope(self, chan): + """ + get_cmd of the chXX_slope parameter + """ + return self._slopes.get(chan, 'Inf') + + def print_slopes(self): + """ + Print the finite slopes assigned to channels, sorted by channel number + """ + for chan, slope in sorted(self._slopes.items()): + print('Channel {}, slope: {} (V/s)'.format(chan, slope)) + + def _get_minmax_outputvoltage(self, channel, vrange_int): + """ + Returns a dictionary of the calibrated Min and Max output + voltages of 'channel' for the voltage given range (0,1) given by + 'vrange_int' + """ + # For firmware 1.07 verbose mode and nn verbose mode give verbose + # result, So this is designed for verbose mode + if channel not in range(1, self.num_chans+1): + raise ValueError( + 'Channel number must be 1-{}.'.format(self.num_chans)) + if vrange_int not in range(0, 2): + raise ValueError('Range must be 0 or 1.') + + self.write('rang {} {}'.format(channel, vrange_int)) + FW_str = self._write_response + return {'Min': float(FW_str.split('MIN:')[1].split('MAX')[0].strip()), + 'Max': float(FW_str.split('MAX:')[1].strip())} + + def write(self, cmd): + """ + QDac always returns something even from set commands, even when + verbose mode is off, so we'll override write to take this out + if you want to use this response, we put it in self._write_response + (but only for the very last write call) + + In this method we expect to read one termination char per command. As + commands are concatenated by `;` we count the number of concatenated + commands as count(';') + 1 e.g. 'wav 1 1 1 0;fun 2 1 100 1 1' is two + commands. Note that only the response of the last command will be + available in `_write_response` + """ + + log.debug("Writing to instrument {}: {}".format(self.name, cmd)) + _, ret_code = self.visa_handle.write(cmd) + self.check_error(ret_code) + for _ in range(cmd.count(';')+1): + self._write_response = self.visa_handle.read() + + def read(self): + return self.visa_handle.read() + + def _wait_and_clear(self, delay=0.5): + time.sleep(delay) + self.visa_handle.clear() + + def connect_message(self, idn_param='IDN', begin_time=None): + """ + Override of the standard Instrument class connect_message. + Usually, the response to `*IDN?` is printed. Here, the + software version is printed. + """ + self.visa_handle.write('version') + log.info('Connected to QDAC on {}, {}'.format( + self._address, self.visa_handle.read())) + + def _get_firmware_version(self): + """ + Check if the "version" command reponds. If so we probbaly have a QDevil + QDAC, and the version number is returned. Otherwise 0.0 is returned. + """ + self.write('version') + FW_str = self._write_response + if ((not ("Unrecognized command" in FW_str)) + and ("Software Version: " in FW_str)): + FW_version = float( + self._write_response.replace("Software Version: ", "")) + else: + FW_version = 0.0 + return FW_version + + def _get_number_of_channels(self): + """ + Returns the number of channels for the instrument + """ + self.write('boardNum') + FW_str = self._write_response + return 8*int(FW_str.strip("numberOfBoards:")) + + def print_overview(self, update_currents=False): + """ + Pretty-prints the status of the QDac + """ + + self._get_status(readcurrents=update_currents) + + paramstoget = [['v', 'i'], ['mode']] # Second item to be translated + printdict = {'v': 'Voltage', 'i': 'Current', 'mode': 'Mode'} + returnmap = {'mode': mode_dict} + + # Print the channels + for ii in range(self.num_chans): + line = 'Channel {} \n'.format(ii+1) + line += ' ' + for pp in paramstoget[0]: + param = getattr(self.channels[ii], pp) + line += printdict[pp] + line += ': {}'.format(param.get_latest()) + line += ' ({})'.format(param.unit) + line += '. ' + line += '\n ' + for pp in paramstoget[1]: + param = getattr(self.channels[ii], pp) + line += printdict[pp] + value = param.get_latest() + line += ': {}'.format(returnmap[pp][value]) + line += '. ' + print(line) + + def _get_functiongenerator(self, chan): + """ + Function for getting a free generator (of 8 available) for a channel. + Used as helper function for ramp_voltages, but may also be used if the + user wants to use a function generator for something else. + If there are no free generators this function will wait for up to + FGS_TIMEOUT for one to be ready. + + To mark a function generator as available for others set + self._assigned_fgs[chan].t_end = 0 + + Args: + chan: (1..24/48) the channel for which a function generator is + requested. + """ + FGS_TIMEOUT = 2 # Max time to wait for next available generator + + if len(self._assigned_fgs) < 8: + fg = min(self._fgs.difference( + set([g.fg for g in self._assigned_fgs.values()]))) + self._assigned_fgs[chan] = Generator(fg) + else: + # If no available fgs, see if one is soon to be ready + time_now = time.time() + available_fgs_chans = [] + fgs_t_end_ok = [g.t_end for chan, g + in self._assigned_fgs.items() + if g.t_end < time_now+FGS_TIMEOUT] + if len(fgs_t_end_ok) > 0: + first_ready_t = min(fgs_t_end_ok) + available_fgs_chans = [chan for chan, g + in self._assigned_fgs.items() + if g.t_end == first_ready_t] + if first_ready_t > time_now: + log.warning(''' + Trying to ramp more channels than there are generators.\n + Waiting for ramp generator to be released''') + time.sleep(first_ready_t - time_now) + + if len(available_fgs_chans) > 0: + oldchan = available_fgs_chans[0] + fg = self._assigned_fgs[oldchan].fg + self._assigned_fgs.pop(oldchan) + self._assigned_fgs[chan] = Generator(fg) + # Set the old channel in DC mode + v_set = self.channels[oldchan-1].v.get_latest() + self.write('set {} {:.6f};wav {} 0 0 0' + .format(oldchan, v_set, oldchan)) + else: + raise RuntimeError(''' + Trying to ramp more channels than there are generators + available. Please insert delays allowing channels to finish + ramping before trying to ramp other channels, or reduce the + number of ramped channels. Or increase FGS_TIMEOUT.''') + return fg + + def ramp_voltages(self, channellist, v_startlist, v_endlist, ramptime): + """ + Function for smoothly ramping one channel or more channels + simultaneously (max. 8). This is a shallow interface to + ramp_voltages_2D. Function generators and triggers are + are assigned automatically. + + Args: + channellist: List (int) of channels to be ramped (1 indexed)\n + v_startlist: List (int) of voltages to ramp from. + MAY BE EMPTY. But if provided, time is saved by + NOT reading the present values from the instrument. + + v_endlist: List (int) of voltages to ramp to.\n + ramptime: Total ramp time in seconds (min. 0.002). Has + to be an integer number of 0.001 secs).\n + Returns: + Estimated time of the excecution of the 2D scan. + + NOTE: This function returns as the ramps are started. So you need + to wait for 'ramptime' until measuring.... + """ + + if ramptime < 0.002: + log.warning('Ramp time too short: {:.3f} s. Ramp time set to 2 ms.' + .format(ramptime)) + ramptime = 0.002 + steps = int(ramptime*1000) + return self.ramp_voltages_2D( + slow_chans=[], slow_vstart=[], slow_vend=[], + fast_chans=channellist, fast_vstart=v_startlist, + fast_vend=v_endlist, step_length=0.001, + slow_steps=1, fast_steps=steps) + + def ramp_voltages_2D(self, slow_chans, slow_vstart, slow_vend, + fast_chans, fast_vstart, fast_vend, + step_length, slow_steps, fast_steps): + """ + Function for smoothly ramping two channel groups simultaneously with + one slow (x) and one fast (y) group. used by 'ramp_voltages' where x is + empty. Function generators and triggers are assigned automatically. + + Args: + slow_chans: List (int) of channels to be ramped (1 indexed) in + the slow-group\n + slow_vstart: List (int) of voltages to ramp from in the + slow-group. + MAY BE EMPTY. But if provided, time is saved by NOT + reading the present values from the instrument.\n + slow_vend: list (int) of voltages to ramp to in the slow-group. + + slow_steps: (int) number of steps in the x direction.\n + fast_chans: List (int) of channels to be ramped (1 indexed) in + the fast-group.\n + fast_vstart: List (int) of voltages to ramp from in the + fast-group. + MAY BE EMPTY. But if provided, time is saved by NOT + reading the present values from the instrument.\n + fast_vend: list (int) of voltages to ramp to in the fast-group. + + fast_steps: (int) number of steps in the fast direction.\n + step_length: (float) Time spent at each step in seconds + (min. 0.001) multiple of 1 ms.\n + Returns: + Estimated time of the excecution of the 2D scan.\n + NOTE: This function returns as the ramps are started. + """ + channellist = [*slow_chans, *fast_chans] + v_endlist = [*slow_vend, *fast_vend] + v_startlist = [*slow_vstart, *fast_vstart] + step_length_ms = int(step_length*1000) + + if step_length < 0.001: + log.warning('step_length too short: {:.3f} s. \nstep_length set to' + .format(step_length_ms) + ' minimum (1ms).') + step_length_ms = 1 + + if any([ch in fast_chans for ch in slow_chans]): + raise ValueError( + 'Channel cannot be in both slow_chans and fast_chans!') + + no_channels = len(channellist) + if no_channels != len(v_endlist): + raise ValueError( + 'Number of channels and number of voltages inconsistent!') + + for chan in channellist: + if chan not in range(1, self.num_chans+1): + raise ValueError( + 'Channel number must be 1-{}.'.format(self.num_chans)) + if not (chan in self._assigned_fgs): + self._get_functiongenerator(chan) + + # Voltage validation + for i in range(no_channels): + self.channels[channellist[i]-1].v.validate(v_endlist[i]) + if v_startlist: + for i in range(no_channels): + self.channels[channellist[i]-1].v.validate(v_startlist[i]) + + # Get start voltages if not provided + if not slow_vstart: + slow_vstart = [self.channels[ch-1].v.get() for ch in slow_chans] + + if not fast_vstart: + fast_vstart = [self.channels[ch-1].v.get() for ch in fast_chans] + + v_startlist = [*slow_vstart, *fast_vstart] + if no_channels != len(v_startlist): + raise ValueError( + 'Number of start voltages do not match number of channels!') + + # Find trigger not aleady uses (avoid starting other + # channels/function generators) + if no_channels == 1: + trigger = 0 + else: + trigger = int(min(self._trigs.difference( + set(self._assigned_triggers.values())))) + + # Make sure any sync outputs are configured + for chan in channellist: + if chan in self._syncoutputs: + sync = self._syncoutputs[chan] + sync_duration = 1000*self.channels[chan-1].sync_duration.get() + sync_delay = 1000*self.channels[chan-1].sync_delay.get() + self.write('syn {} {} {} {}'.format( + sync, self._assigned_fgs[chan].fg, + sync_delay, sync_duration)) + + # Now program the channel amplitudes and function generators + msg = '' + for i in range(no_channels): + amplitude = v_endlist[i]-v_startlist[i] + ch = channellist[i] + fg = self._assigned_fgs[ch].fg + if trigger > 0: # Trigger 0 is not a trigger + self._assigned_triggers[fg] = trigger + msg += 'wav {} {} {} {}'.format(ch, fg, amplitude, v_startlist[i]) + # using staircase = function 4 + nsteps = slow_steps if ch in slow_chans else fast_steps + repetitions = slow_steps if ch in fast_chans else 1 + + delay = step_length_ms \ + if ch in fast_chans else fast_steps*step_length_ms + msg += ';fun {} {} {} {} {} {};'.format( + fg, Waveform.staircase, delay, int(nsteps), + repetitions, trigger) + # Update latest values to ramp end values + # (actually not necessary when called from _set_voltage) + self.channels[ch-1].v._save_val(v_endlist[i]) + self.write(msg[:-1]) # last semicolon is stripped + + # Fire trigger to start generators simultaneously, saving communication + # time by not using triggers for single channel ramping + if trigger > 0: + self.write('trig {}'.format(trigger)) + + # Update fgs dict so that we know when the ramp is supposed to end + time_ramp = slow_steps * fast_steps * step_length_ms / 1000 + time_end = time_ramp + time.time() + for i in range(no_channels): + self._assigned_fgs[chan].t_end = time_end + return time_ramp diff --git a/qcodes_contrib_drivers/drivers/QDevil/QDevil_QDac_channels.py b/qcodes_contrib_drivers/drivers/QDevil/QDevil_QDac_channels.py deleted file mode 100644 index d3b28131a..000000000 --- a/qcodes_contrib_drivers/drivers/QDevil/QDevil_QDac_channels.py +++ /dev/null @@ -1,833 +0,0 @@ -# QCoDeS driver for the QDevil QDAC using channels -# Adapted by QDevil from "qdev\QDac_channels.py" in the instrument drivers package - kudos to QDEV and the Microsoft quantum computing consortium for making that -# AK QDevil 2019-06-24 - -import time -import visa -import logging - -from datetime import datetime -from functools import partial -from operator import xor -from collections import OrderedDict - -from qcodes.instrument.channel import InstrumentChannel, ChannelList -from qcodes.instrument.channel import MultiChannelInstrumentParameter -from qcodes.instrument.visa import VisaInstrument -from qcodes.utils import validators as vals - -log = logging.getLogger(__name__) - -class QDacChannel(InstrumentChannel): - """ - A single output channel of the QDac. - - Exposes chan.v, chan.vrange, chan.vmin, chan.vmax, chan.slope, chan.i, chan.irange, chan.slope - Always set v to zero before changing vrange or irange - """ - - _CHANNEL_VALIDATION = vals.Numbers(1, 48) - - def __init__(self, parent, name, channum): - """ - Args: - parent (Instrument): The instrument to which the channel is - attached. - name (str): The name of the channel - channum (int): The number of the channel in question (1-48) - """ - super().__init__(parent, name) - - # Validate the channel - self._CHANNEL_VALIDATION.validate(channum) - - # Add the parameters - - self.add_parameter('v', - label='Channel {} voltage'.format(channum), - unit='V', - set_cmd=partial(self._parent._set_voltage, channum), - get_cmd='set {}'.format(channum), - get_parser=float, - vals=vals.Numbers(-9.99, 9.99) # Initial values. Are updated at initialisation and when vrange is changed - ) - - self.add_parameter('vrange', - label='Channel {} range.'.format(channum), - set_cmd=partial(self._parent._set_vrange, channum), - get_cmd='vol {}'.format(channum), - get_parser=int, - vals=vals.Enum(0, 1) - ) - - self.add_parameter('vmin', - label='Channel {} min voltage'.format(channum), - unit='V', - set_cmd=None, - get_cmd=partial(self._parent._get_vmin, channum), - get_parser=float, - ) - - self.add_parameter('vmax', - label='Channel {} max voltage'.format(channum), - unit='V', - set_cmd=None, - get_cmd=partial(self._parent._get_vmax, channum), - get_parser=float, - ) - - self.add_parameter('i', - label='Channel {} current'.format(channum), - get_cmd='get {}'.format(channum), - unit='A', - get_parser=self._parent._current_parser - ) - - self.add_parameter('irange', - label='Channel {} irange'.format(channum), - set_cmd=partial(self._parent._set_irange, channum), - get_cmd='cur {}'.format(channum), - get_parser=int - ) - - self.add_parameter('slope', - label='Channel {} slope'.format(channum), - unit='V/s', - set_cmd=partial(self._parent._setslope, channum), - get_cmd=partial(self._parent._getslope, channum), - vals=vals.MultiType(vals.Enum('Inf'), - vals.Numbers(1e-3, 100)) - ) - - self.add_parameter('sync', - label='Channel {} sync output'.format(channum), - set_cmd=partial(self._parent._setsync, channum), - get_cmd=partial(self._parent._getsync, channum), - vals=vals.Ints(0, 5) - ) - - self.add_parameter(name='sync_delay', - label='Channel {} sync pulse delay'.format(channum), - unit='s', - get_cmd=None, set_cmd=None, - initial_value=0 - ) - - self.add_parameter(name='sync_duration', - label='Channel {} sync pulse duration'.format(channum), - unit='s', - get_cmd=None, set_cmd=None, - initial_value=0.01 - ) - - def snapshot_base(self, update=False, params_to_skip_update=None): - update_currents = self._parent._update_currents and update - if update and not self._parent._get_status_performed: - self._parent._get_status(readcurrents=update_currents) - # call get_status rather than getting the status individually for - # each parameter. This is only done if _get_status_performed is False - # this is used to signal that the parent has already called it and - # no need to repeat. - if params_to_skip_update is None: - params_to_skip_update = ('v', 'i', 'irange', 'vrange') - snap = super().snapshot_base(update=update, - params_to_skip_update=params_to_skip_update) - return snap - - -class QDacMultiChannelParameter(MultiChannelInstrumentParameter): - """ - The class to be returned by __getattr__ of the ChannelList. Here customised - for fast multi-readout of voltages. - """ - def __init__(self, channels, param_name, *args, **kwargs): - super().__init__(channels, param_name, *args, **kwargs) - - def get_raw(self): - """ - Return a tuple containing the data from each of the channels in the - list. - """ - # For voltages, we can do something slightly faster than the naive - # approach - - if self._param_name == 'v': - qdac = self._channels[0]._parent - qdac._get_status(readcurrents=False) - output = tuple(chan.parameters[self._param_name].get_latest() - for chan in self._channels) - else: - output = tuple(chan.parameters[self._param_name].get() - for chan in self._channels) - - return output - - -class QDac(VisaInstrument): - """ - Channelised driver for the QDevil QDAC voltage source - - Based on "PM10091-7 QDAC SW1.07.pdf" - Tested with Firmware Version: 1.07 - - The driver assumes that the instrument is ALWAYS in verbose mode OFF - and sets this as part of the initialization. - """ - - # Range factors for QDevil QDAC 1.0 - voltage_range_status = {'X 1': 0, 'X 0.1': 1} - - # set nonzero value (seconds) to accept older status when reading settings - max_status_age = 1 - - def __init__(self, - name, - address, - num_chans=48, - update_currents=True, - **kwargs): - """ - Instantiates the instrument. - - Args: - name (str): The instrument name used by qcodes - address (str): The VISA name of the resource - num_chans (int): Number of channels to assign. Default: 48 (Legacy, not required) - update_currents (bool): Whether to query all channels for their - current current value on startup. Default: True. - - Returns: - QDac object - """ - - super().__init__(name, address, **kwargs) - handle = self.visa_handle - self._get_status_performed = False - - # This is the baud rate on power-up. It can be changed later but - # you must start out with this value. - handle.baud_rate = 480600 - handle.parity = visa.constants.Parity(0) - handle.data_bits = 8 - self.set_terminator('\n') - # TODO: do we want a method for write termination too? - handle.write_termination = '\n' - # TODO: do we need a query delay for robust operation? - self._write_response = '' - - # isQDevilQDAC = self._check_if_qdevilqdac() - firmware_version = self._get_firmware_version() - if firmware_version < 1.07: - log.warning("Firmware version: {}".format(firmware_version)) - raise RuntimeError( ''' - No QDevil QDAC detected or the firmware version is obsolete. - QCoDeS only supports version 1.07 or newer. In that case, - please Contact info@qdevil.com for an update. - ''') - - self.num_chans = self._get_number_of_channels() - num_boards = int(self.num_chans/8) - self._output_n_lines = self.num_chans + 2 - - # Assigned slopes. Entries will eventually be [chan, slope] - self._slopes = [] - # Function generators (used in _set_voltage) - self._fgs = set(range(1, 9)) - self._assigned_fgs = {} # {chan: fg} - # Sync channels - self._syncoutputs = [] # Entries: [chan, syncchannel] - - self.chan_range = range(1, 1 + self.num_chans) - self.channel_validator = vals.Ints(1, self.num_chans) - - channels = ChannelList(self, "Channels", QDacChannel, - snapshotable=False, - multichan_paramclass=QDacMultiChannelParameter) - - for i in self.chan_range: - channel = QDacChannel(self, 'chan{:02}'.format(i), i) - channels.append(channel) - # Should raise valueerror if name is invalid (silently fails now) - self.add_submodule('ch{:02}'.format(i), channel) - channels.lock() - self.add_submodule('channels', channels) - - for board in range(num_boards): - for sensor in range(3): - label = 'Board {}, Temperature {}'.format(board, sensor) - self.add_parameter(name='temp{}_{}'.format(board, sensor), - label=label, - unit='C', - get_cmd='tem {} {}'.format(board, sensor), - get_parser=self._num_verbose) - - self.add_parameter(name='cal', - set_cmd='cal {}', - vals=self.channel_validator) - # TO-DO: maybe it's too dangerous to have this settable. - # And perhaps ON is a better verbose mode default? - self.add_parameter(name='verbose', - set_cmd='ver {}', - val_mapping={True: 1, False: 0}) - - # Initialise the instrument, all channels DC (unbind func. generators) - for chan in self.chan_range: - # Note: this call does NOT change the voltage on the channel - self.write('wav {} 0 1 0'.format(chan)) - - # Get all calibrated min/max output values, before swithcing to verbose off mode - self.vranges = {} - for chan in self.chan_range: - self.vranges.update({chan: {0:self.get_MinMax_OutputVoltage(chan, 0), 1:self.get_MinMax_OutputVoltage(chan, 1)}}) - - self.verbose.set(False) - self.connect_message() - log.info('[*] Querying all channels for voltages and currents...') - self._get_status(readcurrents=update_currents) - self._update_currents = update_currents - # Set "v" parameter limits to actual calibrated limits - self._set_vvals_to_current_range() - log.info('[+] Done') - - def snapshot_base(self, update=False, params_to_skip_update=None): - update_currents = self._update_currents and update - if update: - self._get_status(readcurrents=update_currents) - self._get_status_performed = True - # call get_status rather than getting the status individually for - # each parameter. We set _get_status_performed to True - # to indicate that each update channel does not need to call this - # function as opposed to when snapshot is called on an individual - # channel - snap = super().snapshot_base(update=update, - params_to_skip_update=params_to_skip_update) - self._get_status_performed = False - return snap - - ######################### - # Channel gets/sets - ######################### - - def _set_voltage(self, chan, v_set): - """ - set_cmd for the chXX_v parameter - - Args: - chan (int): The 1-indexed channel number - v_set (float): The target voltage - - If a finite slope has been assigned, we assign a function generator to - ramp the voltage. - """ - # validation - atten = self.channels[chan-1].vrange.get_latest() - - outofrange = False - if v_set > self.vranges[chan][atten]['Max']: - v_set = self.vranges[chan][atten]['Max'] - outofrange = True - elif v_set < self.vranges[chan][atten]['Min']: - v_set = self.vranges[chan][atten]['Min'] - outofrange = True - - if outofrange: - log.warning('Requested voltage outside reachable range.' + - ' Setting voltage on channel ' + - '{} to {} V. '.format(chan, v_set) + - 'Note: _get_latest not up to date') - - slopechans = [sl[0] for sl in self._slopes] - if chan in slopechans: - slope = [sl[1] for sl in self._slopes if sl[0] == chan][0] - # find and assign fg - fg = min(self._fgs.difference(set(self._assigned_fgs.values()))) - self._assigned_fgs[chan] = fg - # We need .get and not get_latest in case a ramp was interrupted - v_start = self.channels[chan-1].v.get() - time = abs(v_set-v_start)/slope - log.info('Slope: {}, time: {}'.format(slope, time)) - # Attenuation compensation and syncing - # happen inside _rampvoltage - self._rampvoltage(chan, fg, v_start, v_set, time) - else: - self.write('wav {} 0 0 0;set {} {:.6f}'.format(chan, chan, v_set)) - - def _set_vrange(self, chan, switchint): - """ - set_cmd for changing the voltage range: chXX_vrange - - The switchint is an integer. 0: high range, 1: low range. - - The output voltage should be set to zero before changin the range, - eventhough we try to keep the voltage output steady if within range. - A spike will occur if the voltage is not zero. - """ - - old = self.channels[chan-1].vrange.get_latest() - old_irange = self.channels[chan-1].irange.get_latest() - - if xor(old, switchint): - # In the QDevil QDAC version 1.0 we can not have the high current range and the low voltage range at the same time - if (old_irange == 1) and (switchint == 1): - self.channels[chan-1].irange.set(0) - log.warning('QDAC can not be in low voltage range and high current range at the same time. Setting current range to low.') - voltageparam = self.channels[chan-1].v - volmessage = 'vol {} {}'.format(chan, switchint) - - # Avoid making coltage jumps when switching range by compensating for design flaws FW1.07. Spikes are unavoidable. - # Even if a channel is a slow channel it might not yet have be assigned to a generator and therefore - # Therefore it is not enough to test if the channel is a slope channel. That is why we ask the channel - self.write('wav {}'.format(chan)) - FW_str = self._write_response - gen,_,_ = FW_str.split(',') - if int(gen) > 0: - volmessage +=';wav {} {} {:.6f}'.format(chan, int(gen), 0) #The amplitude must be set to zero to avoid potential overflow. Assuming that vrange is not changed during a ramp - else: - volmessage +=';set {}'.format(chan) - - oldvoltage = voltageparam.get() # The only way to be sure about the actual output is to read it, as the buffered value is not updated after ramping - if (switchint == 0): - newvoltage = oldvoltage - else: - if oldvoltage > self.vranges[chan][switchint]['Max']: - newvoltage = self.vranges[chan][switchint]['Max'] - log.warning('Voltage is outside the bounds of the low range and is therefore clipped.') - elif oldvoltage < self.vranges[chan][switchint]['Min']: - newvoltage = self.vranges[chan][switchint]['Min'] - log.warning('Voltage is outside the bounds of the low range and is therefore clipped.') - else: - newvoltage = oldvoltage - volmessage += ' {:.6f}'.format(newvoltage) - self.write(volmessage) - voltageparam.vals = vals.Numbers(self.vranges[chan][switchint]['Min'],self.vranges[chan][switchint]['Max']) - voltageparam._save_val(newvoltage) - - def _set_vvals_to_current_range(self): - """ - Command for setting all 'v' limits ('vals') of all channels to the - actual calibrated output limits for the range each individual channel is currently in. - """ - for chan in range(1,self.num_chans+1): - vrange = self.channels[chan-1].vrange.get_latest() - self.channels[chan-1].v.vals = vals.Numbers(self.vranges[chan][vrange]['Min'],self.vranges[chan][vrange]['Max']) - - def _set_irange(self, chan, switchint): - """ - set_cmd for changing the current measurement range: chXX_irange - - The switchint is an integer. 1: high range, 0: low range. - - The output voltage should be set to zero before changing the range, - otherwise a spike will occur on the output. When switching from high - current range to low current range the volage (if not zero) may drop if - the current draw is exceeding the max current in the low current range. - """ - old = self.channels[chan-1].irange.get_latest() - old_vrange = self.channels[chan-1].vrange.get_latest() - - if xor(old, switchint): - if (old_vrange == 1) and (switchint == 1): - log.warning(''' - QDAC can not be in high current range and low voltage range and at the same time. - Current range has NOT been changed and _get_latest is NOT up to date - ''') - self.channels[chan-1].irange._save_val(0) - else: - self.write('cur {} {}'.format(chan, switchint)) - - def _num_verbose(self, s): - """ - Turns a return value from the QDac into a number. - If the QDac is in verbose mode, this involves stripping off the - value descriptor. - """ - if self.verbose.get_latest(): - s = s.split[': '][-1] - return float(s) - - def _current_parser(self, s): - """ - Parser for chXX_i parameter (converts from uA to A) - """ - return 1e-6*self._num_verbose(s) - - - def read_state(self, chan, param): - """ - Specific routine for reading items out of status response - - Args: - chan (int): The 1-indexed channel number - param (str): The parameter in question, e.g. 'v' or 'vrange' - """ - if chan not in self.chan_range: - raise ValueError('valid channels are {}'.format(self.chan_range)) - valid_params = ('v', 'vrange', 'irange') - if param not in valid_params: - raise ValueError( - 'read_state valid params are {}'.format(valid_params)) - self._get_status(readcurrents=False) - - value = getattr(self.channels[chan-1], param).get_latest() - - returnmap = {'vrange': {1: 1, 10: 0}, - 'irange': {0: '1 muA', 1: '100 muA'}} - - if 'range' in param: - value = returnmap[param][value] - - return value - - def _get_status(self, readcurrents=False): - """ - Function to query the instrument and get the status of all channels. - Takes a while to finish. - - The `status` call generates 27 or 51 lines of output. Send the command and - read the first one, which is the software version line - the full output looks like: - Software Version: 1.07\r\n - Channel\tOut V\t\tVoltage range\tCurrent range\n - \n - 8\t 0.000000\t\tX 1\t\tpA\n - 7\t 0.000000\t\tX 1\t\tpA\n - ... (all 24/48 channels like this in a somewhat peculiar order) - (no termination afterward besides the \n ending the last channel) - returns a list of dicts [{v, vrange, irange}] - NOTE - channels are 1-based, but the return is a list, so of course - 0-based, ie chan1 is out[0] - """ - - # Status call - version_line = self.ask('status') - - if version_line.startswith('Software Version: '): - self.version = version_line.strip().split(': ')[1] - else: - self._wait_and_clear() - raise ValueError('unrecognized version line: ' + version_line) - - header_line = self.read() - headers = header_line.lower().strip('\r\n').split('\t') - expected_headers = ['channel', 'out v', '', 'voltage range', - 'current range'] - if headers != expected_headers: - raise ValueError('unrecognized header line: ' + header_line) - - chans = [{} for _ in self.chan_range] - chans_left = set(self.chan_range) - while chans_left: - line = self.read().strip() - if not line: - continue - chanstr, v, _, vrange, _, irange = line.split('\t') - chan = int(chanstr) - - irange_trans = {'hi cur': 1, 'lo cur': 0} - - # In the original driver from QDEV it was necessary to have vrange before v in the dict - vals_dict = OrderedDict() - vals_dict.update({'vrange': ('vrange', - self.voltage_range_status[vrange.strip()])}) - vals_dict.update({'irange': ('irange', irange_trans[irange])}) - vals_dict.update({'v': ('v', float(v))}) - chans[chan - 1] = vals_dict - - for param in vals_dict: - value = vals_dict[param][1] - getattr(self.channels[chan-1], param)._save_val(value) - chans_left.remove(chan) - - if readcurrents: - for chan in range(1, self.num_chans+1): - param = self.channels[chan-1].i - param._save_val(param.get()) - - self._status = chans - self._status_ts = datetime.now() - return chans - - def _setsync(self, chan, sync): - """ - set_cmd for the chXX_sync parameter. - - Args: - chan (int): The channel number (1-48 or 1-24) - sync (int): The associated sync output (1-3 on 24 ch units or 1-5 on 48 ch units). 0 means 'unassign' - """ - - if chan not in range(1, self.num_chans+1): - raise ValueError('Channel number must be 1-{}.'.format(self.num_chans)) - - if sync == 0: - # try to remove the sync from internal bookkeeping - try: - sc = self._syncoutputs - to_remove = [sc.index(syn) for syn in sc if syn[0] == chan][0] - self._syncoutputs.remove(sc[to_remove]) - except IndexError: - pass - # free the previously assigned sync - oldsync = self.channels[chan-1].sync.get_latest() - if oldsync is not None: - self.write('syn {} 0 0 0'.format(oldsync)) - return - - if sync in [syn[1] for syn in self._syncoutputs]: - oldchan = [syn[0] for syn in self._syncoutputs if syn[1] == sync][0] - self._syncoutputs.remove([oldchan, sync]) - - if chan in [syn[0] for syn in self._syncoutputs]: - oldsyn = [syn[1] for syn in self._syncoutputs if syn[0] == chan][0] - self._syncoutputs[self._syncoutputs.index([chan, oldsyn])] = [chan, - sync] - return - - self._syncoutputs.append([chan, sync]) - return - - def _getsync(self, chan): - """ - get_cmd of the chXX_sync parameter - """ - if chan in [syn[0] for syn in self._syncoutputs]: - sync = [syn[1] for syn in self._syncoutputs if syn[0] == chan][0] - return sync - else: - return 0 - - def _setslope(self, chan, slope): - """ - set_cmd for the chXX_slope parameter, the maximum slope of a channel. - - Args: - chan (int): The channel number (1-24 or 1-48) - slope (Union[float, str]): The slope in V/s. Write 'Inf' to allow - arbitraryly small rise times. - """ - if chan not in range(1, self.num_chans+1): - raise ValueError('Channel number must be 1-{}.'.format(self.num_chans)) - - if slope == 'Inf': - self.write('wav {} 0 0 0'.format(chan)) - - # Now clear the assigned slope and function generator (if possible) - try: - self._assigned_fgs.pop(chan) - except KeyError: - pass - # Remove a sync output, if one was assigned - syncchans = [syn[0] for syn in self._syncoutputs] - if chan in syncchans: - self.channels[chan-1].sync.set(0) - try: - sls = self._slopes - to_remove = [sls.index(sl) for sl in sls if sl[0] == chan][0] - self._slopes.remove(sls[to_remove]) - return - # If the value was already 'Inf', the channel was not - # in the list and nothing happens - except IndexError: - return - - if chan in [sl[0] for sl in self._slopes]: - oldslope = [sl[1] for sl in self._slopes if sl[0] == chan][0] - self._slopes[self._slopes.index([chan, oldslope])] = [chan, slope] - return - - if len(self._slopes) >= 8: - rampchans = ', '.join([str(c[0]) for c in self._slopes]) - raise ValueError('Can not assign finite slope to more than ' + - "8 channels. Assign 'Inf' to at least one of " + - 'the following channels: {}'.format(rampchans)) - - self._slopes.append([chan, slope]) - return - - def _getslope(self, chan): - """ - get_cmd of the chXX_slope parameter - """ - if chan in [sl[0] for sl in self._slopes]: - slope = [sl[1] for sl in self._slopes if sl[0] == chan][0] - return slope - else: - return 'Inf' - - def printslopes(self): - """ - Print the finite slopes assigned to channels - """ - for sl in self._slopes: - print('Channel {}, slope: {} (V/s)'.format(sl[0], sl[1])) - - def _rampvoltage(self, chan, fg, v_start, setvoltage, ramptime): - """ - Smoothly ramp the voltage of a channel by the means of a function - generator. Helper function used by _set_voltage. - - Args: - chan (int): The channel number (counting from 1) - fg (int): The function generator (counting from 1) - setvoltage (float): The voltage to ramp to - ramptime (float): The ramp time in seconds. - """ - - # Crazy stuff happens if the period is too small, e.g. the channel - # can jump to its max voltage - if ramptime <= 0.002: - ramptime = 0 - log.warning('Cancelled a ramp with a ramptime of ' - '{} s'.format(ramptime) + '. Voltage not changed.') - - offset = v_start - amplitude = setvoltage-v_start - - chanmssg = 'wav {} {} {} {}'.format(chan, fg, - amplitude, - offset) - - if chan in [syn[0] for syn in self._syncoutputs]: - sync = [syn[1] for syn in self._syncoutputs if syn[0] == chan][0] - sync_duration = 1000*self.channels[chan-1].sync_duration.get() - sync_delay = 1000*self.channels[chan-1].sync_delay.get() - self.write('syn {} {} {} {}'.format(sync, fg, - sync_delay, - sync_duration)) - - typedict = {'SINE': 1, 'SQUARE': 2, 'RAMP': 3} - typeval = typedict['RAMP'] - dutyval = 100 - # s -> ms - periodval = ramptime*1e3 - repval = 1 - funmssg = 'fun {} {} {} {} {}'.format(fg, - typeval, periodval, - dutyval, repval) - self.write(chanmssg) - self.write(funmssg) - - def get_MinMax_OutputVoltage(self, channel, vrange_int): - """ - This command returns a dictionary of the calibrated Min and Max output - voltages of 'channel' for the voltage given range (0,1) given by 'vrange_int' - """ - # For firmware 1.07 verbose mode and nn verbose mode give verbose result. So this is designed for verbose mode - if channel not in range(1, self.num_chans+1): - raise ValueError('Channel number must be 1-{}.'.format(self.num_chans)) - if vrange_int not in range(0, 2): - raise ValueError('Range must be 0 or 1.') - - self.write('rang {} {}'.format(channel, vrange_int)) - FW_str = self._write_response - return {'Min': float(FW_str.split('MIN:')[1].split('MAX')[0].strip()), - 'Max': float(FW_str.split('MAX:')[1].strip())} - - def write(self, cmd): - """ - QDac always returns something even from set commands, even when - verbose mode is off, so we'll override write to take this out - if you want to use this response, we put it in self._write_response - (but only for the very last write call) - - In this method we expect to read one termination char per command. As - commands are concatenated by `;` we count the number of concatenated - commands as count(';') + 1 e.g. 'wav 1 1 1 0;fun 2 1 100 1 1' is two - commands. Note that only the response of the last command will be - available in `_write_response` - - """ - - log.debug("Writing to instrument {}: {}".format(self.name, cmd)) - _, ret_code = self.visa_handle.write(cmd) - self.check_error(ret_code) - for _ in range(cmd.count(';')+1): - self._write_response = self.visa_handle.read() - - def read(self): - return self.visa_handle.read() - - def _wait_and_clear(self, delay=0.5): - time.sleep(delay) - self.visa_handle.clear() - - def connect_message(self, idn_param='IDN', begin_time=None): - """ - Override of the standard Instrument class connect_message. - Usually, the response to `*IDN?` is printed. Here, the - software version is printed. - """ - self.visa_handle.write('version') - log.info('Connected to QDac on {}, {}'.format(self._address,self.visa_handle.read())) - - def _get_firmware_version(self): - """ - Check if the "version" command reponds. If so we probbaly have a QDevil QDAC, - and the version number is returned. Otherwise 0.0 is returned. - """ - self.write('version') - FW_str = self._write_response - if (not ("Unrecognized command" in FW_str)) and ("Software Version: " in FW_str): - FW_version = float(self._write_response.replace("Software Version: ", "")) - else: - FW_version= 0.0 - return FW_version - - def _get_number_of_channels(self): - """ - Returns the number of channels for the instrument - """ - self.write('boardNum') - FW_str = self._write_response - return 8*int(FW_str.strip("numberOfBoards:")) - - def _get_vmin(self,chan): - """ - Returns the calibrated minimum output voltage for the channel, - for the current voltage output range - chan: 1-24 or 1-48 - """ - range_int = self.channels[chan-1].vrange.get_latest() - return self.vranges[chan][range_int]['Min'] - - def _get_vmax(self,chan): - """ - Returns the calibrated maximum output voltage for the channel, - for the current voltage output range - chan: 1-24 or 1-48 - """ - range_int = self.channels[chan-1].vrange.get_latest() - return self.vranges[chan][range_int]['Max'] - - def print_overview(self, update_currents=False): - """ - Pretty-prints the status of the QDac - """ - - self._get_status(readcurrents=update_currents) - - paramstoget = [['i', 'v'], ['irange', 'vrange']] - printdict = {'i': 'Current', 'v': 'Voltage', 'vrange': 'Voltage range', - 'irange': 'Current range'} - - returnmap = {'vrange': {1: '-1 V to 1 V', 0: '-10 V to 10 V'}, - 'irange': {0: '0 to 1 muA', 1: '0 to 100 muA'}} - - # Print the channels - for ii in range(self.num_chans): - line = 'Channel {} \n'.format(ii+1) - line += ' ' - for pp in paramstoget[0]: - param = getattr(self.channels[ii], pp) - line += printdict[pp] - line += ': {}'.format(param.get_latest()) - line += ' ({})'.format(param.unit) - line += '. ' - line += '\n ' - for pp in paramstoget[1]: - param = getattr(self.channels[ii], pp) - line += printdict[pp] - value = param.get_latest() - line += ': {}'.format(returnmap[pp][value]) - line += '. ' - print(line) From 54b2ea2bdd46f7053051ac5eff68ecd53e649c4c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Anders=20Ku=CC=88hle?= Date: Tue, 19 Nov 2019 14:53:29 +0100 Subject: [PATCH 07/32] Rename of jupyter notebook and enhanced vals check in driver --- .../drivers/QDevil/QDAC1.py | 26 ++++++++++++++++--- 1 file changed, 22 insertions(+), 4 deletions(-) diff --git a/qcodes_contrib_drivers/drivers/QDevil/QDAC1.py b/qcodes_contrib_drivers/drivers/QDevil/QDAC1.py index 2f8366ddd..1f5f409b4 100644 --- a/qcodes_contrib_drivers/drivers/QDevil/QDAC1.py +++ b/qcodes_contrib_drivers/drivers/QDevil/QDAC1.py @@ -99,7 +99,7 @@ def __init__(self, parent, name, channum): set_cmd=partial(self._parent._setslope, channum), get_cmd=partial(self._parent._getslope, channum), vals=vals.MultiType(vals.Enum('Inf'), - vals.Numbers(1e-3, 1000)) + vals.Numbers(1e-3, 10000)) ) self.add_parameter(name='sync', @@ -113,6 +113,7 @@ def __init__(self, parent, name, channum): label='Channel {} sync pulse delay'.format(channum), unit='s', get_cmd=None, set_cmd=None, + vals=vals.Numbers(0, 10000), initial_value=0 ) @@ -121,6 +122,7 @@ def __init__(self, parent, name, channum): label='Channel {} sync pulse duration'.format(channum), unit='s', get_cmd=None, set_cmd=None, + vals=vals.Numbers(0.001, 10000), initial_value=0.01 ) @@ -524,10 +526,19 @@ def _setsync(self, chan, sync): self.write('syn {} 0 0 0'.format(oldsync)) return - if sync in self._syncoutputs.values(): + # Make sure to clear hardware an _syncoutpus appropriately + if chan in self._syncoutputs: + # Changing SYNC port for a channel + oldsync = self.channels[chan-1].sync.get_latest() + if sync != oldsync: + self.write('syn {} 0 0 0'.format(oldsync)) + elif sync in self._syncoutputs.values(): + # Assigning an already used SYNC port to a different channel oldchan = [ch for ch, sy in self._syncoutputs.items() if sy == sync] self._syncoutputs.pop(oldchan[0], None) + self.write('syn {} 0 0 0'.format(sync)) + self._syncoutputs[chan] = sync return @@ -537,6 +548,13 @@ def _getsync(self, chan): """ return self._syncoutputs.get(chan, 0) + def print_syncs(self): + """ + Print assigned SYNC ports, sorted by channel number + """ + for chan, sync in sorted(self._syncoutputs.items()): + print('Channel {}, SYNC: {} (V/s)'.format(chan, sync)) + def _setslope(self, chan, slope): """ set_cmd for the chXX_slope parameter, the maximum slope of a channel. @@ -877,8 +895,8 @@ def ramp_voltages_2D(self, slow_chans, slow_vstart, slow_vend, for chan in channellist: if chan in self._syncoutputs: sync = self._syncoutputs[chan] - sync_duration = 1000*self.channels[chan-1].sync_duration.get() - sync_delay = 1000*self.channels[chan-1].sync_delay.get() + sync_duration = int(1000*self.channels[chan-1].sync_duration.get()) + sync_delay = int(1000*self.channels[chan-1].sync_delay.get()) self.write('syn {} {} {} {}'.format( sync, self._assigned_fgs[chan].fg, sync_delay, sync_duration)) From 776611e78ef47e9a0a0a9e3fb8cc9feaac0d9ffd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Anders=20Ku=CC=88hle?= Date: Thu, 28 Nov 2019 14:34:21 +0100 Subject: [PATCH 08/32] Added "nbsphinx": { "execute": "never" } to notebook example, so that it is not executed during automatic tests Removed unused imports from drivers Replaced parameter .get_latest by .cache and ._save_val by .cache.set --- .../drivers/QDevil/QDAC1.py | 32 ++++++++----------- 1 file changed, 14 insertions(+), 18 deletions(-) diff --git a/qcodes_contrib_drivers/drivers/QDevil/QDAC1.py b/qcodes_contrib_drivers/drivers/QDevil/QDAC1.py index 1f5f409b4..857d412f6 100644 --- a/qcodes_contrib_drivers/drivers/QDevil/QDAC1.py +++ b/qcodes_contrib_drivers/drivers/QDevil/QDAC1.py @@ -7,11 +7,7 @@ import visa import logging -from datetime import datetime from functools import partial -from operator import xor -from collections import OrderedDict - from qcodes.instrument.channel import InstrumentChannel, ChannelList from qcodes.instrument.channel import MultiChannelInstrumentParameter from qcodes.instrument.visa import VisaInstrument @@ -161,7 +157,7 @@ def get_raw(self): if self._param_name == 'v': qdac = self._channels[0]._parent qdac._get_status(readcurrents=False) - output = tuple(chan.parameters[self._param_name].get_latest() + output = tuple(chan.parameters[self._param_name].cache() for chan in self._channels) else: output = tuple(chan.parameters[self._param_name].get() @@ -285,7 +281,7 @@ def __init__(self, 1: self._get_minmax_outputvoltage(chan, 1)}}) self.write('ver 0') - self.verbose._save_val(False) + self.verbose.cache.set(False) self.connect_message() log.info('[*] Querying all channels for voltages and currents...') self._get_status(readcurrents=update_currents) @@ -328,7 +324,7 @@ def _set_voltage(self, chan, v_set): slope = self._slopes.get(chan, None) if slope: - # We need .get and not get_latest in case a ramp was interrupted + # We need .get and not cache/get_latest in case a ramp was interrupted v_start = self.channels[chan-1].v.get() duration = abs(v_set-v_start)/slope log.info('Slope: {}, time: {}'.format(slope, duration)) @@ -375,7 +371,7 @@ def _set_mode(self, chan, new_mode): # 3,4,5, # 6,7,8) - old_mode = self.channels[chan-1].mode.get_latest() + old_mode = self.channels[chan-1].mode.cache() new_vrange = ivrangedict[new_mode]["vol"] new_irange = ivrangedict[new_mode]["cur"] switchint = int(3*old_mode+new_mode) @@ -407,7 +403,7 @@ def _set_mode(self, chan, new_mode): self.channels[chan-1].v.vals = vals.Numbers( self.vranges[chan][new_vrange]['Min'], self.vranges[chan][new_vrange]['Max']) - self.channels[chan-1].v._save_val(new_voltage) + self.channels[chan-1].v.cache.set(new_voltage) self.write(message) def _vrange(self, range): @@ -440,7 +436,7 @@ def _num_verbose(self, s): If the QDac is in verbose mode, this involves stripping off the value descriptor. """ - if self.verbose.get_latest(): + if self.verbose.cache(): s = s.split[': '][-1] return float(s) @@ -495,8 +491,8 @@ def _get_status(self, readcurrents=False): vrange_int = int(vrange_trans[vrange.strip()]) irange_int = int(irange_trans[irange.strip()]) mode = vi_range_dict[vrange_int][irange_int] - self.channels[chan-1].mode._save_val(mode) - self.channels[chan-1].v._save_val(float(v)) + self.channels[chan-1].mode.cache.set(mode) + self.channels[chan-1].v.cache.set(float(v)) chans_left.remove(chan) if readcurrents: @@ -518,7 +514,7 @@ def _setsync(self, chan, sync): 'Channel number must be 1-{}.'.format(self.num_chans)) if sync == 0: - oldsync = self.channels[chan-1].sync.get_latest() + oldsync = self.channels[chan-1].sync.cache() # try to remove the sync from internal bookkeeping self._syncoutputs.pop(chan, None) # free the previously assigned sync @@ -529,7 +525,7 @@ def _setsync(self, chan, sync): # Make sure to clear hardware an _syncoutpus appropriately if chan in self._syncoutputs: # Changing SYNC port for a channel - oldsync = self.channels[chan-1].sync.get_latest() + oldsync = self.channels[chan-1].sync.cache() if sync != oldsync: self.write('syn {} 0 0 0'.format(oldsync)) elif sync in self._syncoutputs.values(): @@ -703,14 +699,14 @@ def print_overview(self, update_currents=False): for pp in paramstoget[0]: param = getattr(self.channels[ii], pp) line += printdict[pp] - line += ': {}'.format(param.get_latest()) + line += ': {}'.format(param.cache()) line += ' ({})'.format(param.unit) line += '. ' line += '\n ' for pp in paramstoget[1]: param = getattr(self.channels[ii], pp) line += printdict[pp] - value = param.get_latest() + value = param.cache() line += ': {}'.format(returnmap[pp][value]) line += '. ' print(line) @@ -760,7 +756,7 @@ def _get_functiongenerator(self, chan): self._assigned_fgs.pop(oldchan) self._assigned_fgs[chan] = Generator(fg) # Set the old channel in DC mode - v_set = self.channels[oldchan-1].v.get_latest() + v_set = self.channels[oldchan-1].v.cache() self.write('set {} {:.6f};wav {} 0 0 0' .format(oldchan, v_set, oldchan)) else: @@ -921,7 +917,7 @@ def ramp_voltages_2D(self, slow_chans, slow_vstart, slow_vend, repetitions, trigger) # Update latest values to ramp end values # (actually not necessary when called from _set_voltage) - self.channels[ch-1].v._save_val(v_endlist[i]) + self.channels[ch-1].v.cache.set(v_endlist[i]) self.write(msg[:-1]) # last semicolon is stripped # Fire trigger to start generators simultaneously, saving communication From ca31575e71e4ec1bbe2bec65e8308a042b249233 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Anders=20Ku=CC=88hle?= Date: Mon, 2 Dec 2019 13:29:46 +0100 Subject: [PATCH 09/32] style: Corrected various linting errors --- .../drivers/QDevil/QDAC1.py | 134 +++++++++--------- 1 file changed, 70 insertions(+), 64 deletions(-) diff --git a/qcodes_contrib_drivers/drivers/QDevil/QDAC1.py b/qcodes_contrib_drivers/drivers/QDevil/QDAC1.py index 857d412f6..519b10ad7 100644 --- a/qcodes_contrib_drivers/drivers/QDevil/QDAC1.py +++ b/qcodes_contrib_drivers/drivers/QDevil/QDAC1.py @@ -13,9 +13,9 @@ from qcodes.instrument.visa import VisaInstrument from qcodes.utils import validators as vals -log = logging.getLogger(__name__) +LOG = logging.getLogger(__name__) -mode_dict = {0: "V-high / I-high", 1: "V-high / I-low", 2: "V-low / I-low"} +MODE_DICT = {0: "V-high / I-high", 1: "V-high / I-low", 2: "V-low / I-low"} class Mode: @@ -41,6 +41,32 @@ def __init__(self, generator_number): self.fg = generator_number +# Helper functions for QDac._set_mode and for usability +def _clipto(value, min_, max_, errmsg=""): + if value > max_: + LOG.warning(errmsg) + return max_ + elif value < min_: + LOG.warning(errmsg) + return min_ + else: + return value + + +def _vrange(range_): + if range_ == Mode.vlow_ilow: + return 1 + else: + return 0 + + +def _irange(range_): + if range_ == Mode.vhigh_ihigh: + return 1 + else: + return 0 + + class QDacChannel(InstrumentChannel): """ A single output channel of the QDac. @@ -215,7 +241,7 @@ def __init__(self, self._write_response = '' firmware_version = self._get_firmware_version() if firmware_version < 1.07: - log.warning("Firmware version: {}".format(firmware_version)) + LOG.warning("Firmware version: {}".format(firmware_version)) raise RuntimeError(''' No QDevil QDAC detected or the firmware version is obsolete. This driver only supports version 1.07 or newer. Please @@ -283,12 +309,12 @@ def __init__(self, self.write('ver 0') self.verbose.cache.set(False) self.connect_message() - log.info('[*] Querying all channels for voltages and currents...') + LOG.info('[*] Querying all channels for voltages and currents...') self._get_status(readcurrents=update_currents) self._update_currents = update_currents # Set "v" parameter limits to actual calibrated limits self._set_vvals_to_current_range() - log.info('[+] Done') + LOG.info('[+] Done') def snapshot_base(self, update=False, params_to_skip_update=None): update_currents = self._update_currents and update @@ -324,32 +350,23 @@ def _set_voltage(self, chan, v_set): slope = self._slopes.get(chan, None) if slope: - # We need .get and not cache/get_latest in case a ramp was interrupted + # We need .get and not cache/get_latest in case a ramp + # was interrupted v_start = self.channels[chan-1].v.get() duration = abs(v_set-v_start)/slope - log.info('Slope: {}, time: {}'.format(slope, duration)) + LOG.info('Slope: {}, time: {}'.format(slope, duration)) # SYNCing happens inside ramp_voltages self.ramp_voltages([chan], [v_start], [v_set], duration) else: - self.write('wav {} 0 0 0;set {} {:.6f}'.format(chan, chan, v_set)) - - # Helper for _set_mode - def _clipto(self, value, min, max, errmsg=""): - if value > max: - log.warning(errmsg) - return max - elif value < min: - log.warning(errmsg) - return min - else: - return value + self.write('wav {ch} 0 0 0;set {ch} {voltage:.6f}' + .format(ch=chan, voltage=v_set)) # Helper for _set_mode. It is not possible ot say if the channel is # connected to a generator, so we need to ask. def _wav_or_set_msg(self, chan, new_voltage): self.write('wav {}'.format(chan)) - FW_str = self._write_response - gen, _, _ = FW_str.split(',') + fw_str = self._write_response + gen, _, _ = fw_str.split(',') if int(gen) > 0: # The amplitude must be set to zero to avoid potential overflow. # Assuming that voltage range is not changed during a ramp @@ -390,7 +407,7 @@ def _set_mode(self, chan, new_mode): if switchint == 2: message = 'cur {} {};'.format(chan, new_irange) old_voltage = self.channels[chan-1].v.get() - new_voltage = self._clipto( + new_voltage = _clipto( # Actually, for 6,7 new_voltage = old_voltage, always. old_voltage, self.vranges[chan][new_vrange]['Min'], self.vranges[chan][new_vrange]['Max'], @@ -406,18 +423,6 @@ def _set_mode(self, chan, new_mode): self.channels[chan-1].v.cache.set(new_voltage) self.write(message) - def _vrange(self, range): - if range == Mode.vlow_ilow: - return 1 - else: - return 0 - - def _irange(self, range): - if range == Mode.vhigh_ihigh: - return 1 - else: - return 0 - def _set_vvals_to_current_range(self): """ Command for setting all 'v' limits ('vals') of all channels to the @@ -425,7 +430,7 @@ def _set_vvals_to_current_range(self): is currently in. """ for chan in range(1, self.num_chans+1): - vrange = self._vrange(self.channels[chan-1].mode()) + vrange = _vrange(self.channels[chan-1].mode()) self.channels[chan-1].v.vals = vals.Numbers( self.vranges[chan][vrange]['Min'], self.vranges[chan][vrange]['Max']) @@ -570,7 +575,8 @@ def _setslope(self, chan, slope): if slope == 'Inf': # Set the channel in DC mode v_set = self.channels[chan-1].v.get() - self.write('set {} {:.6f};wav {} 0 0 0'.format(chan, v_set, chan)) + self.write('set {ch} {voltage:.6f};wav {ch} 0 0 0' + .format(ch=chan, voltage=v_set)) # Now release the function generator and fg trigger (if possible) try: @@ -587,7 +593,6 @@ def _setslope(self, chan, slope): self._slopes.pop(chan, None) else: self._slopes[chan] = slope - return def _getslope(self, chan): """ @@ -617,9 +622,9 @@ def _get_minmax_outputvoltage(self, channel, vrange_int): raise ValueError('Range must be 0 or 1.') self.write('rang {} {}'.format(channel, vrange_int)) - FW_str = self._write_response - return {'Min': float(FW_str.split('MIN:')[1].split('MAX')[0].strip()), - 'Max': float(FW_str.split('MAX:')[1].strip())} + fw_str = self._write_response + return {'Min': float(fw_str.split('MIN:')[1].split('MAX')[0].strip()), + 'Max': float(fw_str.split('MAX:')[1].strip())} def write(self, cmd): """ @@ -635,7 +640,7 @@ def write(self, cmd): available in `_write_response` """ - log.debug("Writing to instrument {}: {}".format(self.name, cmd)) + LOG.debug("Writing to instrument {}: {}".format(self.name, cmd)) _, ret_code = self.visa_handle.write(cmd) self.check_error(ret_code) for _ in range(cmd.count(';')+1): @@ -655,7 +660,7 @@ def connect_message(self, idn_param='IDN', begin_time=None): software version is printed. """ self.visa_handle.write('version') - log.info('Connected to QDAC on {}, {}'.format( + LOG.info('Connected to QDAC on {}, {}'.format( self._address, self.visa_handle.read())) def _get_firmware_version(self): @@ -664,22 +669,22 @@ def _get_firmware_version(self): QDAC, and the version number is returned. Otherwise 0.0 is returned. """ self.write('version') - FW_str = self._write_response - if ((not ("Unrecognized command" in FW_str)) - and ("Software Version: " in FW_str)): - FW_version = float( + fw_str = self._write_response + if ((not ("Unrecognized command" in fw_str)) + and ("Software Version: " in fw_str)): + fw_version = float( self._write_response.replace("Software Version: ", "")) else: - FW_version = 0.0 - return FW_version + fw_version = 0.0 + return fw_version def _get_number_of_channels(self): """ Returns the number of channels for the instrument """ self.write('boardNum') - FW_str = self._write_response - return 8*int(FW_str.strip("numberOfBoards:")) + fw_str = self._write_response + return 8*int(fw_str.strip("numberOfBoards:")) def print_overview(self, update_currents=False): """ @@ -690,7 +695,7 @@ def print_overview(self, update_currents=False): paramstoget = [['v', 'i'], ['mode']] # Second item to be translated printdict = {'v': 'Voltage', 'i': 'Current', 'mode': 'Mode'} - returnmap = {'mode': mode_dict} + returnmap = {'mode': MODE_DICT} # Print the channels for ii in range(self.num_chans): @@ -717,7 +722,7 @@ def _get_functiongenerator(self, chan): Used as helper function for ramp_voltages, but may also be used if the user wants to use a function generator for something else. If there are no free generators this function will wait for up to - FGS_TIMEOUT for one to be ready. + fgs_timeout for one to be ready. To mark a function generator as available for others set self._assigned_fgs[chan].t_end = 0 @@ -726,7 +731,7 @@ def _get_functiongenerator(self, chan): chan: (1..24/48) the channel for which a function generator is requested. """ - FGS_TIMEOUT = 2 # Max time to wait for next available generator + fgs_timeout = 2 # Max time to wait for next available generator if len(self._assigned_fgs) < 8: fg = min(self._fgs.difference( @@ -738,14 +743,14 @@ def _get_functiongenerator(self, chan): available_fgs_chans = [] fgs_t_end_ok = [g.t_end for chan, g in self._assigned_fgs.items() - if g.t_end < time_now+FGS_TIMEOUT] + if g.t_end < time_now+fgs_timeout] if len(fgs_t_end_ok) > 0: first_ready_t = min(fgs_t_end_ok) available_fgs_chans = [chan for chan, g in self._assigned_fgs.items() if g.t_end == first_ready_t] if first_ready_t > time_now: - log.warning(''' + LOG.warning(''' Trying to ramp more channels than there are generators.\n Waiting for ramp generator to be released''') time.sleep(first_ready_t - time_now) @@ -757,21 +762,21 @@ def _get_functiongenerator(self, chan): self._assigned_fgs[chan] = Generator(fg) # Set the old channel in DC mode v_set = self.channels[oldchan-1].v.cache() - self.write('set {} {:.6f};wav {} 0 0 0' - .format(oldchan, v_set, oldchan)) + self.write('set {ch} {voltage:.6f};wav {ch} 0 0 0' + .format(ch=oldchan, voltage=v_set)) else: raise RuntimeError(''' Trying to ramp more channels than there are generators available. Please insert delays allowing channels to finish ramping before trying to ramp other channels, or reduce the - number of ramped channels. Or increase FGS_TIMEOUT.''') + number of ramped channels. Or increase fgs_timeout.''') return fg def ramp_voltages(self, channellist, v_startlist, v_endlist, ramptime): """ Function for smoothly ramping one channel or more channels simultaneously (max. 8). This is a shallow interface to - ramp_voltages_2D. Function generators and triggers are + ramp_voltages_2d. Function generators and triggers are are assigned automatically. Args: @@ -791,17 +796,17 @@ def ramp_voltages(self, channellist, v_startlist, v_endlist, ramptime): """ if ramptime < 0.002: - log.warning('Ramp time too short: {:.3f} s. Ramp time set to 2 ms.' + LOG.warning('Ramp time too short: {:.3f} s. Ramp time set to 2 ms.' .format(ramptime)) ramptime = 0.002 steps = int(ramptime*1000) - return self.ramp_voltages_2D( + return self.ramp_voltages_2d( slow_chans=[], slow_vstart=[], slow_vend=[], fast_chans=channellist, fast_vstart=v_startlist, fast_vend=v_endlist, step_length=0.001, slow_steps=1, fast_steps=steps) - def ramp_voltages_2D(self, slow_chans, slow_vstart, slow_vend, + def ramp_voltages_2d(self, slow_chans, slow_vstart, slow_vend, fast_chans, fast_vstart, fast_vend, step_length, slow_steps, fast_steps): """ @@ -840,7 +845,7 @@ def ramp_voltages_2D(self, slow_chans, slow_vstart, slow_vend, step_length_ms = int(step_length*1000) if step_length < 0.001: - log.warning('step_length too short: {:.3f} s. \nstep_length set to' + LOG.warning('step_length too short: {:.3f} s. \nstep_length set to' .format(step_length_ms) + ' minimum (1ms).') step_length_ms = 1 @@ -891,7 +896,8 @@ def ramp_voltages_2D(self, slow_chans, slow_vstart, slow_vend, for chan in channellist: if chan in self._syncoutputs: sync = self._syncoutputs[chan] - sync_duration = int(1000*self.channels[chan-1].sync_duration.get()) + sync_duration = int( + 1000*self.channels[chan-1].sync_duration.get()) sync_delay = int(1000*self.channels[chan-1].sync_delay.get()) self.write('syn {} {} {} {}'.format( sync, self._assigned_fgs[chan].fg, From c81a527c24273cd6196a05465e0c551da7fd1ca9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Anders=20Ku=CC=88hle?= Date: Mon, 2 Dec 2019 15:56:46 +0100 Subject: [PATCH 10/32] style: Removed blank line after QDacChannels docstring --- qcodes_contrib_drivers/drivers/QDevil/QDAC1.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/qcodes_contrib_drivers/drivers/QDevil/QDAC1.py b/qcodes_contrib_drivers/drivers/QDevil/QDAC1.py index 519b10ad7..391f4b86a 100644 --- a/qcodes_contrib_drivers/drivers/QDevil/QDAC1.py +++ b/qcodes_contrib_drivers/drivers/QDevil/QDAC1.py @@ -72,10 +72,9 @@ class QDacChannel(InstrumentChannel): A single output channel of the QDac. Exposes chan.v, chan.i, chan.mode, chan.slope, - chan.sync, chan.sync_delay, chan.sync_duration - Always set v to zero before changing range + chan.sync, chan.sync_delay, chan.sync_duration.\n + Always set v to zero before changing mode. """ - _CHANNEL_VALIDATION = vals.Numbers(1, 48) def __init__(self, parent, name, channum): From f758a409f7972002ee2fe481c2105a87c430c875 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Anders=20Ku=CC=88hle?= Date: Mon, 10 Feb 2020 14:18:43 +0100 Subject: [PATCH 11/32] refactor: Update QDevil_QDAC driver and example notebook Apply most changes suggested in review, and changed behavior to not touch the outputs at initialisation. The QDAC state is instead read at initialisation leaving any on going ramping to continue, thus restoring the internal generator book keeping. A reset command is added to make it possible to reset all parameters to default values. --- .../drivers/QDevil/QDAC1.py | 465 +++++++++++------- 1 file changed, 294 insertions(+), 171 deletions(-) diff --git a/qcodes_contrib_drivers/drivers/QDevil/QDAC1.py b/qcodes_contrib_drivers/drivers/QDevil/QDAC1.py index 391f4b86a..849b455a3 100644 --- a/qcodes_contrib_drivers/drivers/QDevil/QDAC1.py +++ b/qcodes_contrib_drivers/drivers/QDevil/QDAC1.py @@ -1,7 +1,7 @@ # QCoDeS driver for the QDevil QDAC using channels # Adapted by QDevil from "qdev\QDac_channels.py" in # the instrument drivers package -# Version 2.0 QDevil 2019-11-19 +# Version 2.1 QDevil 2020-02-10 import time import visa @@ -12,16 +12,32 @@ from qcodes.instrument.channel import MultiChannelInstrumentParameter from qcodes.instrument.visa import VisaInstrument from qcodes.utils import validators as vals +from enum import Enum +from collections import namedtuple LOG = logging.getLogger(__name__) -MODE_DICT = {0: "V-high / I-high", 1: "V-high / I-low", 2: "V-low / I-low"} +_ModeTuple = namedtuple('Mode', 'v i') -class Mode: - vhigh_ihigh = 0 - vhigh_ilow = 1 - vlow_ilow = 2 + +class Mode(Enum): + """ + Enum type use as the mode parameter for channels + defining the combined voltage and current range. + + get_label() returns a text representation of the mode. + """ + vhigh_ihigh = _ModeTuple(v=0, i=1) + vhigh_ilow = _ModeTuple(v=0, i=0) + vlow_ilow = _ModeTuple(v=1, i=0) + + def get_label(self): + _MODE_LABELS = { + self.vhigh_ihigh: "V range high / I range high", + self.vhigh_ilow: "V range high / I range low", + self.vlow_ilow: "V range low / I range low"} + return _MODE_LABELS[self] class Waveform: @@ -30,41 +46,14 @@ class Waveform: square = 2 triangle = 3 staircase = 4 - all = [sine, square, triangle, staircase] + all_waveforms = [sine, square, triangle, staircase] class Generator(): - fg = 0 - t_end = 9.9e9 - + # Class used in the internal book keeping of generators def __init__(self, generator_number): self.fg = generator_number - - -# Helper functions for QDac._set_mode and for usability -def _clipto(value, min_, max_, errmsg=""): - if value > max_: - LOG.warning(errmsg) - return max_ - elif value < min_: - LOG.warning(errmsg) - return min_ - else: - return value - - -def _vrange(range_): - if range_ == Mode.vlow_ilow: - return 1 - else: - return 0 - - -def _irange(range_): - if range_ == Mode.vhigh_ihigh: - return 1 - else: - return 0 + self. t_end = 9.9e9 class QDacChannel(InstrumentChannel): @@ -73,22 +62,19 @@ class QDacChannel(InstrumentChannel): Exposes chan.v, chan.i, chan.mode, chan.slope, chan.sync, chan.sync_delay, chan.sync_duration.\n - Always set v to zero before changing mode. + NB: Set v to zero before changing mode if the + mode_force lfag is False (default). """ - _CHANNEL_VALIDATION = vals.Numbers(1, 48) def __init__(self, parent, name, channum): """ Args: parent (Instrument): The instrument to which the channel belongs. name (str): The name of the channel - channum (int): The number of the channel (1-48) + channum (int): The number of the channel (1-24 or 1-48) """ super().__init__(parent, name) - # Validate the channel - self._CHANNEL_VALIDATION.validate(channum) - # Add the parameters self.add_parameter(name='v', label='Channel {} voltage'.format(channum), @@ -96,7 +82,7 @@ def __init__(self, parent, name, channum): set_cmd=partial(self._parent._set_voltage, channum), get_cmd='set {}'.format(channum), get_parser=float, - # Initial values. Updated on init and during + # Initial range. Updated on init and during # operation: vals=vals.Numbers(-9.99, 9.99) ) @@ -104,7 +90,8 @@ def __init__(self, parent, name, channum): self.add_parameter(name='mode', label='Channel {} mode.'.format(channum), set_cmd=partial(self._parent._set_mode, channum), - vals=vals.Enum(0, 1, 2) + get_cmd=None, + vals=vals.Enum(*list(Mode)) ) self.add_parameter(name='i', @@ -127,7 +114,7 @@ def __init__(self, parent, name, channum): label='Channel {} sync output'.format(channum), set_cmd=partial(self._parent._setsync, channum), get_cmd=partial(self._parent._getsync, channum), - vals=vals.Ints(0, 5) + vals=vals.Ints(0, 4) # Updated at qdac init ) self.add_parameter(name='sync_delay', @@ -150,8 +137,8 @@ def __init__(self, parent, name, channum): def snapshot_base(self, update=False, params_to_skip_update=None): update_currents = self._parent._update_currents and update if update and not self._parent._get_status_performed: - self._parent._get_status(readcurrents=update_currents) - # call get_status rather than getting the status individually for + self._parent._update_cache(update_currents=update_currents) + # call update_cache rather than getting the status individually for # each parameter. This is only done if _get_status_performed is False # this is used to signal that the parent has already called it and # no need to repeat. @@ -177,11 +164,11 @@ def get_raw(self): list. """ # For voltages, we can do something slightly faster than the naive - # approach + # approach by asking the instrument for a channel overview. if self._param_name == 'v': qdac = self._channels[0]._parent - qdac._get_status(readcurrents=False) + qdac._update_cache(readcurrents=False) output = tuple(chan.parameters[self._param_name].cache() for chan in self._channels) else: @@ -193,9 +180,15 @@ def get_raw(self): class QDac(VisaInstrument): """ - Channelised driver for the QDevil QDAC voltage source + Channelised driver for the QDevil QDAC voltage source. + Exposes channels, temperature sensors and calibration output, - and 'ramp_voltages' for mutil channel ramping. + and 'ramp_voltages' + 'ramp_voltages_2d' for multi channel ramping. + + In addition a 'mode_force' flag (default False) is exposed. + 'mode_force' (=True) is used to enable voltage range switching, via + the channel 'mode' parameter, even at non-zero output voltages. + Tested with Firmware Version: 1.07 The driver assumes that the instrument is ALWAYS in verbose mode OFF @@ -228,15 +221,12 @@ def __init__(self, handle = self.visa_handle self._get_status_performed = False - # This is the baud rate on power-up. It can be changed later but - # you must start out with this value. + # Communication setup + firmware check handle.baud_rate = 480600 handle.parity = visa.constants.Parity(0) handle.data_bits = 8 self.set_terminator('\n') - # TODO: do we want a method for write termination too? handle.write_termination = '\n' - # TODO: do we need a query delay for robust operation? self._write_response = '' firmware_version = self._get_firmware_version() if firmware_version < 1.07: @@ -247,23 +237,15 @@ def __init__(self, contact info@qdevil.com for a firmware update. ''') + # Initialse basic information and internal book keeping self.num_chans = self._get_number_of_channels() num_boards = int(self.num_chans/8) self._output_n_lines = self.num_chans + 2 - - # Assigned slopes. Entries will eventually be {chan: slope} - self._slopes = {} - # Function generators and triggers (used in ramping) - self._fgs = set(range(1, 9)) - self._assigned_fgs = {} # {chan: fg} - self._trigs = set(range(1, 10)) - self._assigned_triggers = {} # {fg: trigger} - # Sync channels - self._syncoutputs = {} # {chan: syncoutput} - self._chan_range = range(1, 1 + self.num_chans) self.channel_validator = vals.Ints(1, self.num_chans) + self._reset_bookkeeping() + # Add channels (and channel parameters) channels = ChannelList(self, "Channels", QDacChannel, snapshotable=False, multichan_paramclass=QDacMultiChannelParameter) @@ -275,6 +257,12 @@ def __init__(self, channels.lock() self.add_submodule('channels', channels) + # Updatechannel sync port validator according to number of boards + self._num_syns = max(num_boards-1, 1) + for chan in self._chan_range: + self.channels[chan-1].sync.vals = vals.Ints(0, self._num_syns) + + # Add non-channel parameters for board in range(num_boards): for sensor in range(3): label = 'Board {}, Temperature {}'.format(board, sensor) @@ -286,41 +274,140 @@ def __init__(self, self.add_parameter(name='cal', set_cmd='cal {}', - vals=self.channel_validator) - - self.add_parameter(name='verbose', - label=label, - val_mapping={True: 1, False: 0}) - - # Initialise the instrument, all channels DC (unbind func. generators) - for chan in self._chan_range: - # Note: this call may change the voltage on the channel - self.write('wav {} 0 1 0'.format(chan)) - - # Get all calibrated min/max output values, before switching to - # verbose off mode - self.vranges = {} - for chan in self._chan_range: - self.vranges.update( - {chan: {0: self._get_minmax_outputvoltage(chan, 0), - 1: self._get_minmax_outputvoltage(chan, 1)}}) - + vals=vals.Ints(0, self.num_chans)) + + self.add_parameter(name='mode_force', + label='Mode force', + get_cmd=None, set_cmd=None, + vals=vals.Bool(), + initial_value=False + ) + + # Due to a firmware bug in 1.07 voltage ranges are always reported + # vebosely. So for future compatibility we set verbose True + self.write('ver 1') + self._update_voltage_ranges() + # The driver require verbose mode off except for the above command self.write('ver 0') - self.verbose.cache.set(False) + self._verbose = False # Just so that the code can check the state self.connect_message() LOG.info('[*] Querying all channels for voltages and currents...') - self._get_status(readcurrents=update_currents) + self._update_cache(update_currents=update_currents) self._update_currents = update_currents - # Set "v" parameter limits to actual calibrated limits - self._set_vvals_to_current_range() + self._load_state() LOG.info('[+] Done') + def _reset_bookkeeping(self): + """ + Resets all internal variables used for ramping and + synchronization outputs. + """ + # Assigned slopes. Entries will eventually be {chan: slope} + self._slopes = {} + # Function generators and triggers (used in ramping) + self._fgs = set(range(1, 9)) + self._assigned_fgs = {} # {chan: fg} + self._trigs = set(range(1, 10)) + self._assigned_triggers = {} # {fg: trigger} + # Sync channels + self._syncoutputs = {} # {chan: syncoutput} + + def _load_state(self): + """ + Used as part of initiaisation. DON'T use _load_state() separately.\n + Updates internal book keeping of running function generators. + used triggers and active sync outputs.\n + Slopes can not be read/updated as it is not possible to + say if a generator is running because a slope has been assigned + or because it is being ramped direcly (by e.g. ramp_voltages_2d()). + """ + # Assumes that all variables and virtual + # parameters have been initialised (and read) + + self.write('ver 0') # Just to be on the safe side + + self._reset_bookkeeping() + for ch_idx in range(self.num_chans): + chan = ch_idx + 1 + # Check if the channels are being ramped + # It is not possible to find out if it has a slope assigned + # as it may be ramped explicitely by the user + self.write('wav {}'.format(chan)) + fg, amplitude, offset = self._write_response.split(',') + fg = int(fg) + if fg in range(1, 9): + voltage = self.channels[ch_idx].v.get() + self.write('fun {}'.format(fg)) + response = self._write_response.split(',') + waveform = int(response[0]) + if waveform == Waveform.staircase: + if len(response) == 6: + step_length_ms, no_steps, rep, remain, trigger \ + = response[1:6] + else: + step_length_ms, no_steps, rep, trigger = response[1:5] + remain = 0 + ramp_time = 0.001 * float(step_length_ms) \ + * int(no_steps) * int(rep) + ramp_done = voltage / (float(amplitude) + float(offset)) + time_end = ((1-ramp_done)+max(0, int(rep)-1) + + int(remain))*ramp_time + time.time() + 0.001 + else: + if waveform == Waveform.sine: + period_ms, rep, remain, trigger = response[1:6] + else: + period_ms, _, rep, remain, trigger = response[1:6] + if int(rep) == -1: + time_end = time.time() + 315360000 # 10 years from now + else: # +1 is just a safe guard + time_end = time.time() \ + + 0.001 * (int(remain)+1) * float(period_ms) + + self._assigned_fgs[chan] = Generator(fg) + self._assigned_fgs[chan].t_end = time_end + if trigger != 0: + self._assigned_triggers[fg] = int(trigger) + for syn in range(1, self._num_syns+1): + self.write('syn {}'.format(syn)) + syn_fg, delay_ms, duration_ms = \ + self._write_response.split(',') + if int(syn_fg) == fg: + self.channels[ch_idx].sync.cache.set(syn) + self.channels[ch_idx].sync_delay(float(delay_ms)/1000) + self.channels[ch_idx].sync_duration( + float(duration_ms)/1000) + + def reset(self, update_currents=False): + """ + Resets the instrument setting all channels to zero output voltage + and all parameters to their default values, including removing any + assigned sync putputs, function generators, triggers etc. + """ + # In case the QDAC has been switched off/on + # clear the io buffer and set verbose False + self.device_clear() + self.write('ver 0') + + self.cal(0) + # Resetting all slopes first will cause v.set() disconnect generators + self.channels[0:self.num_chans].slope('Inf') + self.channels[0:self.num_chans].v(0) + self.channels[0:self.num_chans].mode(Mode.vhigh_ihigh) + self.channels[0:self.num_chans].sync(0) + self.channels[0:self.num_chans].sync_delay(0) + self.channels[0:self.num_chans].sync_duration(0.01) + + if update_currents: + self.channels[0:self.num_chans].i.get() + self.mode_force(False) + self._reset_bookkeeping() + def snapshot_base(self, update=False, params_to_skip_update=None): update_currents = self._update_currents and update if update: - self._get_status(readcurrents=update_currents) + self._update_cache(update_currents=update_currents) self._get_status_performed = True - # call get_status rather than getting the status individually for + # call _update_cache rather than getting the status individually for # each parameter. We set _get_status_performed to True # to indicate that each update channel does not need to call this # function as opposed to when snapshot is called on an individual @@ -356,83 +443,105 @@ def _set_voltage(self, chan, v_set): LOG.info('Slope: {}, time: {}'.format(slope, duration)) # SYNCing happens inside ramp_voltages self.ramp_voltages([chan], [v_start], [v_set], duration) - else: + else: # Should not be necessary to wav here. TODO: Check this self.write('wav {ch} 0 0 0;set {ch} {voltage:.6f}' .format(ch=chan, voltage=v_set)) - # Helper for _set_mode. It is not possible ot say if the channel is - # connected to a generator, so we need to ask. - def _wav_or_set_msg(self, chan, new_voltage): - self.write('wav {}'.format(chan)) - fw_str = self._write_response - gen, _, _ = fw_str.split(',') - if int(gen) > 0: - # The amplitude must be set to zero to avoid potential overflow. - # Assuming that voltage range is not changed during a ramp - return 'wav {} {} {:.6f} {:.6f}'\ - .format(chan, int(gen), 0, new_voltage) - else: - return 'set {} {:.6f}'.format(chan, new_voltage) - def _set_mode(self, chan, new_mode): """ - set_cmd for the QDAC's mode (combined voltage and current sense range) + set_cmd for the QDAC's mode (combined voltage and current sense range). + It is not possible to switch from voltage range without setting the + the volage to zero first or set the global mode_force parameter True. """ - # Mode 0: (10V, 100uA), Mode 1: (10V, 1uA), Mode 2: (1V, 1uA) - ivrangedict = { - 0: {"vol": 0, "cur": 1}, - 1: {"vol": 0, "cur": 0}, - 2: {"vol": 1, "cur": 0}} - # switchint = (0,1,2, - # 3,4,5, - # 6,7,8) + def _clipto(value, min_, max_): + errmsg = ("Voltage is outside the bounds of the new voltage range" + " and is therefore clipped.") + if value > max_: + LOG.warning(errmsg) + return max_ + elif value < min_: + LOG.warning(errmsg) + return min_ + else: + return value + + # It is not possible ot say if the channel is connected to + # a generator, so we need to ask. + def wav_or_set_msg(chan, new_voltage): + self.write('wav {}'.format(chan)) + fw_str = self._write_response + gen, _, _ = fw_str.split(',') + if int(gen) > 0: + # The amplitude must be set to zero to avoid potential overflow. + # Assuming that voltage range is not changed during a ramp + return 'wav {} {} {:.6f} {:.6f}'\ + .format(chan, int(gen), 0, new_voltage) + else: + return 'set {} {:.6f}'.format(chan, new_voltage) old_mode = self.channels[chan-1].mode.cache() - new_vrange = ivrangedict[new_mode]["vol"] - new_irange = ivrangedict[new_mode]["cur"] - switchint = int(3*old_mode+new_mode) + new_vrange = new_mode.value.v + old_vrange = old_mode.value.v + new_irange = new_mode.value.i + old_irange = old_mode.value.i message = '' + max_zero_voltage = {0: 20e-6, 1: 3e-6} + NON_ZERO_VOLTAGE_MSG = ( + 'Please set the voltage to zero before changing the voltage' + ' range in order to avoid jumps or spikes.' + ' Or set mode_force=True to allow voltage range change for' + ' non-zero voltages.') if old_mode == new_mode: return # If the voltage range is going to change we have to take care of - # setting the voltage after the switch, and therefore read them first - # Only the current range has to change - if switchint in [1, 3]: - message = 'cur {} {}'.format(chan, new_irange) - # The voltage range has to change - else: # switchint in [2,5,6,7] (7 is max) - if switchint == 2: - message = 'cur {} {};'.format(chan, new_irange) + # setting the voltage after the switch, and therefore read it first + # We also need to make sure than only one of the voltage/current + # relays is on at a time (otherwise the firmware will enforce it). + + if (new_irange != old_irange) and (new_vrange == old_vrange == 0): + # Only the current sensor relay has to switch: + message += 'cur {} {}'.format(chan, new_irange) + # The voltage relay (also) has to switch: + else: + # Current sensor relay on->off before voltage relay off->on: + if new_irange < old_irange and new_vrange > old_vrange: + message += 'cur {} {};'.format(chan, new_irange) old_voltage = self.channels[chan-1].v.get() + # Check if voltage is non-zero and mode_force is off + if ((self.mode_force() is False) and + (abs(old_voltage) > max_zero_voltage[old_vrange])): + raise ValueError(NON_ZERO_VOLTAGE_MSG) new_voltage = _clipto( - # Actually, for 6,7 new_voltage = old_voltage, always. old_voltage, self.vranges[chan][new_vrange]['Min'], - self.vranges[chan][new_vrange]['Max'], - "Voltage is outside the bounds of the new voltage range" - " and is therefore clipped.") + self.vranges[chan][new_vrange]['Max']) message += 'vol {} {};'.format(chan, new_vrange) - message += self._wav_or_set_msg(chan, new_voltage) - if switchint == 6: + message += wav_or_set_msg(chan, new_voltage) + # Current sensor relay off->on after voltage relay on->off: + if new_irange > old_irange and new_vrange < old_vrange: message += ';cur {} {}'.format(chan, new_irange) - self.channels[chan-1].v.vals = vals.Numbers( - self.vranges[chan][new_vrange]['Min'], - self.vranges[chan][new_vrange]['Max']) + self.channels[chan-1].v.vals = self._v_vals(chan, new_vrange) self.channels[chan-1].v.cache.set(new_voltage) + self.write(message) - def _set_vvals_to_current_range(self): + def _v_vals(self, chan, vrange_int): + """ + Returns the validator for the specified voltage range. + """ + return vals.Numbers(self.vranges[chan][vrange_int]['Min'], + self.vranges[chan][vrange_int]['Max']) + + def _update_v_validators(self): """ Command for setting all 'v' limits ('vals') of all channels to the actual calibrated output limits for the range each individual channel is currently in. """ for chan in range(1, self.num_chans+1): - vrange = _vrange(self.channels[chan-1].mode()) - self.channels[chan-1].v.vals = vals.Numbers( - self.vranges[chan][vrange]['Min'], - self.vranges[chan][vrange]['Max']) + vrange = self.channels[chan-1].mode.value.v + self.channels[chan-1].v.vals = self._v_vals(chan, vrange) def _num_verbose(self, s): """ @@ -440,7 +549,7 @@ def _num_verbose(self, s): If the QDac is in verbose mode, this involves stripping off the value descriptor. """ - if self.verbose.cache(): + if self._verbose: s = s.split[': '][-1] return float(s) @@ -450,7 +559,7 @@ def _current_parser(self, s): """ return 1e-6*self._num_verbose(s) - def _get_status(self, readcurrents=False): + def _update_cache(self, update_currents=False): """ Function to query the instrument and get the status of all channels. Takes a while to finish. @@ -468,16 +577,16 @@ def _get_status(self, readcurrents=False): """ irange_trans = {'hi cur': 1, 'lo cur': 0} vrange_trans = {'X 1': 0, 'X 0.1': 1} - vi_range_dict = {0: {0: 1, 1: 0}, 1: {0: 2, 1: 3}} - # Status call - version_line = self.ask('status') + # Status call, check the + version_line = self.ask('status') if version_line.startswith('Software Version: '): self.version = version_line.strip().split(': ')[1] else: self._wait_and_clear() raise ValueError('unrecognized version line: ' + version_line) + # Check header line header_line = self.read() headers = header_line.lower().strip('\r\n').split('\t') expected_headers = ['channel', 'out v', '', 'voltage range', @@ -494,12 +603,13 @@ def _get_status(self, readcurrents=False): chan = int(chanstr) vrange_int = int(vrange_trans[vrange.strip()]) irange_int = int(irange_trans[irange.strip()]) - mode = vi_range_dict[vrange_int][irange_int] + mode = Mode((vrange_int, irange_int)) self.channels[chan-1].mode.cache.set(mode) self.channels[chan-1].v.cache.set(float(v)) + self.channels[chan-1].v.vals = self._v_vals(chan, vrange_int) chans_left.remove(chan) - if readcurrents: + if update_currents: for chan in self._chan_range: self.channels[chan-1].i.get() @@ -625,6 +735,17 @@ def _get_minmax_outputvoltage(self, channel, vrange_int): return {'Min': float(fw_str.split('MIN:')[1].split('MAX')[0].strip()), 'Max': float(fw_str.split('MAX:')[1].strip())} + def _update_voltage_ranges(self): + # Get all calibrated min/max output values, requires verbose on + # in firmware version 1.07 + self.write('ver 1') + self.vranges = {} + for chan in self._chan_range: + self.vranges.update( + {chan: {0: self._get_minmax_outputvoltage(chan, 0), + 1: self._get_minmax_outputvoltage(chan, 1)}}) + self.write('ver 0') + def write(self, cmd): """ QDac always returns something even from set commands, even when @@ -690,29 +811,29 @@ def print_overview(self, update_currents=False): Pretty-prints the status of the QDac """ - self._get_status(readcurrents=update_currents) - - paramstoget = [['v', 'i'], ['mode']] # Second item to be translated - printdict = {'v': 'Voltage', 'i': 'Current', 'mode': 'Mode'} - returnmap = {'mode': MODE_DICT} + self._update_cache(update_currents=update_currents) - # Print the channels for ii in range(self.num_chans): line = 'Channel {} \n'.format(ii+1) - line += ' ' - for pp in paramstoget[0]: - param = getattr(self.channels[ii], pp) - line += printdict[pp] - line += ': {}'.format(param.cache()) - line += ' ({})'.format(param.unit) - line += '. ' - line += '\n ' - for pp in paramstoget[1]: - param = getattr(self.channels[ii], pp) - line += printdict[pp] - value = param.cache() - line += ': {}'.format(returnmap[pp][value]) - line += '. ' + line += ' Voltage: {} ({}).\n'.format( + self.channels[ii].v.cache(), self.channels[ii].v.unit) + line += ' Current: {} ({}).\n'.format( + self.channels[ii].i.cache.get(get_if_invalid=False), self.channels[ii].i.unit) + line += ' Mode: {}.\n'.format( + self.channels[ii].mode.cache().get_label()) + line += ' Slope: {} ({}).\n'.format( + self.channels[ii].slope.cache(), + self.channels[ii].slope.unit) + if self.channels[ii].sync.cache() > 0: + line += ' Sync Out: {}, Delay: {} ({}), '\ + 'Duration: {} ({}).\n'.format( + self.channels[ii].sync.cache(), + self.channels[ii].sync_delay.cache(), + self.channels[ii].sync_delay.unit, + self.channels[ii].sync_duration.cache(), + self.channels[ii].sync_duration.unit, + ) + print(line) def _get_functiongenerator(self, chan): @@ -738,6 +859,8 @@ def _get_functiongenerator(self, chan): self._assigned_fgs[chan] = Generator(fg) else: # If no available fgs, see if one is soon to be ready + # Nte, this does not handle if teh user has assigned the + # same fg to multiple channels cheating the driver time_now = time.time() available_fgs_chans = [] fgs_t_end_ok = [g.t_end for chan, g @@ -822,7 +945,6 @@ def ramp_voltages_2d(self, slow_chans, slow_vstart, slow_vend, reading the present values from the instrument.\n slow_vend: list (int) of voltages to ramp to in the slow-group. - slow_steps: (int) number of steps in the x direction.\n fast_chans: List (int) of channels to be ramped (1 indexed) in the fast-group.\n fast_vstart: List (int) of voltages to ramp from in the @@ -831,9 +953,11 @@ def ramp_voltages_2d(self, slow_chans, slow_vstart, slow_vend, reading the present values from the instrument.\n fast_vend: list (int) of voltages to ramp to in the fast-group. - fast_steps: (int) number of steps in the fast direction.\n step_length: (float) Time spent at each step in seconds (min. 0.001) multiple of 1 ms.\n + slow_steps: (int) number of steps in the slow direction.\n + fast_steps: (int) number of steps in the fast direction.\n + Returns: Estimated time of the excecution of the 2D scan.\n NOTE: This function returns as the ramps are started. @@ -874,7 +998,6 @@ def ramp_voltages_2d(self, slow_chans, slow_vstart, slow_vend, # Get start voltages if not provided if not slow_vstart: slow_vstart = [self.channels[ch-1].v.get() for ch in slow_chans] - if not fast_vstart: fast_vstart = [self.channels[ch-1].v.get() for ch in fast_chans] @@ -933,6 +1056,6 @@ def ramp_voltages_2d(self, slow_chans, slow_vstart, slow_vend, # Update fgs dict so that we know when the ramp is supposed to end time_ramp = slow_steps * fast_steps * step_length_ms / 1000 time_end = time_ramp + time.time() - for i in range(no_channels): + for chan in channellist: self._assigned_fgs[chan].t_end = time_end return time_ramp From 1541d841456bcd06892b9ab08345fdbc3d95b4d7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Anders=20Ku=CC=88hle?= Date: Tue, 11 Feb 2020 08:32:35 +0100 Subject: [PATCH 12/32] fix: delete a rogue space and shorten a too long line --- qcodes_contrib_drivers/drivers/QDevil/QDAC1.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/qcodes_contrib_drivers/drivers/QDevil/QDAC1.py b/qcodes_contrib_drivers/drivers/QDevil/QDAC1.py index 849b455a3..976003871 100644 --- a/qcodes_contrib_drivers/drivers/QDevil/QDAC1.py +++ b/qcodes_contrib_drivers/drivers/QDevil/QDAC1.py @@ -818,7 +818,8 @@ def print_overview(self, update_currents=False): line += ' Voltage: {} ({}).\n'.format( self.channels[ii].v.cache(), self.channels[ii].v.unit) line += ' Current: {} ({}).\n'.format( - self.channels[ii].i.cache.get(get_if_invalid=False), self.channels[ii].i.unit) + self.channels[ii].i.cache.get(get_if_invalid=False), + self.channels[ii].i.unit) line += ' Mode: {}.\n'.format( self.channels[ii].mode.cache().get_label()) line += ' Slope: {} ({}).\n'.format( From 268f8c7e39501ffa1fb514fdaad5c27d37aefa45 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Anders=20Ku=CC=88hle?= Date: Fri, 21 Feb 2020 16:04:43 +0100 Subject: [PATCH 13/32] fix: change keyword parameter to correct name when calling _update_cache() --- .../drivers/QDevil/QDAC1.py | 19 +++++++++---------- 1 file changed, 9 insertions(+), 10 deletions(-) diff --git a/qcodes_contrib_drivers/drivers/QDevil/QDAC1.py b/qcodes_contrib_drivers/drivers/QDevil/QDAC1.py index 976003871..97665a79d 100644 --- a/qcodes_contrib_drivers/drivers/QDevil/QDAC1.py +++ b/qcodes_contrib_drivers/drivers/QDevil/QDAC1.py @@ -168,7 +168,7 @@ def get_raw(self): if self._param_name == 'v': qdac = self._channels[0]._parent - qdac._update_cache(readcurrents=False) + qdac._update_cache(update_currents=False) output = tuple(chan.parameters[self._param_name].cache() for chan in self._channels) else: @@ -277,11 +277,10 @@ def __init__(self, vals=vals.Ints(0, self.num_chans)) self.add_parameter(name='mode_force', - label='Mode force', - get_cmd=None, set_cmd=None, - vals=vals.Bool(), - initial_value=False - ) + label='Mode force', + get_cmd=None, set_cmd=None, + vals=vals.Bool(), + initial_value=False) # Due to a firmware bug in 1.07 voltage ranges are always reported # vebosely. So for future compatibility we set verbose True @@ -289,7 +288,7 @@ def __init__(self, self._update_voltage_ranges() # The driver require verbose mode off except for the above command self.write('ver 0') - self._verbose = False # Just so that the code can check the state + self._verbose = False # Just so that the code can check the state self.connect_message() LOG.info('[*] Querying all channels for voltages and currents...') self._update_cache(update_currents=update_currents) @@ -343,7 +342,7 @@ def _load_state(self): if waveform == Waveform.staircase: if len(response) == 6: step_length_ms, no_steps, rep, remain, trigger \ - = response[1:6] + = response[1:6] else: step_length_ms, no_steps, rep, trigger = response[1:5] remain = 0 @@ -472,7 +471,7 @@ def wav_or_set_msg(chan, new_voltage): fw_str = self._write_response gen, _, _ = fw_str.split(',') if int(gen) > 0: - # The amplitude must be set to zero to avoid potential overflow. + # The amplitude must be set to zero to avoid potential overflow # Assuming that voltage range is not changed during a ramp return 'wav {} {} {:.6f} {:.6f}'\ .format(chan, int(gen), 0, new_voltage) @@ -511,7 +510,7 @@ def wav_or_set_msg(chan, new_voltage): old_voltage = self.channels[chan-1].v.get() # Check if voltage is non-zero and mode_force is off if ((self.mode_force() is False) and - (abs(old_voltage) > max_zero_voltage[old_vrange])): + (abs(old_voltage) > max_zero_voltage[old_vrange])): raise ValueError(NON_ZERO_VOLTAGE_MSG) new_voltage = _clipto( old_voltage, self.vranges[chan][new_vrange]['Min'], From 38310ab16090f30448d2c79f04109df3708895af Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Anders=20Ku=CC=88hle?= Date: Fri, 28 Feb 2020 14:47:19 +0100 Subject: [PATCH 14/32] fix: safeguard potential division by zero in _load_state() . Remove TODO --- .../drivers/QDevil/QDAC1.py | 38 ++++++++++++------- 1 file changed, 24 insertions(+), 14 deletions(-) diff --git a/qcodes_contrib_drivers/drivers/QDevil/QDAC1.py b/qcodes_contrib_drivers/drivers/QDevil/QDAC1.py index 97665a79d..f858325f7 100644 --- a/qcodes_contrib_drivers/drivers/QDevil/QDAC1.py +++ b/qcodes_contrib_drivers/drivers/QDevil/QDAC1.py @@ -331,36 +331,46 @@ def _load_state(self): # Check if the channels are being ramped # It is not possible to find out if it has a slope assigned # as it may be ramped explicitely by the user + # We assume that generators are running, but we cannot know self.write('wav {}'.format(chan)) - fg, amplitude, offset = self._write_response.split(',') + fg, amplitude_str, offset_str = self._write_response.split(',') + amplitude = float(amplitude_str) + offset = float(offset_str) fg = int(fg) if fg in range(1, 9): voltage = self.channels[ch_idx].v.get() + time_now = time.time() self.write('fun {}'.format(fg)) response = self._write_response.split(',') waveform = int(response[0]) + # Probably this driver is involved if a stair case is assigned if waveform == Waveform.staircase: if len(response) == 6: - step_length_ms, no_steps, rep, remain, trigger \ + step_length_ms, no_steps, rep, rep_remain, trigger \ = response[1:6] + rep_remain = int(rep_remain) else: step_length_ms, no_steps, rep, trigger = response[1:5] - remain = 0 - ramp_time = 0.001 * float(step_length_ms) \ - * int(no_steps) * int(rep) - ramp_done = voltage / (float(amplitude) + float(offset)) - time_end = ((1-ramp_done)+max(0, int(rep)-1) - + int(remain))*ramp_time + time.time() + 0.001 + rep_remain = int(rep) + ramp_time = 0.001 * float(step_length_ms) * int(no_steps) + ramp_remain = 0 + if (amplitude != 0): + ramp_remain = (amplitude+offset-voltage)/amplitude + if int(rep) == -1: + time_end = time_now + 315360000 + else: + time_end = (ramp_remain + max(0, rep_remain-1)) \ + * ramp_time + time_now + 0.001 else: if waveform == Waveform.sine: - period_ms, rep, remain, trigger = response[1:6] + period_ms, rep, rep_remain, trigger = response[1:5] else: - period_ms, _, rep, remain, trigger = response[1:6] + period_ms, _, rep, rep_remain, trigger = response[1:6] if int(rep) == -1: - time_end = time.time() + 315360000 # 10 years from now + time_end = time_now + 315360000 # 10 years from now else: # +1 is just a safe guard - time_end = time.time() \ - + 0.001 * (int(remain)+1) * float(period_ms) + time_end = time_now + 0.001 \ + * (int(rep_remain)+1) * float(period_ms) self._assigned_fgs[chan] = Generator(fg) self._assigned_fgs[chan].t_end = time_end @@ -442,7 +452,7 @@ def _set_voltage(self, chan, v_set): LOG.info('Slope: {}, time: {}'.format(slope, duration)) # SYNCing happens inside ramp_voltages self.ramp_voltages([chan], [v_start], [v_set], duration) - else: # Should not be necessary to wav here. TODO: Check this + else: # Should not be necessary to wav here. self.write('wav {ch} 0 0 0;set {ch} {voltage:.6f}' .format(ch=chan, voltage=v_set)) From 66f4e3939b80b89c35947d75a0dfd49075b80497 Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Fri, 25 Sep 2020 12:04:38 +0200 Subject: [PATCH 15/32] pyupgrade qcodes/instrument_drivers/QDevil --- .../drivers/QDevil/QDAC1.py | 76 +++++++++---------- 1 file changed, 38 insertions(+), 38 deletions(-) diff --git a/qcodes_contrib_drivers/drivers/QDevil/QDAC1.py b/qcodes_contrib_drivers/drivers/QDevil/QDAC1.py index f858325f7..8021e31d7 100644 --- a/qcodes_contrib_drivers/drivers/QDevil/QDAC1.py +++ b/qcodes_contrib_drivers/drivers/QDevil/QDAC1.py @@ -77,10 +77,10 @@ def __init__(self, parent, name, channum): # Add the parameters self.add_parameter(name='v', - label='Channel {} voltage'.format(channum), + label=f'Channel {channum} voltage', unit='V', set_cmd=partial(self._parent._set_voltage, channum), - get_cmd='set {}'.format(channum), + get_cmd=f'set {channum}', get_parser=float, # Initial range. Updated on init and during # operation: @@ -88,21 +88,21 @@ def __init__(self, parent, name, channum): ) self.add_parameter(name='mode', - label='Channel {} mode.'.format(channum), + label=f'Channel {channum} mode.', set_cmd=partial(self._parent._set_mode, channum), get_cmd=None, vals=vals.Enum(*list(Mode)) ) self.add_parameter(name='i', - label='Channel {} current'.format(channum), - get_cmd='get {}'.format(channum), + label=f'Channel {channum} current', + get_cmd=f'get {channum}', unit='A', get_parser=self._parent._current_parser ) self.add_parameter(name='slope', - label='Channel {} slope'.format(channum), + label=f'Channel {channum} slope', unit='V/s', set_cmd=partial(self._parent._setslope, channum), get_cmd=partial(self._parent._getslope, channum), @@ -111,14 +111,14 @@ def __init__(self, parent, name, channum): ) self.add_parameter(name='sync', - label='Channel {} sync output'.format(channum), + label=f'Channel {channum} sync output', set_cmd=partial(self._parent._setsync, channum), get_cmd=partial(self._parent._getsync, channum), vals=vals.Ints(0, 4) # Updated at qdac init ) self.add_parameter(name='sync_delay', - label='Channel {} sync pulse delay'.format(channum), + label=f'Channel {channum} sync pulse delay', unit='s', get_cmd=None, set_cmd=None, vals=vals.Numbers(0, 10000), @@ -127,7 +127,7 @@ def __init__(self, parent, name, channum): self.add_parameter( name='sync_duration', - label='Channel {} sync pulse duration'.format(channum), + label=f'Channel {channum} sync pulse duration', unit='s', get_cmd=None, set_cmd=None, vals=vals.Numbers(0.001, 10000), @@ -230,7 +230,7 @@ def __init__(self, self._write_response = '' firmware_version = self._get_firmware_version() if firmware_version < 1.07: - LOG.warning("Firmware version: {}".format(firmware_version)) + LOG.warning(f"Firmware version: {firmware_version}") raise RuntimeError(''' No QDevil QDAC detected or the firmware version is obsolete. This driver only supports version 1.07 or newer. Please @@ -251,9 +251,9 @@ def __init__(self, multichan_paramclass=QDacMultiChannelParameter) for i in self._chan_range: - channel = QDacChannel(self, 'chan{:02}'.format(i), i) + channel = QDacChannel(self, f'chan{i:02}', i) channels.append(channel) - self.add_submodule('ch{:02}'.format(i), channel) + self.add_submodule(f'ch{i:02}', channel) channels.lock() self.add_submodule('channels', channels) @@ -265,11 +265,11 @@ def __init__(self, # Add non-channel parameters for board in range(num_boards): for sensor in range(3): - label = 'Board {}, Temperature {}'.format(board, sensor) - self.add_parameter(name='temp{}_{}'.format(board, sensor), + label = f'Board {board}, Temperature {sensor}' + self.add_parameter(name=f'temp{board}_{sensor}', label=label, unit='C', - get_cmd='tem {} {}'.format(board, sensor), + get_cmd=f'tem {board} {sensor}', get_parser=self._num_verbose) self.add_parameter(name='cal', @@ -332,7 +332,7 @@ def _load_state(self): # It is not possible to find out if it has a slope assigned # as it may be ramped explicitely by the user # We assume that generators are running, but we cannot know - self.write('wav {}'.format(chan)) + self.write(f'wav {chan}') fg, amplitude_str, offset_str = self._write_response.split(',') amplitude = float(amplitude_str) offset = float(offset_str) @@ -340,7 +340,7 @@ def _load_state(self): if fg in range(1, 9): voltage = self.channels[ch_idx].v.get() time_now = time.time() - self.write('fun {}'.format(fg)) + self.write(f'fun {fg}') response = self._write_response.split(',') waveform = int(response[0]) # Probably this driver is involved if a stair case is assigned @@ -377,7 +377,7 @@ def _load_state(self): if trigger != 0: self._assigned_triggers[fg] = int(trigger) for syn in range(1, self._num_syns+1): - self.write('syn {}'.format(syn)) + self.write(f'syn {syn}') syn_fg, delay_ms, duration_ms = \ self._write_response.split(',') if int(syn_fg) == fg: @@ -449,7 +449,7 @@ def _set_voltage(self, chan, v_set): # was interrupted v_start = self.channels[chan-1].v.get() duration = abs(v_set-v_start)/slope - LOG.info('Slope: {}, time: {}'.format(slope, duration)) + LOG.info(f'Slope: {slope}, time: {duration}') # SYNCing happens inside ramp_voltages self.ramp_voltages([chan], [v_start], [v_set], duration) else: # Should not be necessary to wav here. @@ -477,7 +477,7 @@ def _clipto(value, min_, max_): # It is not possible ot say if the channel is connected to # a generator, so we need to ask. def wav_or_set_msg(chan, new_voltage): - self.write('wav {}'.format(chan)) + self.write(f'wav {chan}') fw_str = self._write_response gen, _, _ = fw_str.split(',') if int(gen) > 0: @@ -486,7 +486,7 @@ def wav_or_set_msg(chan, new_voltage): return 'wav {} {} {:.6f} {:.6f}'\ .format(chan, int(gen), 0, new_voltage) else: - return 'set {} {:.6f}'.format(chan, new_voltage) + return f'set {chan} {new_voltage:.6f}' old_mode = self.channels[chan-1].mode.cache() new_vrange = new_mode.value.v @@ -511,12 +511,12 @@ def wav_or_set_msg(chan, new_voltage): if (new_irange != old_irange) and (new_vrange == old_vrange == 0): # Only the current sensor relay has to switch: - message += 'cur {} {}'.format(chan, new_irange) + message += f'cur {chan} {new_irange}' # The voltage relay (also) has to switch: else: # Current sensor relay on->off before voltage relay off->on: if new_irange < old_irange and new_vrange > old_vrange: - message += 'cur {} {};'.format(chan, new_irange) + message += f'cur {chan} {new_irange};' old_voltage = self.channels[chan-1].v.get() # Check if voltage is non-zero and mode_force is off if ((self.mode_force() is False) and @@ -525,11 +525,11 @@ def wav_or_set_msg(chan, new_voltage): new_voltage = _clipto( old_voltage, self.vranges[chan][new_vrange]['Min'], self.vranges[chan][new_vrange]['Max']) - message += 'vol {} {};'.format(chan, new_vrange) + message += f'vol {chan} {new_vrange};' message += wav_or_set_msg(chan, new_voltage) # Current sensor relay off->on after voltage relay on->off: if new_irange > old_irange and new_vrange < old_vrange: - message += ';cur {} {}'.format(chan, new_irange) + message += f';cur {chan} {new_irange}' self.channels[chan-1].v.vals = self._v_vals(chan, new_vrange) self.channels[chan-1].v.cache.set(new_voltage) @@ -634,7 +634,7 @@ def _setsync(self, chan, sync): if chan not in range(1, self.num_chans+1): raise ValueError( - 'Channel number must be 1-{}.'.format(self.num_chans)) + f'Channel number must be 1-{self.num_chans}.') if sync == 0: oldsync = self.channels[chan-1].sync.cache() @@ -642,7 +642,7 @@ def _setsync(self, chan, sync): self._syncoutputs.pop(chan, None) # free the previously assigned sync if oldsync is not None: - self.write('syn {} 0 0 0'.format(oldsync)) + self.write(f'syn {oldsync} 0 0 0') return # Make sure to clear hardware an _syncoutpus appropriately @@ -650,13 +650,13 @@ def _setsync(self, chan, sync): # Changing SYNC port for a channel oldsync = self.channels[chan-1].sync.cache() if sync != oldsync: - self.write('syn {} 0 0 0'.format(oldsync)) + self.write(f'syn {oldsync} 0 0 0') elif sync in self._syncoutputs.values(): # Assigning an already used SYNC port to a different channel oldchan = [ch for ch, sy in self._syncoutputs.items() if sy == sync] self._syncoutputs.pop(oldchan[0], None) - self.write('syn {} 0 0 0'.format(sync)) + self.write(f'syn {sync} 0 0 0') self._syncoutputs[chan] = sync return @@ -672,7 +672,7 @@ def print_syncs(self): Print assigned SYNC ports, sorted by channel number """ for chan, sync in sorted(self._syncoutputs.items()): - print('Channel {}, SYNC: {} (V/s)'.format(chan, sync)) + print(f'Channel {chan}, SYNC: {sync} (V/s)') def _setslope(self, chan, slope): """ @@ -688,7 +688,7 @@ def _setslope(self, chan, slope): """ if chan not in range(1, self.num_chans+1): raise ValueError( - 'Channel number must be 1-{}.'.format(self.num_chans)) + f'Channel number must be 1-{self.num_chans}.') if slope == 'Inf': # Set the channel in DC mode @@ -723,7 +723,7 @@ def print_slopes(self): Print the finite slopes assigned to channels, sorted by channel number """ for chan, slope in sorted(self._slopes.items()): - print('Channel {}, slope: {} (V/s)'.format(chan, slope)) + print(f'Channel {chan}, slope: {slope} (V/s)') def _get_minmax_outputvoltage(self, channel, vrange_int): """ @@ -735,11 +735,11 @@ def _get_minmax_outputvoltage(self, channel, vrange_int): # result, So this is designed for verbose mode if channel not in range(1, self.num_chans+1): raise ValueError( - 'Channel number must be 1-{}.'.format(self.num_chans)) + f'Channel number must be 1-{self.num_chans}.') if vrange_int not in range(0, 2): raise ValueError('Range must be 0 or 1.') - self.write('rang {} {}'.format(channel, vrange_int)) + self.write(f'rang {channel} {vrange_int}') fw_str = self._write_response return {'Min': float(fw_str.split('MIN:')[1].split('MAX')[0].strip()), 'Max': float(fw_str.split('MAX:')[1].strip())} @@ -769,7 +769,7 @@ def write(self, cmd): available in `_write_response` """ - LOG.debug("Writing to instrument {}: {}".format(self.name, cmd)) + LOG.debug(f"Writing to instrument {self.name}: {cmd}") _, ret_code = self.visa_handle.write(cmd) self.check_error(ret_code) for _ in range(cmd.count(';')+1): @@ -865,7 +865,7 @@ def _get_functiongenerator(self, chan): if len(self._assigned_fgs) < 8: fg = min(self._fgs.difference( - set([g.fg for g in self._assigned_fgs.values()]))) + {g.fg for g in self._assigned_fgs.values()})) self._assigned_fgs[chan] = Generator(fg) else: # If no available fgs, see if one is soon to be ready @@ -994,7 +994,7 @@ def ramp_voltages_2d(self, slow_chans, slow_vstart, slow_vend, for chan in channellist: if chan not in range(1, self.num_chans+1): raise ValueError( - 'Channel number must be 1-{}.'.format(self.num_chans)) + f'Channel number must be 1-{self.num_chans}.') if not (chan in self._assigned_fgs): self._get_functiongenerator(chan) @@ -1061,7 +1061,7 @@ def ramp_voltages_2d(self, slow_chans, slow_vstart, slow_vend, # Fire trigger to start generators simultaneously, saving communication # time by not using triggers for single channel ramping if trigger > 0: - self.write('trig {}'.format(trigger)) + self.write(f'trig {trigger}') # Update fgs dict so that we know when the ramp is supposed to end time_ramp = slow_steps * fast_steps * step_length_ms / 1000 From f60cebf0e5bdd790e389ae637eab4fc9025709fc Mon Sep 17 00:00:00 2001 From: Trevor Morgan <63689909+trevormorgan@users.noreply.github.com> Date: Sun, 18 Oct 2020 21:40:11 -0700 Subject: [PATCH 16/32] chane import visa to pyvisa visa --- qcodes_contrib_drivers/drivers/QDevil/QDAC1.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/qcodes_contrib_drivers/drivers/QDevil/QDAC1.py b/qcodes_contrib_drivers/drivers/QDevil/QDAC1.py index 8021e31d7..494eea2f9 100644 --- a/qcodes_contrib_drivers/drivers/QDevil/QDAC1.py +++ b/qcodes_contrib_drivers/drivers/QDevil/QDAC1.py @@ -4,7 +4,7 @@ # Version 2.1 QDevil 2020-02-10 import time -import visa +import pyvisa as visa import logging from functools import partial From b4ec4fb1ef4e99e22dbb644bcc80cbcc600aa671 Mon Sep 17 00:00:00 2001 From: Jens Hedegaard Nielsen Date: Fri, 23 Oct 2020 12:43:05 +0200 Subject: [PATCH 17/32] Qdac drivers: Don't check return from visa write --- qcodes_contrib_drivers/drivers/QDevil/QDAC1.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/qcodes_contrib_drivers/drivers/QDevil/QDAC1.py b/qcodes_contrib_drivers/drivers/QDevil/QDAC1.py index 494eea2f9..49fbe4200 100644 --- a/qcodes_contrib_drivers/drivers/QDevil/QDAC1.py +++ b/qcodes_contrib_drivers/drivers/QDevil/QDAC1.py @@ -770,8 +770,7 @@ def write(self, cmd): """ LOG.debug(f"Writing to instrument {self.name}: {cmd}") - _, ret_code = self.visa_handle.write(cmd) - self.check_error(ret_code) + self.visa_handle.write(cmd) for _ in range(cmd.count(';')+1): self._write_response = self.visa_handle.read() From 409ba5e0f3519ff9b0bcb47f672786d57c17efd9 Mon Sep 17 00:00:00 2001 From: "Jens H. Nielsen" Date: Fri, 23 Oct 2020 18:55:42 +0200 Subject: [PATCH 18/32] Add types to QDevil Qdac --- .../drivers/QDevil/QDAC1.py | 195 ++++++++++-------- 1 file changed, 111 insertions(+), 84 deletions(-) diff --git a/qcodes_contrib_drivers/drivers/QDevil/QDAC1.py b/qcodes_contrib_drivers/drivers/QDevil/QDAC1.py index 49fbe4200..1594177ed 100644 --- a/qcodes_contrib_drivers/drivers/QDevil/QDAC1.py +++ b/qcodes_contrib_drivers/drivers/QDevil/QDAC1.py @@ -3,8 +3,10 @@ # the instrument drivers package # Version 2.1 QDevil 2020-02-10 +from typing import Optional, Sequence, Dict, Any, Tuple, Union import time import pyvisa as visa +from pyvisa.resources.serial import SerialInstrument import logging from functools import partial @@ -12,6 +14,7 @@ from qcodes.instrument.channel import MultiChannelInstrumentParameter from qcodes.instrument.visa import VisaInstrument from qcodes.utils import validators as vals +from qcodes.instrument.parameter import ParamRawDataType from enum import Enum from collections import namedtuple @@ -32,12 +35,12 @@ class Mode(Enum): vhigh_ilow = _ModeTuple(v=0, i=0) vlow_ilow = _ModeTuple(v=1, i=0) - def get_label(self): + def get_label(self) -> str: _MODE_LABELS = { - self.vhigh_ihigh: "V range high / I range high", - self.vhigh_ilow: "V range high / I range low", - self.vlow_ilow: "V range low / I range low"} - return _MODE_LABELS[self] + "vhigh_ihigh": "V range high / I range high", + "vhigh_ilow": "V range high / I range low", + "vlow_ilow": "V range low / I range low"} + return _MODE_LABELS[self.name] class Waveform: @@ -49,9 +52,9 @@ class Waveform: all_waveforms = [sine, square, triangle, staircase] -class Generator(): +class Generator: # Class used in the internal book keeping of generators - def __init__(self, generator_number): + def __init__(self, generator_number: int): self.fg = generator_number self. t_end = 9.9e9 @@ -66,12 +69,12 @@ class QDacChannel(InstrumentChannel): mode_force lfag is False (default). """ - def __init__(self, parent, name, channum): + def __init__(self, parent: "QDac", name: str, channum: int): """ Args: - parent (Instrument): The instrument to which the channel belongs. - name (str): The name of the channel - channum (int): The number of the channel (1-24 or 1-48) + parent: The instrument to which the channel belongs. + name: The name of the channel + channum: The number of the channel (1-24 or 1-48) """ super().__init__(parent, name) @@ -134,7 +137,10 @@ def __init__(self, parent, name, channum): initial_value=0.01 ) - def snapshot_base(self, update=False, params_to_skip_update=None): + def snapshot_base( + self, + update: Optional[bool] = False, + params_to_skip_update: Optional[Sequence[str]] = None) -> Dict: update_currents = self._parent._update_currents and update if update and not self._parent._get_status_performed: self._parent._update_cache(update_currents=update_currents) @@ -155,10 +161,13 @@ class QDacMultiChannelParameter(MultiChannelInstrumentParameter): The class to be returned by __getattr__ of the ChannelList. Here customised for fast multi-readout of voltages. """ - def __init__(self, channels, param_name, *args, **kwargs): + def __init__(self, channels: Sequence[InstrumentChannel], + param_name: str, + *args: Any, + **kwargs: Any): super().__init__(channels, param_name, *args, **kwargs) - def get_raw(self): + def get_raw(self) -> Tuple[ParamRawDataType, ...]: """ Return a tuple containing the data from each of the channels in the list. @@ -199,17 +208,17 @@ class QDac(VisaInstrument): max_status_age = 1 def __init__(self, - name, - address, - update_currents=False, - **kwargs): + name: str, + address: str, + update_currents: bool = False, + **kwargs: Any): """ Instantiates the instrument. Args: - name (str): The instrument name used by qcodes - address (str): The VISA name of the resource - update_currents (bool): Whether to query all channels for their + name: The instrument name used by qcodes + address: The VISA name of the resource + update_currents: Whether to query all channels for their current sensor value on startup, which takes about 0.5 sec per channel. Default: False. @@ -221,6 +230,7 @@ def __init__(self, handle = self.visa_handle self._get_status_performed = False + assert isinstance(handle, SerialInstrument) # Communication setup + firmware check handle.baud_rate = 480600 handle.parity = visa.constants.Parity(0) @@ -296,22 +306,22 @@ def __init__(self, self._load_state() LOG.info('[+] Done') - def _reset_bookkeeping(self): + def _reset_bookkeeping(self) -> None: """ Resets all internal variables used for ramping and synchronization outputs. """ # Assigned slopes. Entries will eventually be {chan: slope} - self._slopes = {} + self._slopes: Dict[int, Union[str, float]] = {} # Function generators and triggers (used in ramping) self._fgs = set(range(1, 9)) - self._assigned_fgs = {} # {chan: fg} + self._assigned_fgs: Dict[int, Generator] = {} # {chan: fg} self._trigs = set(range(1, 10)) - self._assigned_triggers = {} # {fg: trigger} + self._assigned_triggers: Dict[int, int] = {} # {fg: trigger} # Sync channels - self._syncoutputs = {} # {chan: syncoutput} + self._syncoutputs: Dict[int, int] = {} # {chan: syncoutput} - def _load_state(self): + def _load_state(self) -> None: """ Used as part of initiaisation. DON'T use _load_state() separately.\n Updates internal book keeping of running function generators. @@ -333,10 +343,10 @@ def _load_state(self): # as it may be ramped explicitely by the user # We assume that generators are running, but we cannot know self.write(f'wav {chan}') - fg, amplitude_str, offset_str = self._write_response.split(',') + fg_str, amplitude_str, offset_str = self._write_response.split(',') amplitude = float(amplitude_str) offset = float(offset_str) - fg = int(fg) + fg = int(fg_str) if fg in range(1, 9): voltage = self.channels[ch_idx].v.get() time_now = time.time() @@ -346,9 +356,9 @@ def _load_state(self): # Probably this driver is involved if a stair case is assigned if waveform == Waveform.staircase: if len(response) == 6: - step_length_ms, no_steps, rep, rep_remain, trigger \ + step_length_ms, no_steps, rep, rep_remain_str, trigger \ = response[1:6] - rep_remain = int(rep_remain) + rep_remain = int(rep_remain_str) else: step_length_ms, no_steps, rep, trigger = response[1:5] rep_remain = int(rep) @@ -363,14 +373,14 @@ def _load_state(self): * ramp_time + time_now + 0.001 else: if waveform == Waveform.sine: - period_ms, rep, rep_remain, trigger = response[1:5] + period_ms, rep, rep_remain_str, trigger = response[1:5] else: - period_ms, _, rep, rep_remain, trigger = response[1:6] + period_ms, _, rep, rep_remain_str, trigger = response[1:6] if int(rep) == -1: time_end = time_now + 315360000 # 10 years from now else: # +1 is just a safe guard time_end = time_now + 0.001 \ - * (int(rep_remain)+1) * float(period_ms) + * (int(rep_remain_str)+1) * float(period_ms) self._assigned_fgs[chan] = Generator(fg) self._assigned_fgs[chan].t_end = time_end @@ -386,7 +396,7 @@ def _load_state(self): self.channels[ch_idx].sync_duration( float(duration_ms)/1000) - def reset(self, update_currents=False): + def reset(self, update_currents: bool = False) -> None: """ Resets the instrument setting all channels to zero output voltage and all parameters to their default values, including removing any @@ -411,11 +421,13 @@ def reset(self, update_currents=False): self.mode_force(False) self._reset_bookkeeping() - def snapshot_base(self, update=False, params_to_skip_update=None): - update_currents = self._update_currents and update + def snapshot_base(self, + update: Optional[bool] = False, + params_to_skip_update: Optional[Sequence[str]] = None) -> Dict: + update_currents = self._update_currents and update is True if update: self._update_cache(update_currents=update_currents) - self._get_status_performed = True + self._get_status_performed = True # call _update_cache rather than getting the status individually for # each parameter. We set _get_status_performed to True # to indicate that each update channel does not need to call this @@ -431,13 +443,13 @@ def snapshot_base(self, update=False, params_to_skip_update=None): # Channel gets/sets ######################### - def _set_voltage(self, chan, v_set): + def _set_voltage(self, chan: int, v_set: float) -> None: """ set_cmd for the chXX_v parameter Args: - chan (int): The 1-indexed channel number - v_set (float): The target voltage + chan: The 1-indexed channel number + v_set: The target voltage If a finite slope has been assigned, a function generator will ramp the voltage. @@ -456,13 +468,13 @@ def _set_voltage(self, chan, v_set): self.write('wav {ch} 0 0 0;set {ch} {voltage:.6f}' .format(ch=chan, voltage=v_set)) - def _set_mode(self, chan, new_mode): + def _set_mode(self, chan: int, new_mode: Mode) -> None: """ set_cmd for the QDAC's mode (combined voltage and current sense range). It is not possible to switch from voltage range without setting the the volage to zero first or set the global mode_force parameter True. """ - def _clipto(value, min_, max_): + def _clipto(value: float, min_: float, max_: float) -> float: errmsg = ("Voltage is outside the bounds of the new voltage range" " and is therefore clipped.") if value > max_: @@ -476,7 +488,7 @@ def _clipto(value, min_, max_): # It is not possible ot say if the channel is connected to # a generator, so we need to ask. - def wav_or_set_msg(chan, new_voltage): + def wav_or_set_msg(chan: int, new_voltage: float) -> str: self.write(f'wav {chan}') fw_str = self._write_response gen, _, _ = fw_str.split(',') @@ -535,14 +547,14 @@ def wav_or_set_msg(chan, new_voltage): self.write(message) - def _v_vals(self, chan, vrange_int): + def _v_vals(self, chan: int, vrange_int: int) -> vals.Numbers: """ Returns the validator for the specified voltage range. """ return vals.Numbers(self.vranges[chan][vrange_int]['Min'], self.vranges[chan][vrange_int]['Max']) - def _update_v_validators(self): + def _update_v_validators(self) -> None: """ Command for setting all 'v' limits ('vals') of all channels to the actual calibrated output limits for the range each individual channel @@ -552,23 +564,23 @@ def _update_v_validators(self): vrange = self.channels[chan-1].mode.value.v self.channels[chan-1].v.vals = self._v_vals(chan, vrange) - def _num_verbose(self, s): + def _num_verbose(self, s: str) -> float: """ Turns a return value from the QDac into a number. If the QDac is in verbose mode, this involves stripping off the value descriptor. """ if self._verbose: - s = s.split[': '][-1] + s = s.split(': ')[-1] return float(s) - def _current_parser(self, s): + def _current_parser(self, s: str) -> float: """ Parser for chXX_i parameter (converts from uA to A) """ return 1e-6*self._num_verbose(s) - def _update_cache(self, update_currents=False): + def _update_cache(self, update_currents: bool = False) -> None: """ Function to query the instrument and get the status of all channels. Takes a while to finish. @@ -622,13 +634,13 @@ def _update_cache(self, update_currents=False): for chan in self._chan_range: self.channels[chan-1].i.get() - def _setsync(self, chan, sync): + def _setsync(self, chan: int, sync: int) -> None: """ set_cmd for the chXX_sync parameter. Args: - chan (int): The channel number (1-48 or 1-24) - sync (int): The associated sync output (1-3 on 24 ch units + chan: The channel number (1-48 or 1-24) + sync: The associated sync output (1-3 on 24 ch units or 1-5 on 48 ch units). 0 means 'unassign' """ @@ -661,27 +673,27 @@ def _setsync(self, chan, sync): self._syncoutputs[chan] = sync return - def _getsync(self, chan): + def _getsync(self, chan: int) -> int: """ get_cmd of the chXX_sync parameter """ return self._syncoutputs.get(chan, 0) - def print_syncs(self): + def print_syncs(self) -> None: """ Print assigned SYNC ports, sorted by channel number """ for chan, sync in sorted(self._syncoutputs.items()): print(f'Channel {chan}, SYNC: {sync} (V/s)') - def _setslope(self, chan, slope): + def _setslope(self, chan: int, slope: Union[float, str]) -> None: """ set_cmd for the chXX_slope parameter, the maximum slope of a channel. With a finite slope the channel will be ramped using a generator. Args: - chan (int): The channel number (1-24 or 1-48) - slope (Union[float, str]): The slope in V/s. + chan: The channel number (1-24 or 1-48) + slope: The slope in V/s. Write 'Inf' to release the channelas slope channel and to release the associated function generator. The output rise time will now only depend on the analog electronics. @@ -700,7 +712,7 @@ def _setslope(self, chan, slope): try: fg = self._assigned_fgs[chan] self._assigned_fgs[chan].t_end = 0 - self._assigned_triggers.pop(fg) + self._assigned_triggers.pop(fg.fg) except KeyError: pass @@ -712,20 +724,20 @@ def _setslope(self, chan, slope): else: self._slopes[chan] = slope - def _getslope(self, chan): + def _getslope(self, chan: int) -> Union[str, float]: """ get_cmd of the chXX_slope parameter """ return self._slopes.get(chan, 'Inf') - def print_slopes(self): + def print_slopes(self) -> None: """ Print the finite slopes assigned to channels, sorted by channel number """ for chan, slope in sorted(self._slopes.items()): print(f'Channel {chan}, slope: {slope} (V/s)') - def _get_minmax_outputvoltage(self, channel, vrange_int): + def _get_minmax_outputvoltage(self, channel: int, vrange_int: int) -> Dict[str, float]: """ Returns a dictionary of the calibrated Min and Max output voltages of 'channel' for the voltage given range (0,1) given by @@ -744,7 +756,7 @@ def _get_minmax_outputvoltage(self, channel, vrange_int): return {'Min': float(fw_str.split('MIN:')[1].split('MAX')[0].strip()), 'Max': float(fw_str.split('MAX:')[1].strip())} - def _update_voltage_ranges(self): + def _update_voltage_ranges(self) -> None: # Get all calibrated min/max output values, requires verbose on # in firmware version 1.07 self.write('ver 1') @@ -755,7 +767,7 @@ def _update_voltage_ranges(self): 1: self._get_minmax_outputvoltage(chan, 1)}}) self.write('ver 0') - def write(self, cmd): + def write(self, cmd: str) -> None: """ QDac always returns something even from set commands, even when verbose mode is off, so we'll override write to take this out @@ -774,14 +786,16 @@ def write(self, cmd): for _ in range(cmd.count(';')+1): self._write_response = self.visa_handle.read() - def read(self): + def read(self) -> str: return self.visa_handle.read() - def _wait_and_clear(self, delay=0.5): + def _wait_and_clear(self, delay: float = 0.5) -> None: time.sleep(delay) self.visa_handle.clear() - def connect_message(self, idn_param='IDN', begin_time=None): + def connect_message(self, + idn_param: str = 'IDN', + begin_time: Optional[float] = None) -> None: """ Override of the standard Instrument class connect_message. Usually, the response to `*IDN?` is printed. Here, the @@ -791,7 +805,7 @@ def connect_message(self, idn_param='IDN', begin_time=None): LOG.info('Connected to QDAC on {}, {}'.format( self._address, self.visa_handle.read())) - def _get_firmware_version(self): + def _get_firmware_version(self) -> float: """ Check if the "version" command reponds. If so we probbaly have a QDevil QDAC, and the version number is returned. Otherwise 0.0 is returned. @@ -806,7 +820,7 @@ def _get_firmware_version(self): fw_version = 0.0 return fw_version - def _get_number_of_channels(self): + def _get_number_of_channels(self) -> int: """ Returns the number of channels for the instrument """ @@ -814,7 +828,7 @@ def _get_number_of_channels(self): fw_str = self._write_response return 8*int(fw_str.strip("numberOfBoards:")) - def print_overview(self, update_currents=False): + def print_overview(self, update_currents: bool = False) -> None: """ Pretty-prints the status of the QDac """ @@ -845,7 +859,7 @@ def print_overview(self, update_currents=False): print(line) - def _get_functiongenerator(self, chan): + def _get_functiongenerator(self, chan: int) -> int: """ Function for getting a free generator (of 8 available) for a channel. Used as helper function for ramp_voltages, but may also be used if the @@ -903,7 +917,12 @@ def _get_functiongenerator(self, chan): number of ramped channels. Or increase fgs_timeout.''') return fg - def ramp_voltages(self, channellist, v_startlist, v_endlist, ramptime): + def ramp_voltages( + self, + channellist: Sequence[int], + v_startlist: Sequence[float], + v_endlist: Sequence[float], + ramptime: float) -> float: """ Function for smoothly ramping one channel or more channels simultaneously (max. 8). This is a shallow interface to @@ -937,35 +956,43 @@ def ramp_voltages(self, channellist, v_startlist, v_endlist, ramptime): fast_vend=v_endlist, step_length=0.001, slow_steps=1, fast_steps=steps) - def ramp_voltages_2d(self, slow_chans, slow_vstart, slow_vend, - fast_chans, fast_vstart, fast_vend, - step_length, slow_steps, fast_steps): + def ramp_voltages_2d( + self, + slow_chans: Sequence[int], + slow_vstart: Sequence[float], + slow_vend: Sequence[float], + fast_chans: Sequence[int], + fast_vstart: Sequence[float], + fast_vend: Sequence[float], + step_length: float, + slow_steps: int, + fast_steps: int) -> float: """ Function for smoothly ramping two channel groups simultaneously with one slow (x) and one fast (y) group. used by 'ramp_voltages' where x is empty. Function generators and triggers are assigned automatically. Args: - slow_chans: List (int) of channels to be ramped (1 indexed) in + slow_chans: List of channels to be ramped (1 indexed) in the slow-group\n - slow_vstart: List (int) of voltages to ramp from in the + slow_vstart: List of voltages to ramp from in the slow-group. MAY BE EMPTY. But if provided, time is saved by NOT reading the present values from the instrument.\n - slow_vend: list (int) of voltages to ramp to in the slow-group. + slow_vend: list of voltages to ramp to in the slow-group. - fast_chans: List (int) of channels to be ramped (1 indexed) in + fast_chans: List of channels to be ramped (1 indexed) in the fast-group.\n - fast_vstart: List (int) of voltages to ramp from in the + fast_vstart: List of voltages to ramp from in the fast-group. MAY BE EMPTY. But if provided, time is saved by NOT reading the present values from the instrument.\n - fast_vend: list (int) of voltages to ramp to in the fast-group. + fast_vend: list of voltages to ramp to in the fast-group. - step_length: (float) Time spent at each step in seconds + step_length: Time spent at each step in seconds (min. 0.001) multiple of 1 ms.\n - slow_steps: (int) number of steps in the slow direction.\n - fast_steps: (int) number of steps in the fast direction.\n + slow_steps: number of steps in the slow direction.\n + fast_steps: number of steps in the fast direction.\n Returns: Estimated time of the excecution of the 2D scan.\n From a0bb0215fdbb28131a1bc6bf07abf92593e06d4a Mon Sep 17 00:00:00 2001 From: Jens Hedegaard Nielsen Date: Mon, 26 Oct 2020 15:31:46 +0100 Subject: [PATCH 19/32] Update qcodes/instrument_drivers/QDevil/QDevil_QDAC.py Co-authored-by: Mikhail Astafev --- qcodes_contrib_drivers/drivers/QDevil/QDAC1.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/qcodes_contrib_drivers/drivers/QDevil/QDAC1.py b/qcodes_contrib_drivers/drivers/QDevil/QDAC1.py index 1594177ed..866fff08f 100644 --- a/qcodes_contrib_drivers/drivers/QDevil/QDAC1.py +++ b/qcodes_contrib_drivers/drivers/QDevil/QDAC1.py @@ -56,7 +56,7 @@ class Generator: # Class used in the internal book keeping of generators def __init__(self, generator_number: int): self.fg = generator_number - self. t_end = 9.9e9 + self.t_end = 9.9e9 class QDacChannel(InstrumentChannel): From ee97549e18f920303ded9a22b283cc443fde52a1 Mon Sep 17 00:00:00 2001 From: "Jens H. Nielsen" Date: Mon, 23 Nov 2020 14:18:45 +0100 Subject: [PATCH 20/32] explicit generics in qdev and qdevil drivers --- qcodes_contrib_drivers/drivers/QDevil/QDAC1.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/qcodes_contrib_drivers/drivers/QDevil/QDAC1.py b/qcodes_contrib_drivers/drivers/QDevil/QDAC1.py index 866fff08f..5c596a28a 100644 --- a/qcodes_contrib_drivers/drivers/QDevil/QDAC1.py +++ b/qcodes_contrib_drivers/drivers/QDevil/QDAC1.py @@ -140,7 +140,7 @@ def __init__(self, parent: "QDac", name: str, channum: int): def snapshot_base( self, update: Optional[bool] = False, - params_to_skip_update: Optional[Sequence[str]] = None) -> Dict: + params_to_skip_update: Optional[Sequence[str]] = None) -> Dict[Any, Any]: update_currents = self._parent._update_currents and update if update and not self._parent._get_status_performed: self._parent._update_cache(update_currents=update_currents) @@ -423,7 +423,7 @@ def reset(self, update_currents: bool = False) -> None: def snapshot_base(self, update: Optional[bool] = False, - params_to_skip_update: Optional[Sequence[str]] = None) -> Dict: + params_to_skip_update: Optional[Sequence[str]] = None) -> Dict[Any, Any]: update_currents = self._update_currents and update is True if update: self._update_cache(update_currents=update_currents) From c8ca70a5ff828427c22a2ffaca1ea1c423324f0a Mon Sep 17 00:00:00 2001 From: "Jens H. Nielsen" Date: Mon, 23 Nov 2020 14:42:40 +0100 Subject: [PATCH 21/32] fix formatting --- qcodes_contrib_drivers/drivers/QDevil/QDAC1.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/qcodes_contrib_drivers/drivers/QDevil/QDAC1.py b/qcodes_contrib_drivers/drivers/QDevil/QDAC1.py index 5c596a28a..fd69f9a7f 100644 --- a/qcodes_contrib_drivers/drivers/QDevil/QDAC1.py +++ b/qcodes_contrib_drivers/drivers/QDevil/QDAC1.py @@ -140,7 +140,8 @@ def __init__(self, parent: "QDac", name: str, channum: int): def snapshot_base( self, update: Optional[bool] = False, - params_to_skip_update: Optional[Sequence[str]] = None) -> Dict[Any, Any]: + params_to_skip_update: Optional[Sequence[str]] = None + ) -> Dict[Any, Any]: update_currents = self._parent._update_currents and update if update and not self._parent._get_status_performed: self._parent._update_cache(update_currents=update_currents) From f7907574c4743b05d3d1e8f9bba6faa1a11d568c Mon Sep 17 00:00:00 2001 From: "Jens H. Nielsen" Date: Mon, 23 Nov 2020 14:51:53 +0100 Subject: [PATCH 22/32] another line ending --- qcodes_contrib_drivers/drivers/QDevil/QDAC1.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/qcodes_contrib_drivers/drivers/QDevil/QDAC1.py b/qcodes_contrib_drivers/drivers/QDevil/QDAC1.py index fd69f9a7f..3c12b96cf 100644 --- a/qcodes_contrib_drivers/drivers/QDevil/QDAC1.py +++ b/qcodes_contrib_drivers/drivers/QDevil/QDAC1.py @@ -422,9 +422,11 @@ def reset(self, update_currents: bool = False) -> None: self.mode_force(False) self._reset_bookkeeping() - def snapshot_base(self, - update: Optional[bool] = False, - params_to_skip_update: Optional[Sequence[str]] = None) -> Dict[Any, Any]: + def snapshot_base( + self, + update: Optional[bool] = False, + params_to_skip_update: Optional[Sequence[str]] = None + ) -> Dict[Any, Any]: update_currents = self._update_currents and update is True if update: self._update_cache(update_currents=update_currents) From c12a5f9ac7fdb2b01d0abad1a377868d0085fffd Mon Sep 17 00:00:00 2001 From: "Jens H. Nielsen" Date: Mon, 25 Jan 2021 09:06:03 +0100 Subject: [PATCH 23/32] namedtuple ensure that name matches variable name --- qcodes_contrib_drivers/drivers/QDevil/QDAC1.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/qcodes_contrib_drivers/drivers/QDevil/QDAC1.py b/qcodes_contrib_drivers/drivers/QDevil/QDAC1.py index 3c12b96cf..ceacb7776 100644 --- a/qcodes_contrib_drivers/drivers/QDevil/QDAC1.py +++ b/qcodes_contrib_drivers/drivers/QDevil/QDAC1.py @@ -21,7 +21,7 @@ LOG = logging.getLogger(__name__) -_ModeTuple = namedtuple('Mode', 'v i') +_ModeTuple = namedtuple('_ModeTuple', 'v i') class Mode(Enum): From a0eca2b57fe72f2fc6baf9e88c2b8ef1f8fed389 Mon Sep 17 00:00:00 2001 From: "Jens H. Nielsen" Date: Fri, 21 May 2021 08:21:59 +0200 Subject: [PATCH 24/32] rerun pyupgrade precomit hook --- .../drivers/QDevil/QDAC1.py | 45 ++++++++++--------- 1 file changed, 25 insertions(+), 20 deletions(-) diff --git a/qcodes_contrib_drivers/drivers/QDevil/QDAC1.py b/qcodes_contrib_drivers/drivers/QDevil/QDAC1.py index ceacb7776..11e2407fe 100644 --- a/qcodes_contrib_drivers/drivers/QDevil/QDAC1.py +++ b/qcodes_contrib_drivers/drivers/QDevil/QDAC1.py @@ -3,20 +3,24 @@ # the instrument drivers package # Version 2.1 QDevil 2020-02-10 -from typing import Optional, Sequence, Dict, Any, Tuple, Union +import logging import time +from collections import namedtuple +from enum import Enum +from functools import partial +from typing import Any, Dict, Optional, Sequence, Tuple, Union + import pyvisa as visa from pyvisa.resources.serial import SerialInstrument -import logging -from functools import partial -from qcodes.instrument.channel import InstrumentChannel, ChannelList -from qcodes.instrument.channel import MultiChannelInstrumentParameter +from qcodes.instrument.channel import ( + ChannelList, + InstrumentChannel, + MultiChannelInstrumentParameter, +) +from qcodes.instrument.parameter import ParamRawDataType from qcodes.instrument.visa import VisaInstrument from qcodes.utils import validators as vals -from qcodes.instrument.parameter import ParamRawDataType -from enum import Enum -from collections import namedtuple LOG = logging.getLogger(__name__) @@ -839,17 +843,18 @@ def print_overview(self, update_currents: bool = False) -> None: self._update_cache(update_currents=update_currents) for ii in range(self.num_chans): - line = 'Channel {} \n'.format(ii+1) - line += ' Voltage: {} ({}).\n'.format( - self.channels[ii].v.cache(), self.channels[ii].v.unit) - line += ' Current: {} ({}).\n'.format( - self.channels[ii].i.cache.get(get_if_invalid=False), - self.channels[ii].i.unit) - line += ' Mode: {}.\n'.format( - self.channels[ii].mode.cache().get_label()) - line += ' Slope: {} ({}).\n'.format( - self.channels[ii].slope.cache(), - self.channels[ii].slope.unit) + line = f"Channel {ii+1} \n" + line += " Voltage: {} ({}).\n".format( + self.channels[ii].v.cache(), self.channels[ii].v.unit + ) + line += " Current: {} ({}).\n".format( + self.channels[ii].i.cache.get(get_if_invalid=False), + self.channels[ii].i.unit, + ) + line += f" Mode: {self.channels[ii].mode.cache().get_label()}.\n" + line += " Slope: {} ({}).\n".format( + self.channels[ii].slope.cache(), self.channels[ii].slope.unit + ) if self.channels[ii].sync.cache() > 0: line += ' Sync Out: {}, Delay: {} ({}), '\ 'Duration: {} ({}).\n'.format( @@ -1072,7 +1077,7 @@ def ramp_voltages_2d( fg = self._assigned_fgs[ch].fg if trigger > 0: # Trigger 0 is not a trigger self._assigned_triggers[fg] = trigger - msg += 'wav {} {} {} {}'.format(ch, fg, amplitude, v_startlist[i]) + msg += f"wav {ch} {fg} {amplitude} {v_startlist[i]}" # using staircase = function 4 nsteps = slow_steps if ch in slow_chans else fast_steps repetitions = slow_steps if ch in fast_chans else 1 From b7c9e2fba5b51d9ba07810894955eb8cca69dc6d Mon Sep 17 00:00:00 2001 From: "Jens H. Nielsen" Date: Fri, 21 Jan 2022 22:06:59 +0100 Subject: [PATCH 25/32] replace locked List with tuple --- qcodes_contrib_drivers/drivers/QDevil/QDAC1.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/qcodes_contrib_drivers/drivers/QDevil/QDAC1.py b/qcodes_contrib_drivers/drivers/QDevil/QDAC1.py index 11e2407fe..4ed483292 100644 --- a/qcodes_contrib_drivers/drivers/QDevil/QDAC1.py +++ b/qcodes_contrib_drivers/drivers/QDevil/QDAC1.py @@ -268,9 +268,8 @@ def __init__(self, for i in self._chan_range: channel = QDacChannel(self, f'chan{i:02}', i) channels.append(channel) - self.add_submodule(f'ch{i:02}', channel) - channels.lock() - self.add_submodule('channels', channels) + self.add_submodule(f"ch{i:02}", channel) + self.add_submodule("channels", channels.to_channel_tuple()) # Updatechannel sync port validator according to number of boards self._num_syns = max(num_boards-1, 1) From 2a74cd39f221626255c9fad7ceb3b5713166562c Mon Sep 17 00:00:00 2001 From: Mikhail Astafev Date: Tue, 15 Feb 2022 18:36:58 +0100 Subject: [PATCH 26/32] Fix baud rate of QDevil QDAC to 460800 --- qcodes_contrib_drivers/drivers/QDevil/QDAC1.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/qcodes_contrib_drivers/drivers/QDevil/QDAC1.py b/qcodes_contrib_drivers/drivers/QDevil/QDAC1.py index 4ed483292..87028551e 100644 --- a/qcodes_contrib_drivers/drivers/QDevil/QDAC1.py +++ b/qcodes_contrib_drivers/drivers/QDevil/QDAC1.py @@ -237,7 +237,7 @@ def __init__(self, assert isinstance(handle, SerialInstrument) # Communication setup + firmware check - handle.baud_rate = 480600 + handle.baud_rate = 460800 handle.parity = visa.constants.Parity(0) handle.data_bits = 8 self.set_terminator('\n') From d4c301fd5bc195fda7fc1cf3d6f9c7624a4e756d Mon Sep 17 00:00:00 2001 From: "Jens H. Nielsen" Date: Thu, 9 Jun 2022 22:25:57 +0200 Subject: [PATCH 27/32] [WIP] Parameter module --- qcodes_contrib_drivers/drivers/QDevil/QDAC1.py | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/qcodes_contrib_drivers/drivers/QDevil/QDAC1.py b/qcodes_contrib_drivers/drivers/QDevil/QDAC1.py index 87028551e..5da6021f2 100644 --- a/qcodes_contrib_drivers/drivers/QDevil/QDAC1.py +++ b/qcodes_contrib_drivers/drivers/QDevil/QDAC1.py @@ -13,13 +13,9 @@ import pyvisa as visa from pyvisa.resources.serial import SerialInstrument -from qcodes.instrument.channel import ( - ChannelList, - InstrumentChannel, - MultiChannelInstrumentParameter, -) -from qcodes.instrument.parameter import ParamRawDataType -from qcodes.instrument.visa import VisaInstrument +from qcodes.instrument import ChannelList, InstrumentChannel, VisaInstrument +from qcodes.instrument.channel import MultiChannelInstrumentParameter +from qcodes.parameters import ParamRawDataType from qcodes.utils import validators as vals LOG = logging.getLogger(__name__) From 36f5aab2901127599f46fa210761c4f9bc0f1d0c Mon Sep 17 00:00:00 2001 From: "Jens H. Nielsen" Date: Mon, 13 Jun 2022 15:07:18 +0200 Subject: [PATCH 28/32] move MultiChannelParameter to parameters module --- qcodes_contrib_drivers/drivers/QDevil/QDAC1.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/qcodes_contrib_drivers/drivers/QDevil/QDAC1.py b/qcodes_contrib_drivers/drivers/QDevil/QDAC1.py index 5da6021f2..e2afb3a5d 100644 --- a/qcodes_contrib_drivers/drivers/QDevil/QDAC1.py +++ b/qcodes_contrib_drivers/drivers/QDevil/QDAC1.py @@ -14,8 +14,7 @@ from pyvisa.resources.serial import SerialInstrument from qcodes.instrument import ChannelList, InstrumentChannel, VisaInstrument -from qcodes.instrument.channel import MultiChannelInstrumentParameter -from qcodes.parameters import ParamRawDataType +from qcodes.parameters import MultiChannelInstrumentParameter, ParamRawDataType from qcodes.utils import validators as vals LOG = logging.getLogger(__name__) From a6086f11abe233d32f3996b2f3ff13f7c0b5f422 Mon Sep 17 00:00:00 2001 From: "Jens H. Nielsen" Date: Wed, 22 Jun 2022 11:30:50 +0200 Subject: [PATCH 29/32] update vals imports in drivers --- qcodes_contrib_drivers/drivers/QDevil/QDAC1.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/qcodes_contrib_drivers/drivers/QDevil/QDAC1.py b/qcodes_contrib_drivers/drivers/QDevil/QDAC1.py index e2afb3a5d..4a60fb6c6 100644 --- a/qcodes_contrib_drivers/drivers/QDevil/QDAC1.py +++ b/qcodes_contrib_drivers/drivers/QDevil/QDAC1.py @@ -13,9 +13,9 @@ import pyvisa as visa from pyvisa.resources.serial import SerialInstrument +from qcodes import validators as vals from qcodes.instrument import ChannelList, InstrumentChannel, VisaInstrument from qcodes.parameters import MultiChannelInstrumentParameter, ParamRawDataType -from qcodes.utils import validators as vals LOG = logging.getLogger(__name__) From 02f20d99adfaecd812ce5948db2f0562c21a7e2c Mon Sep 17 00:00:00 2001 From: "Jens H. Nielsen" Date: Fri, 12 Aug 2022 08:22:35 +0200 Subject: [PATCH 30/32] QDac don't compare the string trigger to 0 this will always be false --- qcodes_contrib_drivers/drivers/QDevil/QDAC1.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/qcodes_contrib_drivers/drivers/QDevil/QDAC1.py b/qcodes_contrib_drivers/drivers/QDevil/QDAC1.py index 4a60fb6c6..e001cc8a5 100644 --- a/qcodes_contrib_drivers/drivers/QDevil/QDAC1.py +++ b/qcodes_contrib_drivers/drivers/QDevil/QDAC1.py @@ -383,7 +383,7 @@ def _load_state(self) -> None: self._assigned_fgs[chan] = Generator(fg) self._assigned_fgs[chan].t_end = time_end - if trigger != 0: + if int(trigger) != 0: self._assigned_triggers[fg] = int(trigger) for syn in range(1, self._num_syns+1): self.write(f'syn {syn}') From 972bd441b9161a4c6c3cb9c1af80f5447f1158de Mon Sep 17 00:00:00 2001 From: "Jens H. Nielsen" Date: Fri, 23 Dec 2022 09:27:38 +0100 Subject: [PATCH 31/32] qdac fix potentially incorrect imports --- qcodes_contrib_drivers/drivers/QDevil/QDAC1.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/qcodes_contrib_drivers/drivers/QDevil/QDAC1.py b/qcodes_contrib_drivers/drivers/QDevil/QDAC1.py index e001cc8a5..1dffe4877 100644 --- a/qcodes_contrib_drivers/drivers/QDevil/QDAC1.py +++ b/qcodes_contrib_drivers/drivers/QDevil/QDAC1.py @@ -10,7 +10,8 @@ from functools import partial from typing import Any, Dict, Optional, Sequence, Tuple, Union -import pyvisa as visa +import pyvisa +import pyvisa.constants from pyvisa.resources.serial import SerialInstrument from qcodes import validators as vals @@ -233,7 +234,7 @@ def __init__(self, assert isinstance(handle, SerialInstrument) # Communication setup + firmware check handle.baud_rate = 460800 - handle.parity = visa.constants.Parity(0) + handle.parity = pyvisa.constants.Parity(0) handle.data_bits = 8 self.set_terminator('\n') handle.write_termination = '\n' From 74c72bbc5453aadb65fdccdd2e22244fa4a956dc Mon Sep 17 00:00:00 2001 From: "Jens H. Nielsen" Date: Mon, 16 Jan 2023 17:28:23 +0100 Subject: [PATCH 32/32] update reference to point to qcodes --- qcodes_contrib_drivers/drivers/QDevil/QDAC1.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/qcodes_contrib_drivers/drivers/QDevil/QDAC1.py b/qcodes_contrib_drivers/drivers/QDevil/QDAC1.py index 1dffe4877..fa3cf3370 100644 --- a/qcodes_contrib_drivers/drivers/QDevil/QDAC1.py +++ b/qcodes_contrib_drivers/drivers/QDevil/QDAC1.py @@ -1,6 +1,5 @@ # QCoDeS driver for the QDevil QDAC using channels -# Adapted by QDevil from "qdev\QDac_channels.py" in -# the instrument drivers package +# Adapted by QDevil from the qdev QDac driver in qcodes # Version 2.1 QDevil 2020-02-10 import logging