diff --git a/docs/examples/driver_examples/Qcodes example with QDac_channels.ipynb b/docs/examples/driver_examples/Qcodes example with QDac_channels.ipynb new file mode 100644 index 000000000000..6246a121d4fd --- /dev/null +++ b/docs/examples/driver_examples/Qcodes example with QDac_channels.ipynb @@ -0,0 +1,296 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Qcodes example with QDac_channels" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "import qcodes as qc\n", + "import numpy as np\n", + "\n", + "from time import sleep\n", + "\n", + "from qcodes.instrument_drivers.QDev.QDac_channels import QDac" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "collapsed": true + }, + "outputs": [], + "source": [ + "# Connect to the instrument\n", + "qdac = QDac('qdac', 'ASRL6::INSTR', update_currents=False)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Basic QDac Usage\n", + "\n", + "The QCoDeS QDac driver currently supports using\n", + " * 48 Output Channels\n", + " * 3 $\\times$ 6 temperature sensors\n", + "\n", + "Each output channel has six parameters:\n", + " * DC voltage\n", + " * DC voltage range\n", + " * Current out (read-only)\n", + " * Current out range\n", + " * slope\n", + " * sync\n", + "\n", + "The slope is the (maximal) slope in V/s that the channel can allow its voltage to change by. By default, all channels have a slope of \"Inf\". The slope can be changed dynamically, but no more than 8 channels can have a finite slope at any given time (this is due to hardware limitations)." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Current out is the current flowing through the channel this is read-only\n", + "print(qdac.ch01.i.get(), qdac.ch01.i.unit)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# The current range can be either 0 to 1 μA or 0 to 100 μA\n", + "print(qdac.ch01.irange.get())\n", + "# This is set with either 0 (0 to 1 μA) or 1 (0 to 100 μA) \n", + "qdac.ch01.irange.set(1)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# The DC voltage may directly be set and gotten\n", + "qdac.ch01.v.set(-1)\n", + "print('Channel 1 voltage: {} {}'.format(qdac.ch01.v.get(), qdac.ch01.v.unit))" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Multiple channels can be addressed simultaneously via the 'channels' list\n", + "qdac.channels[0:20].v.get()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Similarly, we may set them\n", + "qdac.channels[0:2].v.set(-1)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# The maximal voltage change (in V/s) may be set for each channel\n", + "qdac.ch01.slope.set(1)\n", + "qdac.ch02.slope.set(2)\n", + "# An overview may be printed (all other channels have 'Inf' slope)\n", + "qdac.printslopes()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "collapsed": true + }, + "outputs": [], + "source": [ + "# now setting channel 1 and 2 voltages will cause slow ramps to happen\n", + "qdac.ch01.v.set(0)\n", + "qdac.ch02.v.set(0)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Note that only 8 (or fewer) channels can have finite slopes at one time\n", + "# To make space for other channels, set the slope to inifite\n", + "qdac.ch01.slope('Inf')\n", + "qdac.printslopes()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "collapsed": true + }, + "outputs": [], + "source": [ + "# To each channel one may assign a sync channel:\n", + "qdac.ch02.sync(2) # sync output 2 will fire a 10 ms 5 V pulse when ch02 ramps\n", + "# note that even if no visible ramp is performed (i.e. ramping from 1 V to 1 V), a pulse is still fired.\n", + "\n", + "# The sync pulse settings can be modified\n", + "qdac.ch02.sync_delay(0) # The sync pulse delay (s)\n", + "qdac.ch02.sync_duration(25e-3) # The sync pulse duration (s). Default is 10 ms." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "qdac.ch02.v.set(0)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "collapsed": true + }, + "outputs": [], + "source": [ + "# syncs are unassigned by assigning sync 0\n", + "qdac.ch02.sync(0)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Attention!\n", + "\n", + "The v_range parameter is really controlling a 20 dB (amplitude factor 10) attenuator. Upon changing the vrange, the attenuator is **immediately** applied (or revoked). This will --irrespective of any slope set-- cause an instantaneous voltage change unless the channel voltage is zero. By default, all attenuators are off, and the voltage range is from -10 V to 10 V for all channels." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Here is a small example showing what to look out for\n", + "#\n", + "qdac.ch01.vrange.set(0) # Attenuation OFF (the default)\n", + "qdac.ch01.v.set(1.5)\n", + "qdac.ch01.vrange.set(1) # Attenuation ON\n", + "print(qdac.ch01.v.get()) # Returns 0.15 V\n", + "qdac.ch01.v.set(0.1)\n", + "qdac.ch01.vrange.set(0) # Attenuation OFF\n", + "print(qdac.ch01.v.get()) # returns 1 V" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Overview of channel settings\n", + "\n", + "The driver provides a method for pretty-printing the state of all channels. On startup, all channels are queried for voltage and current across them, but the current query is very slow (blame the hardware).\n", + "\n", + "The pretty-print method may or may not **update** the values for the currents, depending on the value of the `update_currents` flag. Each current reading takes some 200 ms, so updating all current values takes about 10 s." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "scrolled": false + }, + "outputs": [], + "source": [ + "qdac.print_overview(update_currents=True)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Temperature sensors\n", + "\n", + "Physically, the QDac consists of six boards each hosting eight channels. On three locations on each board, a temperature sensors is placed. These provide read-only parameters, named `tempX_Y` where `X` is the board number and `Y` the sensor number." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "print(qdac.temp0_0.get(), qdac.temp0_0.unit)\n", + "print(qdac.temp2_1.get(), qdac.temp0_0.unit)\n", + "print(qdac.temp5_2.get(), qdac.temp0_0.unit)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "collapsed": true + }, + "outputs": [], + "source": [ + "qdac.close()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "collapsed": true + }, + "outputs": [], + "source": [] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.5.3" + } + }, + "nbformat": 4, + "nbformat_minor": 1 +} diff --git a/qcodes/__init__.py b/qcodes/__init__.py index f17160163b11..bbd2bfe99266 100644 --- a/qcodes/__init__.py +++ b/qcodes/__init__.py @@ -45,6 +45,7 @@ from qcodes.instrument.base import Instrument from qcodes.instrument.ip import IPInstrument from qcodes.instrument.visa import VisaInstrument +from qcodes.instrument.channel import InstrumentChannel, ChannelList from qcodes.instrument.function import Function from qcodes.instrument.parameter import ( diff --git a/qcodes/instrument/base.py b/qcodes/instrument/base.py index 6c02d7c27388..2151d9cb2b77 100644 --- a/qcodes/instrument/base.py +++ b/qcodes/instrument/base.py @@ -34,6 +34,10 @@ class Instrument(Metadatable, DelegateAttributes): functions (Dict[Function]): All the functions supported by this instrument. Usually populated via ``add_function`` + + submodules (Dict[Metadatable]): All the submodules of this instrument + such as channel lists or logical groupings of parameters. + Usually populated via ``add_submodule`` """ shared_kwargs = () @@ -48,6 +52,7 @@ def __init__(self, name, **kwargs): super().__init__(**kwargs) self.parameters = {} self.functions = {} + self.submodules = {} self.name = str(name) @@ -306,6 +311,36 @@ def add_function(self, name, **kwargs): func = Function(name=name, instrument=self, **kwargs) self.functions[name] = func + def add_submodule(self, name, submodule): + """ + Bind one submodule to this instrument. + + Instrument subclasses can call this repeatedly in their ``__init__`` + method for every submodule of the instrument. + + Submodules can effectively be considered as instruments within the main + instrument, and should at minimum be snapshottable. For example, they can + be used to either store logical groupings of parameters, which may or may + not be repeated, or channel lists. + + Args: + name (str): how the submodule will be stored within ``instrument.submodules`` + and also how it can be addressed. + + submodule (Metadatable): The submodule to be stored. + + Raises: + KeyError: if this instrument already contains a submodule with this + name. + TypeError: if the submodule that we are trying to add is not an instance + of an Metadatable object. + """ + if name in self.submodules: + raise KeyError('Duplicate submodule name {}'.format(name)) + if not isinstance(submodule, Metadatable): + raise TypeError('Submodules must be metadatable.') + self.submodules[name] = submodule + def snapshot_base(self, update=False): """ State of the instrument as a JSON-compatible dict. @@ -321,6 +356,8 @@ def snapshot_base(self, update=False): for name, param in self.parameters.items()), 'functions': dict((name, func.snapshot(update=update)) for name, func in self.functions.items()), + 'submodules': dict((name, subm.snapshot(update=update)) + for name, subm in self.submodules.items()), '__class__': full_class(self), } for attr in set(self._meta_attrs): @@ -459,7 +496,7 @@ def ask_raw(self, cmd): # etc... # # - delegate_attr_dicts = ['parameters', 'functions'] + delegate_attr_dicts = ['parameters', 'functions', 'submodules'] def __getitem__(self, key): """Delegate instrument['name'] to parameter or function 'name'.""" diff --git a/qcodes/instrument/channel.py b/qcodes/instrument/channel.py new file mode 100644 index 000000000000..f5c441101247 --- /dev/null +++ b/qcodes/instrument/channel.py @@ -0,0 +1,394 @@ +""" Base class for the channel of an instrument """ +from typing import List, Tuple, Union + +from .base import Instrument +from .parameter import MultiParameter, ArrayParameter +from ..utils.metadata import Metadatable +from ..utils.helpers import full_class + + +class InstrumentChannel(Instrument): + """ + Base class for a channel in an instrument + + Args: + parent (Instrument): the instrument to which this channel should be attached + + name (str): the name of this channel + + Attributes: + name (str): the name of this channel + + parameters (Dict[Parameter]): All the parameters supported by this channel. + Usually populated via ``add_parameter`` + + functions (Dict[Function]): All the functions supported by this channel. + Usually populated via ``add_function`` + """ + + def __init__(self, parent, name, **kwargs): + # Initialize base classes of Instrument. We will overwrite what we want to do + # in the Instrument initializer + super().__init__(name=name, **kwargs) + + self.parameters = {} + self.functions = {} + + self.name = "{}_{}".format(parent.name, str(name)) + self._meta_attrs = ['name'] + + self._parent = parent + + def __repr__(self): + """Custom repr to give parent information""" + return '<{}: {} of {}: {}>'.format(type(self).__name__, + self.name, + type(self._parent).__name__, + self._parent.name) + + # We aren't a member of the global list of instruments, don't try and remove ourself + def __del__(self): + """ Does nothing for an instrument channel """ + pass + + def close(self): + """ Doesn't make sense to just close a channel by default, raise NotImplemented """ + raise NotImplementedError("Can't close a channel. Close my parent instead.") + + @classmethod + def record_instance(cls, instance): + """ Instances should not be recorded for channels. This should happen for the parent instrument. """ + pass + + @classmethod + def instances(cls): + """ Instances should not be recorded for channels. This should happen for the parent instrument. """ + pass + + @classmethod + def remove_instances(cls, instance): + """ It doesn't make sense to remove a channel from an instrument, raise NotImplemented""" + raise NotImplementedError("Can't remove a channel.") + + # This method doesn't make sense for a channel, raise NotImplemented + @classmethod + def find_instruments(cls, name, instrument_class=None): + raise NotImplementedError("Can't find instruments in a channel") + + # Pass any commands to read or write from the instrument up to the parent + def write(self, cmd): + return self._parent.write(cmd) + + def write_raw(self, cmd): + return self._parent.write_raw(cmd) + + def ask(self, cmd): + return self._parent.ask(cmd) + + def ask_raw(self, cmd): + return self._parent.ask_raw(cmd) + + +class MultiChannelInstrumentParameter(MultiParameter): + """ + Parameter to get or set multiple channels simultaneously. + + Will normally be created by a ChannelList and not directly by anything else. + + Args: + channels(list[chan_type]): A list of channels which we can operate on simultaneously. + + param_name(str): Name of the multichannel parameter + """ + def __init__(self, channels: Union[List, Tuple], param_name, *args, **kwargs): + super().__init__(*args, **kwargs) + self._channels = channels + self._param_name = param_name + + def get(self): + """ + Return a tuple containing the data from each of the channels in the list + """ + return tuple(chan.parameters[self._param_name].get() for chan in self._channels) + + def set(self, value): + """ + Set all parameters to this value + + Args: + value (unknown): The value to set to. The type is given by the + underlying parameter. + """ + for chan in self._channels: + getattr(chan, self._param_name).set(value) + + @property + def full_names(self): + """Overwrite full_names because the instument name is already included in the name. + This happens because the instument name is included in the channel name merged into the + parameter name above. + """ + + return self.names + + +class ChannelList(Metadatable): + """ + Container for channelized parameters that allows for sweeps over + all channels, as well as addressing of individual channels. + + Args: + parent (Instrument): the instrument to which this channel + should be attached + + name (string): the name of the channel list + + chan_type (InstrumentChannel): the type of channel contained + within this list + + chan_list (Iterable[chan_type]): An optional iterable of + channels of type chan_type. This will create a list and + immediately lock the ChannelList. + + snapshotable (bool): Optionally disables taking of snapshots + for a given channel list. This is used when objects + stored inside a channel list are accessible in multiple + ways and should not be repeated in an instrument snapshot. + + multichan_paramclass (MultiChannelInstrumentParameter): The class of the object + to be returned by the ChanneList's __getattr__ method. Should be a + subclass of MultiChannelInstrumentParameter. + + Raises: + ValueError: If chan_type is not a subclass of InstrumentChannel + ValueError: If multichan_paramclass if not a subclass of + MultiChannelInstrumentParameter (note that a class is a subclass + of itself). + + """ + + def __init__(self, parent, name, chan_type: type, chan_list=None, + snapshotable=True, + multichan_paramclass: type=MultiChannelInstrumentParameter): + super().__init__() + + self._parent = parent + self._name = name + if (not isinstance(chan_type, type) or + not issubclass(chan_type, InstrumentChannel)): + raise ValueError("Channel Lists can only hold instances of type" + " InstrumentChannel") + if (not isinstance(multichan_paramclass, type) or + not issubclass(multichan_paramclass, MultiChannelInstrumentParameter)): + raise ValueError("multichan_paramclass must be a (subclass of) " + "MultiChannelInstrumentParameter") + + self._chan_type = chan_type + self._snapshotable = snapshotable + self._paramclass = multichan_paramclass + + # If a list of channels is not provided, define a list to store channels. + # This will eventually become a locked tuple. + if chan_list is None: + self._locked = False + self._channels = [] + else: + self._locked = True + self._channels = tuple(chan_list) + if not all(isinstance(chan, chan_type) for chan in self._channels): + raise TypeError("All items in this channel list must be of type {}.".format(chan_type.__name__)) + + def __getitem__(self, i): + """ + Return either a single channel, or a new ChannelList containing only the specified channels + + Args: + i (int/slice): Either a single channel index or a slice of channels to get + """ + if isinstance(i, slice): + return ChannelList(self._parent, self._name, self._chan_type, + self._channels[i], + multichan_paramclass=self._paramclass) + return self._channels[i] + + def __iter__(self): + return iter(self._channels) + + def __len__(self): + return len(self._channels) + + def __repr__(self): + return "ChannelList({!r}, {}, {!r})".format(self._parent, self._chan_type.__name__, self._channels) + + def __add__(self, other): + """ + Return a new channel list containing the channels from both ChannelList self and r. + + Both channel lists must hold the same type and have the same parent. + + Args: + other(ChannelList): Right argument to add. + """ + if not isinstance(self, ChannelList) or not isinstance(other, ChannelList): + raise TypeError("Can't add objects of type {} and {} together".format( + type(self).__name__, type(other).__name__)) + if self._chan_type != other._chan_type: + raise TypeError("Both l and r arguments to add must contain channels of the same type." + " Adding channels of type {} and {}.".format(self._chan_type.__name__, + other._chan_type.__name__)) + if self._parent != other._parent: + raise ValueError("Can only add channels from the same parent together.") + + return ChannelList(self._parent, self._name, self._chan_type, self._channels + other._channels) + + def append(self, obj): + """ + When initially constructing the channel list, a new channel to add to the end of the list + + Args: + obj(chan_type): New channel to add to the list. + """ + if self._locked: + raise AttributeError("Cannot append to a locked channel list") + if not isinstance(obj, self._chan_type): + raise TypeError("All items in a channel list must be of the same type." + " Adding {} to a list of {}.".format(type(obj).__name__, + self._chan_type.__name__)) + return self._channels.append(obj) + + def extend(self, objects): + """ + Insert an iterable of objects into the list of channels. + + Args: + objects(Iterable[chan_type]): A list of objects to add into the ChannelList. + """ + # objects may be a generator but we need to iterate over it twice below so + # copy it into a tuple just in case. + objects = tuple(objects) + if self._locked: + raise AttributeError("Cannot extend a locked channel list") + if not all(isinstance(obj, self._chan_type) for obj in objects): + raise TypeError("All items in a channel list must be of the same type.") + return self._channels.extend(objects) + + def index(self, obj): + """ + Return the index of the given object + + Args: + obj(chan_type): The object to find in the channel list. + """ + return self._channels.index(obj) + + def insert(self, index, obj): + """ + Insert an object into the channel list at a specific index. + + Args: + index(int): Index to insert object. + + obj(chan_type): Object of type chan_type to insert. + """ + if self._locked: + raise AttributeError("Cannot insert into a locked channel list") + if not isinstance(obj, self._chan_type): + raise TypeError("All items in a channel list must be of the same type." + " Adding {} to a list of {}.".format(type(obj).__name__, + self._chan_type.__name__)) + return self._channels.insert(index, obj) + + def lock(self): + """ + Lock the channel list. Once this is done, the channel list is converted to a tuple + and any future changes to the list are prevented. + """ + if self._locked: + return + self._channels = tuple(self._channels) + self._locked = True + + def snapshot_base(self, update=False): + """ + State of the instrument as a JSON-compatible dict. + + Args: + update (bool): If True, update the state by querying the + instrument. If False, just use the latest values in memory.. + + Returns: + dict: base snapshot + """ + if self._snapshotable: + snap = {'channels': dict((chan.name, chan.snapshot(update=update)) + for chan in self._channels), + 'snapshotable': self._snapshotable, + '__class__': full_class(self), + } + else: + snap = {'snapshotable': self._snapshotable, + '__class__': full_class(self), + } + return snap + + def __getattr__(self, name): + """ + Return a multi-channel function or parameter that we can use to get or set all items + in a channel list simultaneously. + + Params: + name(str): The name of the parameter or function that we want to operate on. + """ + # Check if this is a valid parameter + if name in self._channels[0].parameters: + setpoints = None + setpoint_names = None + setpoint_labels = None + setpoint_units = None + # We need to construct a MultiParameter object to get each of the + # values our of each parameter in our list, we don't currently try to + # construct a multiparameter from a list of multi parameters + if isinstance(self._channels[0].parameters[name], MultiParameter): + raise NotImplementedError("Slicing is currently not supported for MultiParameters") + names = tuple("{}_{}".format(chan.name, name) for chan in self._channels) + labels = tuple(chan.parameters[name].label for chan in self._channels) + units = tuple(chan.parameters[name].unit for chan in self._channels) + + if isinstance(self._channels[0].parameters[name], ArrayParameter): + shapes = tuple(chan.parameters[name].shape for chan in self._channels) + + if self._channels[0].parameters[name].setpoints: + setpoints = tuple(chan.parameters[name].setpoints for chan in self._channels) + if self._channels[0].parameters[name].setpoint_names: + setpoint_names = tuple(chan.parameters[name].setpoint_names for chan in self._channels) + if self._channels[0].parameters[name].setpoint_labels: + setpoint_labels = tuple(chan.parameters[name].setpoint_labels for chan in self._channels) + if self._channels[0].parameters[name].setpoint_units: + setpoint_units = tuple(chan.parameters[name].setpoint_units for chan in self._channels) + else: + shapes = tuple(() for chan in self._channels) + + param = self._paramclass(self._channels, + param_name=name, + name="Multi_{}".format(name), + names=names, + shapes=shapes, + instrument=self._parent, + labels=labels, + units=units, + setpoints=setpoints, + setpoint_names=setpoint_names, + setpoint_units=setpoint_units, + setpoint_labels=setpoint_labels) + return param + + # Check if this is a valid function + if name in self._channels[0].functions: + # We want to return a reference to a function that would call the function + # for each of the channels in turn. + def multi_func(*args, **kwargs): + for chan in self._channels: + chan.functions[name](*args, **kwargs) + return multi_func + + raise AttributeError('\'{}\' object has no attribute \'{}\''.format(self.__class__.__name__, name)) diff --git a/qcodes/instrument_drivers/Harvard/Decadac.py b/qcodes/instrument_drivers/Harvard/Decadac.py index 3e7a798b5ee5..bdc3c32e94ad 100644 --- a/qcodes/instrument_drivers/Harvard/Decadac.py +++ b/qcodes/instrument_drivers/Harvard/Decadac.py @@ -1,266 +1,520 @@ -import logging -from time import sleep +from time import time from functools import partial -from qcodes.instrument.visa import VisaInstrument +from qcodes import VisaInstrument, InstrumentChannel, ChannelList, ManualParameter from qcodes.utils import validators as vals -log = logging.getLogger(__name__) - -class Decadac(VisaInstrument): - """ - The qcodes driver for the Decadac. - Each slot on the Deacadac is to be treated as a seperate - four-channel instrument. - - Tested with a Decadec firmware revion number 14081 (Decadac 139). - - The message strategy is the following: always keep the queue empty, so - that self.visa_handle.ask(XXX) will return the answer to XXX and not - some previous event. +class DACException(Exception): + pass - Attributes: - - _ramp_state (bool): If True, ramp state is ON. Default False. +class DacReader(object): + @staticmethod + def _dac_parse(resp): + """ + Parses responses from the DAC. They should take the form of "!" + This command returns the value of resp. + """ + resp = resp.strip() + if resp[-1] != "!": + raise DACException("Unexpected terminator on response: {}. Should end with \"!\"".format(resp)) + return resp.strip()[1:-1] - _ramp_time (int): The ramp time in ms. Default 100 ms. - """ + def _dac_v_to_code(self, volt): + """ + Convert a voltage to the internal dac code (number between 0-65536) + based on the minimum/maximum values of a given channel. + Midrange is 32768. + """ + frac = (volt - self.min_val) / (self.max_val - self.min_val) + val = int(round(frac * 65536)) + if val >= 65536: # Check limits. For example setting max_val will cause an overflow + return 65535 + if val < 0: # Ensure no negative values + return 0 + return val + + def _dac_code_to_v(self, code): + """ + Convert a voltage to the internal dac code (number between 0-65536) + based on the minimum/maximum values of a given channel. + Midrange is 32768. + """ + frac = code/65536.0 + return (frac * (self.max_val - self.min_val)) + self.min_val - def __init__(self, name, port, slot, timeout=2, baudrate=9600, - bytesize=8, **kwargs): + def _set_slot(self): + """ + Set the active DAC slot + """ + resp = self.ask_raw("B{};".format(self._slot)) + if int(self._dac_parse(resp)) != self._slot: + raise DACException("Unexpected return from DAC when setting slot: " + "{}. DAC slot may not have been set.".format(resp)) + def _set_channel(self): + """ + Set the active DAC channel """ + resp = self.ask_raw("B{};C{};".format(self._slot, self._channel)) + if resp.strip() != "B{}!C{}!".format(self._slot, self._channel): + raise DACException("Unexpected return from DAC when setting channel: " + "{}. DAC channel may not have been set.".format(resp)) - Creates an instance of the Decadac instrument corresponding to one slot - on the physical instrument. + def _query_address(self, addr, count=1, versa_eeprom=False): + """ + Query the value at the dac address given. Args: - name (str): What this instrument is called locally. + addr (int): The address to query. - port (number): The number (only) of the COM port to connect to. + count (int): The number of bytes to query. - slot (int): The slot to use. + versa_eeprom(bool): do we want to read from the versadac (slot) EEPROM + """ + # Check if we actually have anything to query + if count == 0: + return 0 + + # Validate address + addr = int(addr) + if addr < 0 or addr > 1107296266: + raise DACException("Invalid address {}.".format(addr)) + + # Choose a poke command depending on whether we are querying a + # VERSADAC eeprom or main memory + # If we are writing to a VERSADAC, we must also set the slot. + if versa_eeprom: + self._set_slot() + query_command = "e;" + else: + query_command = "p;" + + # Read a number of bytes from the device and convert to an int + val = 0 + for i in range(count): + # Set DAC to point to address + ret = int(self._dac_parse(self.ask_raw("A{};".format(addr)))) + if ret != addr: + raise DACException("Failed to set EEPROM address {}.".format(addr)) + val += int(self._dac_parse(self.ask_raw(query_command))) << (32*(count-i-1)) + addr += 1 + + return val + + def _write_address(self, addr, val, versa_eeprom=False): + """ + Write a value to a given DAC address - timeout (number): Seconds to wait for message response. - Default 0.3. + Args: + addr (int): The address to query. - baudrate (number): The connection baudrate. Default 9600. + val (int): The value to write. - bytesize (number): The connection bytesize. Default 8. + versa_eeprom(bool): do we want to read from the versadac (slot) EEPROM """ + # Validate address + addr = int(addr) + if addr < 0 or addr > 1107296266: + raise DACException("Invalid address {}.".format(addr)) + + # Validate value + val = int(val) + if val < 0 or val >= 2**32: + raise DACException("Writing invalid value ({}) to address {}.".format(val, addr)) + + # Choose a poke command depending on whether we are querying a + # VERSADAC eeprom or main memory. If we are writing to a versadac channel + # we must also set the slot + if versa_eeprom: + query_command = "e;" + write_command = "E" + self._set_slot() + else: + query_command = "p;" + write_command = "P" + + # Write the value to the DAC + # Set DAC to point to address + ret = int(self._dac_parse(self.ask_raw("A{};".format(addr)))) + if ret != addr: + raise DACException("Failed to set EEPROM address {}.".format(addr)) + self.ask_raw("{}{};".format(write_command, val)) + # Check the write was successful + if int(self._dac_parse(self.ask_raw(query_command))) != val: + raise DACException("Failed to write value ({}) to address {}.".format(val, addr)) + + +class DacChannel(InstrumentChannel, DacReader): + """ + A single DAC channel of the DECADAC + """ + _CHANNEL_VAL = vals.Ints(0, 3) + + def __init__(self, parent, name, channel, min_val=-5, max_val=5): + super().__init__(parent, name) + + # Validate slot and channel values + self._CHANNEL_VAL.validate(channel) + self._channel = channel + self._slot = self._parent._slot + + # Calculate base address for querying channel parameters + # Note that the following values can be found using these offsets + # 0: Interrupt Period + # 4: DAC High Limit + # 5: DAC Low Limit + # 6: Slope (double) + # 8: DAC Value (double) + self._base_addr = 1536 + (16*4)*self._slot + 16*self._channel + + # Store min/max voltages + assert(min_val < max_val) + self.min_val = min_val + self.max_val = max_val + + # Add channel parameters + # Note we will use the older addresses to read the value from the dac rather than the newer + # 'd' command for backwards compatibility + self._volt_val = vals.Numbers(self.min_val, self.max_val) + self.add_parameter("volt", get_cmd=partial(self._query_address, self._base_addr+9, 1), + get_parser=self._dac_code_to_v, + set_cmd=self._set_dac, set_parser=self._dac_v_to_code, vals=self._volt_val, + label="Voltage", unit="V") + # The limit commands are used to sweep dac voltages. They are not safety features. + self.add_parameter("lower_ramp_limit", get_cmd=partial(self._query_address, self._base_addr+5), + get_parser=self._dac_code_to_v, + set_cmd="L{};", set_parser=self._dac_v_to_code, vals=self._volt_val, + label="Lower_Ramp_Limit", unit="V") + self.add_parameter("upper_ramp_limit", get_cmd=partial(self._query_address, self._base_addr+4), + get_parser=self._dac_code_to_v, + set_cmd="U{};", set_parser=self._dac_v_to_code, vals=self._volt_val, + label="Upper_Ramp_Limit", unit="V") + self.add_parameter("update_period", get_cmd=partial(self._query_address, self._base_addr), + get_parser=int, set_cmd="T{};", set_parser=int, vals=vals.Ints(50, 65535), + label="Update_Period", unit="us") + self.add_parameter("slope", get_cmd=partial(self._query_address, self._base_addr+6, 2), + get_parser=int, set_cmd="S{};", set_parser=int, vals=vals.Ints(-(2**32), 2**32), + label="Ramp_Slope") + + # Manual parameters to control whether DAC channels should ramp to voltages or jump + self._ramp_val = vals.Numbers(0, 10) + self.add_parameter("enable_ramp", parameter_class=ManualParameter, initial_value=False, + vals=vals.Bool()) + self.add_parameter("ramp_rate", parameter_class=ManualParameter, initial_value=0.1, + vals=self._ramp_val, unit="V/s") + + # Add ramp function to the list of functions + self.add_function("ramp", call_cmd=self._ramp, args=(self._volt_val, self._ramp_val)) + + # If we have access to the VERSADAC (slot) EEPROM, we can set the inital + # value of the channel. + # NOTE: these values will be overwritten by a K3 calibration + if self._parent._VERSA_EEPROM_available: + _INITIAL_ADDR = [6, 8, 32774, 32776] + self.add_parameter("initial_value", + get_cmd=partial(self._query_address, _INITIAL_ADDR[self._channel], versa_eeprom=True), + get_parser=self._dac_code_to_v, + set_cmd=partial(self._write_address, _INITIAL_ADDR[self._channel], versa_eeprom=True), + set_parser=self._dac_v_to_code, vals=vals.Numbers(self.min_val, self.max_val)) + + def _ramp(self, val, rate, block=True): + """ + Ramp the DAC to a given voltage. - address = 'ASRL{:d}::INSTR'.format(port) - self.slot = slot + Params: + val (float): The voltage to ramp to in volts - super().__init__(name, address, timeout=timeout, **kwargs) + rate (float): The ramp rate in units of volts/s - # set instrument operation state variables - self._ramp_state = False - self._ramp_time = 100 - self._voltranges = [1, 1, 1, 1] - self._offsets = [0, 0, 0, 0] + block (bool): Should the call block until the ramp is complete? + """ - # channels - for channelno in range(4): - self.add_parameter('ch{}_voltage'.format(channelno), - get_cmd=partial(self._getvoltage, - channel=channelno), - set_cmd=partial(self._setvoltage, - channel=channelno), - label='Voltage', - unit='V') + # We need to know the current dac value (in raw units), as well as the update rate + c_volt = self.volt.get() # Current Voltage + if c_volt == val: # If we are already at the right voltage, we don't need to ramp + return + c_val = self._dac_v_to_code(c_volt) # Current voltage in DAC units + e_val = self._dac_v_to_code(val) # Endpoint in DAC units + t_rate = 1/(self.update_period.get() * 1e-6) # Number of refreshes per second + secs = abs((c_volt - val)/rate) # Number of seconds to ramp + + # The formula to calculate the slope is: Number of DAC steps divided by the number of time + # steps in the ramp multiplied by 65536 + slope = int(((e_val - c_val)/(t_rate*secs))*65536) + + # Now let's set up our limits and ramo slope + if slope > 0: + self.upper_ramp_limit.set(val) + else: + self.lower_ramp_limit.set(val) + self.slope.set(slope) + + # Block until the ramp is complete is block is True + if block: + while self.slope.get() != 0: + pass - self.add_parameter('ch{}_voltrange'.format(channelno), - get_cmd=partial(self._getvoltrange, channelno), - set_cmd=partial(self._setvoltrange, channelno), - vals=vals.Enum(1, 2, 3)) + def _set_dac(self, code): + """ + Set the voltage on the dac channel, ramping if the enable_rate parameter is set for this + channel. - self.add_parameter('ch{}_offset'.format(channelno), - get_cmd=partial(self._getoffset, channelno), - set_cmd=partial(self._setoffset, channelno), - label='Channel {} offset'.format(channelno), - unit='V', - docstring=""" - The offset is applied to the channel. - E.g. if ch1_offset = 1 and ch_voltage - is set to 1, the instrument is told to - output 2 volts. - """) + Params: + code (int): the DAC code to set the voltage to + """ + if self.enable_ramp.get(): + self._ramp(self._dac_code_to_v(code), rate=self.ramp_rate.get()) + else: + code = int(code) + self._set_channel() + self.ask_raw("U65535;L0;D{};".format(code)) + + def write(self, cmd): + """ + Overload write to set channel prior to any channel operations. + Since all commands are echoed back, we must keep track of responses + as well, otherwise commands receive the wrong response. + """ + self._set_channel() + return self.ask_raw(cmd) - self.add_parameter('mode', - label='Output mode', - set_cmd=partial(self._setmode, self.slot), - vals=vals.Enum(0, 2), - docstring=""" - The operational mode of the slot. - 0: output off, - 2: 4-channel (low res) mode. - """) + def ask(self, cmd): + """ + Overload ask to set channel prior to operations + """ + self._set_channel() + return self.ask_raw(cmd) - # initialise hardware settings - self.mode.set(2) - def _setmode(self, slot, mode): +class DacSlot(InstrumentChannel, DacReader): + """ + A single DAC Slot of the DECADAC + """ + _SLOT_VAL = vals.Ints(0, 5) + + def __init__(self, parent, name, slot, min_val=-5, max_val=5): + super().__init__(parent, name) + + # Validate slot and channel values + self._SLOT_VAL.validate(slot) + self._slot = slot + + # Store whether we have access to the VERSADAC EEPROM + self._VERSA_EEPROM_available = self._parent._VERSA_EEPROM_available + + # Create a list of channels in the slot + self.channels = ChannelList(self, "Slot_Channels", DacChannel) + for i in range(4): + self.channels.append(DacChannel(self, "Chan{}".format(i), i)) + + # Set the slot mode. Valid modes are: + # Off: Channel outputs are disconnected from the input, grounded with 10MOhm. + # Fine: 2-channel mode. Channels 0 and 1 are output, use 2 and 3 for fine + # adjustment of Channels 0 and 1 respectively + # Coarse: All 4 channels are used as output + # FineCald: Calibrated 2-channel mode, with 0 and 1 output, 2 and 3 used + # automatically for fine adjustment. This mode only works for calibrated + # DecaDAC's + # Unfortunately there is no known way of reading the slot mode hence this will be + # set in initialization + if self._parent._cal_supported: + slot_modes = {"Off": 0, "Fine": 1, "Coarse": 2, "FineCald": 3} + else: + slot_modes = {"Off": 0, "Fine": 1, "Coarse": 2} + self.add_parameter('slot_mode', get_cmd="m;", get_parser=self._dac_parse, set_cmd="M{};", + val_mapping=slot_modes) + + # Enable all slots in coarse mode. + self.slot_mode.set("Coarse") + + def write(self, cmd): """ - set_cmd for the mode parameter + Overload write to set channel prior to any channel operations. + Since all commands are echoed back, we must keep track of responses + as well, otherwise commands receive the wrong response. """ - self.visa_handle.write('B {}; M {};'.format(slot, mode)) - self.visa_handle.read() + self._set_slot() + return self.ask_raw(cmd) - def _getoffset(self, n): - return self._offsets[n] + def ask(self, cmd): + """ + Overload ask to set channel prior to operations + """ + self._set_slot() + return self.ask_raw(cmd) - def _setoffset(self, n, val): - self._offsets[n] = val - def _getvoltrange(self, n): - return self._voltranges[n] +class Decadac(VisaInstrument, DacReader): + """ + The qcodes driver for the Decadac. + Each slot on the Deacadac is to be treated as a seperate + four-channel instrument. - def _setvoltrange(self, n, val): - self._voltranges[n] = val + Tested with a Decadec firmware revion number 14081 (Decadac 139). - def _getvoltage(self, channel): - """ - Function to query the voltage. Flushes the message queue in that - process. - """ + The message strategy is the following: always keep the queue empty, so + that self.visa_handle.ask(XXX) will return the answer to XXX and not + some previous event. - # set the relevant channel and slot to query - mssg = 'B {:d}; C {:d};'.format(self.slot, channel) - mssg += 'd;' - # a bit of string juggling to extract the voltage - rawresponse = self.visa_handle.ask(mssg) - temp = rawresponse[::-1] - temp = temp[3:temp.upper().find('D')-1] - response = temp[::-1] + Attributes: - rawvoltage = self._code2voltage(response, channel) - actualvoltage = rawvoltage - self._offsets[channel] + _ramp_state (bool): If True, ramp state is ON. Default False. - return actualvoltage + _ramp_time (int): The ramp time in ms. Default 100 ms. + """ - def _setvoltage(self, voltage, channel): + def __init__(self, name, address, min_val=-5, max_val=5, **kwargs): """ - Function to set the voltage. Depending on whether self._ramp_state is - True or False, this function either ramps from the current voltage to - the specified voltage or directly makes the voltage jump there. + + Creates an instance of the Decadac instrument corresponding to one slot + on the physical instrument. Args: - voltage (number): the set voltage. + name (str): What this instrument is called locally. + + port (str): The address of the DAC. For a serial port this is ASRLn::INSTR + where n is replaced with the address set in the VISA control panel. + Baud rate and other serial parameters must also be set in the VISA control + panel. + + min_val (number): The minimum value in volts that can be output by the DAC. + This value should correspond to the DAC code 0. + + max_val (number): The maximum value in volts that can be output by the DAC. + This value should correspond to the DAC code 65536. + """ - actualvoltage = voltage + self._offsets[channel] - code = self._voltage2code(actualvoltage, channel) + super().__init__(name, address, **kwargs) - mssg = 'B {:d}; C {:d};'.format(self.slot, channel) + # Do feature detection + self._feature_detect() - if not self._ramp_state: - mssg += 'D ' + code + ';' + # Create channels + channels = ChannelList(self, "Channels", DacChannel, snapshotable=False) + slots = ChannelList(self, "Slots", DacSlot) + for i in range(6): # Create the 6 DAC slots + slots.append(DacSlot(self, "Slot{}".format(i), i, min_val, max_val)) + channels.extend(self.slots[i].channels) + slots.lock() + channels.lock() + self.add_submodule("slots", slots) + self.add_submodule("channels", channels) - self.visa_handle.write(mssg) + self.connect_message() - # due to a quirk of the Decadac, we spare the user of an error - # sometimes encountered on first run - try: - self.visa_handle.read() - except UnicodeDecodeError: - log.warning(" Decadac returned nothing and possibly did nothing. " + - "Please re-run the command") - pass + def set_all(self, volt): + """ + Set all dac channels to a specific voltage. If channels are set to ramp then the ramps + will occur in sequence, not simultaneously. - if self._ramp_state: - currentcode = self._voltage2code(self._getvoltage(channel), - channel) - slope = int((float(code)-float(currentcode)) / - (10*self._ramp_time)*2**16) - if slope < 0: - limit = 'L' - else: - limit = 'U' - - script = ['{', - '*1:', - 'M2;', - 'T 100;', # 1 timestep: 100 micro s - limit + code + ';', - 'S' + str(slope) + ';', - 'X0;', - '}'] - runcmd = 'X 1;' - mssg += ''.join(script) + runcmd - self.visa_handle.write(mssg) - sleep(0.0015*self._ramp_time) # Required sleep. - self.visa_handle.read() - - # reset channel voltage ranges - if slope < 0: - self.visa_handle.write('L 0;') - self.visa_handle.read() - else: - self.visa_handle.write('U 65535;') - self.visa_handle.read() + Args: + volt(float): The voltage to set all gates to. + """ + for chan in self.channels: + chan.volt.set(volt) - def set_ramping(self, state, time=None): + def ramp_all(self, volt, ramp_rate): """ - Function to set _ramp_state and _ramp_time. + Ramp all dac channels to a specific voltage at the given rate simultaneously. Note + that the ramps are not synchronized due to communications time and DAC ramps starting + as soon as the commands are in. Args: - state (bool): True sets ramping ON. + volt(float): The voltage to ramp all channels to. - time (Optiona[int]): the ramp time in ms + ramp_rate(float): The rate in volts per second to ramp """ - self._ramp_state = state - if time is not None: - self._ramp_time = time + # Start all channels ramping + for chan in self.channels: + chan._ramp(volt, ramp_rate, block=False) + + # Wait for all channels to complete ramping. + # The slope is reset to 0 once ramping is complete. + for chan in self.channels: + while chan.slope.get(): + pass - def get_ramping(self): + def get_idn(self): """ - Queries the value of self._ramp_state and self._ramp_time. + Attempt to identify the dac. Since we don't have standard SCPI commands, ``*IDN`` will + do nothing on this DAC. Returns: - str: ramp state information + A dict containing a serial and hardware version """ - switch = {True: 'ON', - False: 'OFF'} - mssg = 'Ramp state: ' + switch[self._ramp_state] - mssg += '. Ramp time: {:d} ms.'.format(int(self._ramp_time)) - return mssg + self._feature_detect() + + return {"serial": self.serial_no, "hardware_version": self.version} - def _code2voltage(self, code, channel): + def connect_message(self, idn_param='IDN', begin_time=None): """ - Helper function translating a 32 bit code used internally by - the Decadac into a voltage. + Print a connect message, taking into account the lack of a standard ``*IDN`` on + the Harvard DAC Args: - code (str): The code string from the instrument. - - channel (int): The relevant channel. + begin_time (number): time.time() when init started. + Default is self._t0, set at start of Instrument.__init__. """ + # start with an empty dict, just in case an instrument doesn't + # heed our request to return all 4 fields. + t = time() - (begin_time or self._t0) - code = float(code) - translationdict = {1: lambda x: (x+1)*20/2**16-10, - 2: lambda x: (x+1)*10/2**16, - 3: lambda x: (x+1)*10/2**16-10} + con_msg = ("Connected to Harvard DecaDAC " + "(hw ver: {}, serial: {}) in {:.2f}s".format( + self.version, self.serial_no, t)) + print(con_msg) - return translationdict[self._voltranges[channel]](code) + def __repr__(self): + """Simplified repr giving just the class and name.""" + return '<{}: {}>'.format(type(self).__name__, self.name) - def _voltage2code(self, voltage, channel): + def _feature_detect(self): + """ + Detect which features are available on the DAC by querying various parameters. """ - Helper function translating a voltage in V into a 32 bit code used - internally by the Decadac. - - Args: - voltage (float): The physical voltage. - - channel (int): The relevant channel. - Returns: - code (str): The corresponding voltage code. + # Check whether EEPROM is installed + try: + if self._query_address(1107296256) == 21930: + self._EEPROM_available = True + else: + self._EEPROM_available = False + except DACException: + self._EEPROM_available = False + + # Check whether we can set startup values for the DAC. + # This requires access to the EEPROM on each slot + try: + # Let's temporarily pretend to be slot 0 + self._slot = 0 + self._query_address(6, versa_eeprom=True) + del self._slot + except DACException: + self._VERSA_EEPROM_available = False + + # Check whether calibration is supported + try: + if self._dac_parse(self.ask("k;")): + self._cal_supported = True + except DACException: + self._cal_supported = False + + # Finally try and read the DAC version and S/N. This is only possible if the EEPROM + # is queryable. + if self._EEPROM_available: + self.version = self._query_address(1107296266) + self.serial_no = self._query_address(1107296264) + else: + self.version = 0 + self.serial_no = 0 + + def write(self, cmd): + """ + Since all commands are echoed back, we must keep track of responses + as well, otherwise commands receive the wrong response. Hence + all writes must also read a response. """ - translationdict = {1: lambda x: 2**16/20*(x-2**-16+10), - 2: lambda x: 2**16/10*(x-2**-16), - 3: lambda x: 2**16/10*(x-2**-16+10)} - voltage_float = translationdict[self._voltranges[channel]](voltage) - return str(int(voltage_float)) + return self.ask(cmd) diff --git a/qcodes/instrument_drivers/Lakeshore/Model_336.py b/qcodes/instrument_drivers/Lakeshore/Model_336.py new file mode 100644 index 000000000000..73e33ea452cb --- /dev/null +++ b/qcodes/instrument_drivers/Lakeshore/Model_336.py @@ -0,0 +1,57 @@ +from qcodes import VisaInstrument, InstrumentChannel, ChannelList +from qcodes.utils.validators import Enum, Strings + +class SensorChannel(InstrumentChannel): + """ + A single sensor channel of a temperature controller + """ + + _CHANNEL_VAL = Enum("A", "B", "C", "D") + + def __init__(self, parent, name, channel): + super().__init__(parent, name) + + # Validate the channel value + self._CHANNEL_VAL.validate(channel) + self._channel = channel # Channel on the temperature controller. Can be A-D + + # Add the various channel parameters + self.add_parameter('temperature', get_cmd='KRDG? {}'.format(self._channel), + get_parser=float, + label='Temerature', + unit='K') + self.add_parameter('sensor_raw', get_cmd='SRDG? {}'.format(self._channel), + get_parser=float, + label='Raw_Reading', + unit='Ohms') # TODO: This will vary based on sensor type + self.add_parameter('sensor_status', get_cmd='RDGST? {}'.format(self._channel), + val_mapping={'OK': 0, 'Invalid Reading': 1, 'Temp Underrange': 16, 'Temp Overrange': 32, + 'Sensor Units Zero': 64, 'Sensor Units Overrange': 128}, label='Sensor_Status') + + self.add_parameter('sensor_name', get_cmd='INNAME? {}'.format(self._channel), + get_parser=str, set_cmd='INNAME {},\"{{}}\"'.format(self._channel), vals=Strings(15), + label='Sensor_Name') + + +class Model_336(VisaInstrument): + """ + Lakeshore Model 336 Temperature Controller Driver + Controlled via sockets + """ + + def __init__(self, name, address, **kwargs): + super().__init__(name, address, terminator="\r\n", **kwargs) + + # Allow access to channels either by referring to the channel name + # or through a channel list. + # i.e. Model_336.A.temperature() and Model_336.channels[0].temperature() + # refer to the same parameter. + channels = ChannelList(self, "TempSensors", SensorChannel, snapshotable=False) + for chan_name in ('A', 'B', 'C', 'D'): + channel = SensorChannel(self, 'Chan{}'.format(chan_name), chan_name) + channels.append(channel) + self.add_submodule(chan_name, channel) + channels.lock() + self.add_submodule("channels", channels) + + self.connect_message() diff --git a/qcodes/instrument_drivers/Lakeshore/__init__.py b/qcodes/instrument_drivers/Lakeshore/__init__.py new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/qcodes/instrument_drivers/QDev/QDac_channels.py b/qcodes/instrument_drivers/QDev/QDac_channels.py new file mode 100644 index 000000000000..22858d6762d9 --- /dev/null +++ b/qcodes/instrument_drivers/QDev/QDac_channels.py @@ -0,0 +1,680 @@ +# QCoDeS driver for QDac using channels + +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.parameter import ManualParameter +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.slope, chan.i, chan.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=partial(self._parent.read_state, channum, 'v'), + get_parser=float, + vals=vals.Numbers(-10, 10) # TODO: update onthefly + ) + + self.add_parameter('vrange', + label='Channel {} atten.'.format(channum), + set_cmd=partial(self._parent._set_vrange, channum), + get_cmd=partial(self._parent.read_state, channum, + 'vrange'), + vals=vals.Enum(0, 1) + ) + + 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='cur {} {{}}'.format(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', + parameter_class=ManualParameter, + initial_value=0 + ) + + self.add_parameter(name='sync_duration', + label='Channel {} sync pulse duration'.format(channum), + unit='s', + parameter_class=ManualParameter, + initial_value=0.01 + ) + + +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(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 QDev digital-analog converter QDac + + Based on "DAC_commands_v_13.pdf" + Tested with Software Version: 0.170202 + + The driver assumes that the instrument is ALWAYS in verbose mode OFF + """ + + voltage_range_status = {'X 1': 10, '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): + """ + 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 + update_currents (bool): Whether to query all channels for their + current current value on startup. Default: True. + + Returns: + QDac object + """ + super().__init__(name, address) + handle = self.visa_handle + + # 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 = '' + + # The following bool is used in self.write + self.debugmode = False + + if self._get_firmware_version() < 0.170202: + raise RuntimeError(''' + Obsolete QDAC Software version detected. + QCoDeS only supports version 0.170202 or newer. + Contact rikke.lutge@nbi.ku.dk for an update. + ''') + + self.num_chans = num_chans + + # 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{}'.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(6): + 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)) + + self.verbose.set(False) + self.connect_message() + log.info('[*] Querying all channels for voltages and currents...') + self._get_status(readcurrents=update_currents) + log.info('[+] Done') + + self._output_n_lines = 50 + ######################### + # 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() + + attendict = {0: 10, 1: 1, 10: 10} + if abs(v_set) > attendict[atten]: + v_set = np.sign(v_set)*attendict[atten] + log.warning('Requested voltage outside reachable range.' + + ' Setting voltage on channel ' + + '{} to {} V'.format(chan, v_set)) + + 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: + # compensate for the 0.1 multiplier, if it's on + if self.channels[chan-1].vrange.get_latest() == 1: + v_set = v_set*10 + # set the mode back to DC in case it had been changed + self.write('wav {} 0 0 0'.format(chan)) + self.write('set {} {:.6f}'.format(chan, v_set)) + + def _set_vrange(self, chan, switchint): + """ + set_cmd for the chXX_vrange parameter + + The switchint is an integer. 1 means attenuation ON. + + Since the vrange is actually a 20 dB attenuator (amplitude factor 0.1) + immediately applied to the channel output, we must update the voltage + parameter accordingly + """ + + tdict = {'-10 V to 10 V': 0, + '-1 V to 1 V': 1, + 10: 0, + 0: 0, + 1: 1} + + old = tdict[self.channels[chan-1].vrange.get_latest()] + + self.write('vol {} {}'.format(chan, switchint)) + + if xor(old, switchint): + voltageparam = self.channels[chan-1].v + oldvoltage = voltageparam.get_latest() + newvoltage = {0: 10, 1: 0.1}[switchint]*oldvoltage + voltageparam._save_val(newvoltage) + + def _num_verbose(self, s): + """ + turn 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 + """ + 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): + r""" + Function to query the instrument and get the status of all channels. + Takes a while to finish. + + The `status` call generates 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: 0.160218\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 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} + + # The following dict must be ordered to ensure that vrange comes + # before v when iterating through it + 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] + if param == 'vrange': + attenuation = 0.1*value + if param == 'v': + value *= attenuation + 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) + sync (int): The associated sync output. 0 means 'unassign' + """ + + if chan not in range(1, 49): + raise ValueError('Channel number must be 1-48.') + + 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-48) + slope (Union[float, str]): The slope in V/s. Write 'Inf' to allow + arbitraryly small rise times. + """ + if chan not in range(1, 49): + raise ValueError('Channel number must be 1-48.') + + 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 + if self.channels[chan-1].vrange.get_latest() == 1: + offset *= 10 + amplitude *= 10 + + 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 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) + + Note that this procedure makes it cumbersome to handle the returned + messages from concatenated commands, e.g. 'wav 1 1 1 0;fun 2 1 100 1 1' + Please don't use concatenated commands + + TODO (WilliamHPNielsen): add automatic de-concatenation of commands. + """ + if self.debugmode: + log.info('Sending command string: {}'.format(cmd)) + + nr_bytes_written, ret_code = self.visa_handle.write(cmd) + self.check_error(ret_code) + 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): + self.write('status') + FW_str = self._write_response + FW_version = float(FW_str.replace('Software Version: ', '')) + for _ in range(self._output_n_lines): + self.read() + return FW_version + + 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', 10: '-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) diff --git a/qcodes/tests/instrument_mocks.py b/qcodes/tests/instrument_mocks.py index 4c757a830e63..c010a5b62c15 100644 --- a/qcodes/tests/instrument_mocks.py +++ b/qcodes/tests/instrument_mocks.py @@ -2,8 +2,8 @@ from qcodes.instrument.base import Instrument from qcodes.utils.validators import Numbers -from qcodes.instrument.parameter import MultiParameter, ManualParameter - +from qcodes.instrument.parameter import MultiParameter, ManualParameter, ArrayParameter +from qcodes.instrument.channel import InstrumentChannel, ChannelList class MockParabola(Instrument): ''' @@ -107,6 +107,44 @@ def __init__(self, name='dummy', gates=['dac1', 'dac2', 'dac3'], **kwargs): unit="V", vals=Numbers(-800, 400)) +class DummyChannel(InstrumentChannel): + """ + A single dummy channel implementation + """ + + def __init__(self, parent, name, channel): + super().__init__(parent, name) + + self._channel = channel + + # Add the various channel parameters + self.add_parameter('temperature', + parameter_class=ManualParameter, + initial_value=0, + label="Temperature_{}".format(channel), + unit='K', + vals=Numbers(0, 300)) + + self.add_parameter(name='dummy_multi_parameter', + parameter_class=MultiSetPointParam) + + self.add_parameter(name='dummy_array_parameter', + parameter_class=ArraySetPointParam) + +class DummyChannelInstrument(Instrument): + """ + Dummy instrument with channels + """ + + def __init__(self, name, **kwargs): + super().__init__(name, **kwargs) + + channels = ChannelList(self, "TempSensors", DummyChannel, snapshotable=False) + for chan_name in ('A', 'B', 'C', 'D', 'E', 'F'): + channel = DummyChannel(self, 'Chan{}'.format(chan_name), chan_name) + channels.append(channel) + self.add_submodule(chan_name, channel) + self.add_submodule("channels", channels) class MultiGetter(MultiParameter): """ @@ -138,8 +176,7 @@ class MultiSetPointParam(MultiParameter): Multiparameter which only purpose it to test that units, setpoints and so on are copied correctly to the individual arrays in the datarray. """ - def __init__(self): - name = 'testparameter' + def __init__(self, instrument=None, name='testparameter'): shapes = ((5,), (5,)) names = ('this', 'that') labels = ('this label', 'that label') @@ -150,6 +187,7 @@ def __init__(self): setpoint_labels = (('this setpoint',), ('this setpoint',)) setpoint_units = (('this setpointunit',), ('this setpointunit',)) super().__init__(name, names, shapes, + instrument=instrument, labels=labels, units=units, setpoints=setpoints, @@ -159,3 +197,32 @@ def __init__(self): def get(self): return np.zeros(5), np.ones(5) + +class ArraySetPointParam(ArrayParameter): + """ + Arrayparameter which only purpose it to test that units, setpoints + and so on are copied correctly to the individual arrays in the datarray. + """ + + def __init__(self, instrument=None, name='testparameter'): + shape = (5,) + label = 'this label' + unit = 'this unit' + sp_base = tuple(np.linspace(5, 9, 5)) + setpoints = (sp_base,) + setpoint_names = ('this_setpoint',) + setpoint_labels = ('this setpoint',) + setpoint_units = ('this setpointunit',) + super().__init__(name, + shape, + instrument, + label=label, + unit=unit, + setpoints=setpoints, + setpoint_labels=setpoint_labels, + setpoint_names=setpoint_names, + setpoint_units=setpoint_units) + + def get(self): + return np.ones(5) + 1 + diff --git a/qcodes/tests/test_channels.py b/qcodes/tests/test_channels.py new file mode 100644 index 000000000000..849d9d4f0925 --- /dev/null +++ b/qcodes/tests/test_channels.py @@ -0,0 +1,261 @@ +from unittest import TestCase +import unittest + +from qcodes.tests.instrument_mocks import DummyChannelInstrument, DummyChannel +from qcodes.utils.validators import Numbers +from qcodes.instrument.parameter import ManualParameter + +from hypothesis import given, settings +import hypothesis.strategies as hst +from qcodes.loops import Loop + +import numpy as np +from numpy.testing import assert_array_equal, assert_allclose + + +class TestChannels(TestCase): + + def setUp(self): + # print("setup") + self.instrument = DummyChannelInstrument(name='testchanneldummy') + + def tearDown(self): + + self.instrument.close() + del self.instrument + # del self.instrument is not sufficient in general because the __del__ method is + # first invoked when there are 0 (non weak) references to the instrument. If a test + # fails the unittest framework will keep a reference to the instrument is removed from + # the testcase and __del__ is not invoked until all the tests have run. + + def test_channels_get(self): + + temperatures = self.instrument.channels.temperature.get() + self.assertEqual(len(temperatures), 6) + + @given(value=hst.floats(0, 300), channel=hst.integers(0, 3)) + def test_channel_access_is_identical(self, value, channel): + channel_to_label = {0: 'A', 1: 'B', 2: 'C', 3: "D"} + label = channel_to_label[channel] + channel_via_label = getattr(self.instrument, label) + # set via labeled channel + channel_via_label.temperature(value) + self.assertEqual(channel_via_label.temperature(), value) + self.assertEqual(self.instrument.channels[channel].temperature(), value) + self.assertEqual(self.instrument.channels.temperature()[channel], value) + # reset + channel_via_label.temperature(0) + self.assertEqual(channel_via_label.temperature(), 0) + self.assertEqual(self.instrument.channels[channel].temperature(), 0) + self.assertEqual(self.instrument.channels.temperature()[channel], 0) + # set via index into list + self.instrument.channels[channel].temperature(value) + self.assertEqual(channel_via_label.temperature(), value) + self.assertEqual(self.instrument.channels[channel].temperature(), value) + self.assertEqual(self.instrument.channels.temperature()[channel], value) + # it's not possible to set via self.instrument.channels.temperature as this is a multi parameter + # that currently does not support set. + + def test_add_channel(self): + n_channels = len(self.instrument.channels) + name = 'foo' + channel = DummyChannel(self.instrument, 'Chan'+name, name) + self.instrument.channels.append(channel) + self.instrument.add_submodule(name, channel) + + self.assertEqual(len(self.instrument.channels), n_channels+1) + + self.instrument.channels.lock() + # after locking the channels it's not possible to add any more channels + with self.assertRaises(AttributeError): + name = 'bar' + channel = DummyChannel(self.instrument, 'Chan' + name, name) + self.instrument.channels.append(channel) + self.assertEqual(len(self.instrument.channels), n_channels + 1) + + def test_add_channels_from_generator(self): + n_channels = len(self.instrument.channels) + names = ('foo', 'bar', 'foobar') + channels = (DummyChannel(self.instrument, 'Chan'+name, name) for name in names) + self.instrument.channels.extend(channels) + + self.assertEqual(len(self.instrument.channels), n_channels + len(names)) + + def test_add_channels_from_tuple(self): + n_channels = len(self.instrument.channels) + names = ('foo', 'bar', 'foobar') + channels = tuple(DummyChannel(self.instrument, 'Chan'+name, name) for name in names) + self.instrument.channels.extend(channels) + + self.assertEqual(len(self.instrument.channels), n_channels + len(names)) + + def test_insert_channel(self): + n_channels = len(self.instrument.channels) + name = 'foo' + channel = DummyChannel(self.instrument, 'Chan'+name, name) + self.instrument.channels.insert(1, channel) + self.instrument.add_submodule(name, channel) + + self.assertEqual(len(self.instrument.channels), n_channels+1) + self.assertIs(self.instrument.channels[1], channel) + self.instrument.channels.lock() + # after locking the channels it's not possible to add any more channels + with self.assertRaises(AttributeError): + name = 'bar' + channel = DummyChannel(self.instrument, 'Chan' + name, name) + self.instrument.channels.insert(2, channel) + self.assertEqual(len(self.instrument.channels), n_channels + 1) + + @given(setpoints=hst.lists(hst.floats(0, 300), min_size=4, max_size=4)) + def test_combine_channels(self, setpoints): + self.assertEqual(len(self.instrument.channels), 6) + + mychannels = self.instrument.channels[0:2] + self.instrument.channels[4:] + + self.assertEqual(len(mychannels), 4) + self.assertIs(mychannels[0], self.instrument.A) + self.assertIs(mychannels[1], self.instrument.B) + self.assertIs(mychannels[2], self.instrument.E) + self.assertIs(mychannels[3], self.instrument.F) + + for i, chan in enumerate(mychannels): + chan.temperature(setpoints[i]) + + expected = tuple(setpoints[0:2] + [0, 0] + setpoints[2:]) + self.assertEquals(self.instrument.channels.temperature(), expected) + + +class TestChannelsLoop(TestCase): + + def setUp(self): + self.instrument = DummyChannelInstrument(name='testchanneldummy') + + def tearDown(self): + self.instrument.close() + del self.instrument + + def test_loop_simple(self): + loop = Loop(self.instrument.channels[0].temperature.sweep(0, 300, 10), + 0.001).each(self.instrument.A.temperature) + data = loop.run() + assert_array_equal(data.testchanneldummy_ChanA_temperature_set.ndarray, + data.testchanneldummy_ChanA_temperature.ndarray) + + def test_loop_measure_all_channels(self): + p1 = ManualParameter(name='p1', vals=Numbers(-10, 10)) + loop = Loop(p1.sweep(-10, 10, 1), 1e-6).each(self.instrument.channels.temperature) + data = loop.run() + self.assertEqual(data.p1_set.ndarray.shape, (21, )) + self.assertEqual(len(data.arrays), 7) + for chan in ['A', 'B', 'C', 'D', 'E', 'F']: + self.assertEqual(getattr(data, 'testchanneldummy_Chan{}_temperature'.format(chan)).ndarray.shape, (21,)) + + def test_loop_measure_channels_individually(self): + p1 = ManualParameter(name='p1', vals=Numbers(-10, 10)) + loop = Loop(p1.sweep(-10, 10, 1), 1e-6).each(self.instrument.channels[0].temperature, + self.instrument.channels[1].temperature, + self.instrument.channels[2].temperature, + self.instrument.channels[3].temperature) + data = loop.run() + self.assertEqual(data.p1_set.ndarray.shape, (21, )) + for chan in ['A', 'B', 'C', 'D']: + self.assertEqual(getattr(data, 'testchanneldummy_Chan{}_temperature'.format(chan)).ndarray.shape, (21,)) + + @given(values=hst.lists(hst.floats(0, 300), min_size=4, max_size=4)) + @settings(max_examples=10) + def test_loop_measure_channels_by_name(self, values): + p1 = ManualParameter(name='p1', vals=Numbers(-10, 10)) + for i in range(4): + self.instrument.channels[i].temperature(values[i]) + loop = Loop(p1.sweep(-10, 10, 1), 1e-6).each(self.instrument.A.temperature, + self.instrument.B.temperature, + self.instrument.C.temperature, + self.instrument.D.temperature) + data = loop.run() + self.assertEqual(data.p1_set.ndarray.shape, (21, )) + for i, chan in enumerate(['A', 'B', 'C', 'D']): + self.assertEqual(getattr(data, 'testchanneldummy_Chan{}_temperature'.format(chan)).ndarray.shape, (21,)) + self.assertEqual(getattr(data, 'testchanneldummy_Chan{}_temperature'.format(chan)).ndarray.max(), values[i]) + self.assertEqual(getattr(data, 'testchanneldummy_Chan{}_temperature'.format(chan)).ndarray.min(), values[i]) + + @given(loop_channels=hst.lists(hst.integers(0, 3), min_size=2, max_size=2, unique=True), + measure_channel=hst.integers(0, 3)) + @settings(max_examples=10) + def test_nested_loop_over_channels(self, loop_channels, measure_channel): + channel_to_label = {0: 'A', 1: 'B', 2: 'C', 3: "D"} + loop = Loop(self.instrument.channels[loop_channels[0]].temperature.sweep(0, 10, 0.5)) + loop = loop.loop(self.instrument.channels[loop_channels[1]].temperature.sweep(50, 51, 0.1)) + loop = loop.each(self.instrument.channels[measure_channel].temperature) + data = loop.run() + + self.assertEqual(getattr(data, 'testchanneldummy_Chan{}_temperature_set'.format( + channel_to_label[loop_channels[0]])).ndarray.shape, (21,)) + self.assertEqual(getattr(data, 'testchanneldummy_Chan{}_temperature_set'.format( + channel_to_label[loop_channels[1]])).ndarray.shape, (21, 11,)) + self.assertEqual(getattr(data, 'testchanneldummy_Chan{}_temperature'.format( + channel_to_label[measure_channel])).ndarray.shape, (21, 11)) + + assert_array_equal(getattr(data, 'testchanneldummy_Chan{}_temperature_set'.format( + channel_to_label[loop_channels[0]])).ndarray, + np.arange(0, 10.1, 0.5)) + + expected_array = np.repeat(np.arange(50, 51.01, 0.1).reshape(1, 11), 21, axis=0) + array = getattr(data, 'testchanneldummy_Chan' + '{}_temperature_set'.format(channel_to_label[loop_channels[1]])).ndarray + assert_allclose(array, expected_array) + + def test_loop_slicing_multiparameter_raises(self): + with self.assertRaises(NotImplementedError): + loop = Loop(self.instrument.A.temperature.sweep(0, 10, 1), 0.1) + loop.each(self.instrument.channels[0:2].dummy_multi_parameter).run() + + def test_loop_multiparameter_by_name(self): + loop = Loop(self.instrument.A.temperature.sweep(0, 10, 1), 0.1) + data = loop.each(self.instrument.A.dummy_multi_parameter).run() + self._verify_multiparam_data(data) + self.assertIn('this_setpoint_set', data.arrays.keys()) + + def test_loop_multiparameter_by_index(self): + loop = Loop(self.instrument.channels[0].temperature.sweep(0, 10, 1), 0.1) + data = loop.each(self.instrument.A.dummy_multi_parameter).run() + self._verify_multiparam_data(data) + + def _verify_multiparam_data(self, data): + self.assertIn('this_setpoint_set', data.arrays.keys()) + assert_array_equal(data.arrays['this_setpoint_set'].ndarray, + np.repeat(np.arange(5., 10).reshape(1, 5), 11, axis=0)) + self.assertIn('testchanneldummy_ChanA_this', data.arrays.keys()) + assert_array_equal(data.arrays['testchanneldummy_ChanA_this'].ndarray, np.zeros((11, 5))) + self.assertIn('testchanneldummy_ChanA_that', data.arrays.keys()) + assert_array_equal(data.arrays['testchanneldummy_ChanA_that'].ndarray, np.ones((11, 5))) + self.assertIn('testchanneldummy_ChanA_temperature_set', data.arrays.keys()) + assert_array_equal(data.arrays['testchanneldummy_ChanA_temperature_set'].ndarray, np.arange(0, 10.1, 1)) + + def test_loop_slicing_arrayparameter(self): + loop = Loop(self.instrument.A.temperature.sweep(0, 10, 1), 0.1) + data = loop.each(self.instrument.channels[0:2].dummy_array_parameter).run() + self._verify_array_data(data, channels=('A', 'B')) + + def test_loop_arrayparameter_by_name(self): + loop = Loop(self.instrument.A.temperature.sweep(0, 10, 1), 0.1) + data = loop.each(self.instrument.A.dummy_array_parameter).run() + self._verify_array_data(data) + + def test_loop_arrayparameter_by_index(self): + loop = Loop(self.instrument.channels[0].temperature.sweep(0, 10, 1), 0.1) + data = loop.each(self.instrument.A.dummy_array_parameter).run() + self._verify_array_data(data) + + def _verify_array_data(self, data, channels=('A',)): + self.assertIn('this_setpoint_set', data.arrays.keys()) + assert_array_equal(data.arrays['this_setpoint_set'].ndarray, + np.repeat(np.arange(5., 10).reshape(1, 5), 11, axis=0)) + for channel in channels: + aname = 'testchanneldummy_Chan{}_dummy_array_parameter'.format(channel) + self.assertIn(aname, data.arrays.keys()) + assert_array_equal(data.arrays[aname].ndarray, np.ones((11, 5))+1) + self.assertIn('testchanneldummy_ChanA_temperature_set', data.arrays.keys()) + assert_array_equal(data.arrays['testchanneldummy_ChanA_temperature_set'].ndarray, np.arange(0, 10.1, 1)) + +if __name__ == '__main__': + unittest.main() diff --git a/qcodes/tests/test_instrument.py b/qcodes/tests/test_instrument.py index 2e37b046179e..e80fe9a0d719 100644 --- a/qcodes/tests/test_instrument.py +++ b/qcodes/tests/test_instrument.py @@ -17,6 +17,8 @@ def setUp(self): def tearDown(self): # force gc run + self.instrument.close() + self.instrument2.close() del self.instrument del self.instrument2 gc.collect() @@ -37,6 +39,7 @@ def test_check_instances(self): self.assertEqual(DummyInstrument.instances(), [self.instrument]) self.assertEqual(self.instrument.instances(), [self.instrument]) + def test_attr_access(self): instrument = self.instrument diff --git a/test_requirements.txt b/test_requirements.txt index ff0ae9589438..6e769b3eccf1 100644 --- a/test_requirements.txt +++ b/test_requirements.txt @@ -1,4 +1,5 @@ coverage pytest-cov pytest -codacy-coverage \ No newline at end of file +codacy-coverage +hypothesis \ No newline at end of file