# most of the drivers only need a couple of these... moved all up here for clarity below
from time import sleep, time
import numpy as np
import ctypes # only for DLL-based instrument
import qcodes as qc
from qcodes import (Instrument, VisaInstrument,
ManualParameter, MultiParameter,
validators as vals)
from qcodes.instrument.channel import InstrumentChannel
There are 3 available:
VisaInstrument
- for most instruments that communicate over a text channel (ethernet, GPIB, serial, USB...) that do not have a custom DLL or other driver to manage low-level commands.IPInstrument
- a deprecated driver just for ethernet connections. Do not use this; use VisaInstrument
instead.Instrument
- superclass of both VisaInstrument
and IPInstrument
, use this if you do not communicate over a text channel, for example:If possible, please use a VisaInstrument
, as this allows for the creation of a simulated instrument. (See the Creating Simulated PyVISA Instruments
notebook)
Broadly speaking, a QCoDeS instrument driver is nothing but an object that holds a connection handle to the physical instrument and has some sub-objects that represent the state of the physical instrument. These sub-objects are the Parameters
. Writing a driver basically boils down to adding a ton of Parameters
.
A parameter represents a single value of a single feature of an instrument, e.g. the frequency of a function generator, the mode of a multimeter (resistance, current, or voltage), or the input impedance of an oscilloscope channel. Each Parameter
can have the following attributes:
name
, the name used internally by QCoDeS, e.g. 'input_impedance'label
, the label to use for plotting this parameterunit
, the physical unit. ALWAYS use SI units if a unit is applicableset_cmd
, the command to set the parameter. Either a SCPI string with a single '{}', or a function taking one argument (see examples below)get_cmd
, the command to get the parameter. Follows the same scheme as set_cmd
vals
, a validator (from qcodes.utils.validators
) to reject invalid values before they are sent to the instrument. Since there is no standard for how an instrument responds to an out-of-bound value (e.g. a 10 kHz function generator receiving 12e9 for its frequency), meaning that the user can expect anything from silent failure to the instrument breaking or suddenly outputting random noise, it is MUCH better to catch invalid values in software. Therefore, please provide a validator if at all possible.val_mapping
, a dictionary mapping human-readable values like 'High Impedance' to the instrument's internal representation like '372'. Not always needed. If supplied, a validator is automatically constructed.get_parser
, a parser of the raw return value. Since all VISA instruments return strings, but users usually want numbers, int
and float
are popular get_parsers
docstring
A short string describing the function of the parameterGolden rule: if a Parameter
is settable, it must always accept its own output as input.
In most cases you will probably be adding parameters via the add_parameter
method on the instrument class as shown in the example below.
Similar to parameters QCoDeS instruments implement the concept of functions that can be added to the instrument via add_function
. They are meant to implement simple actions on the instrument such as resetting it. However, the functions do not add any value over normal python methods in the driver Class and we are planning to eventually remove them from QCoDeS. We therefore encourage any driver developer to not use function in any new driver.
A Channel
is a submodule holding Parameter
s. It sometimes makes sense to group Parameter
s, for instance when an oscilloscope has four identical input channels. (see Keithley example below)
Every QCoDeS module should have its own logger that is named with the name of the module. So to create a logger put a line at the top of the module like this:
log = logging.getLogger(__name__)
Use this logger only to log messages that are not originating from an Instrument
instance. For messages from within an instrument instance use the log
member of the Instrument
class, e.g
self.log.info(f"Could not connect at {address}")
This way the instrument name will be prepended to the log message and the log messages can be filtered according to the instrument they originate from. See the example notebook of the logger module for more info (offline,online).
When creating a nested Instrument
, like e.g. something like the InstrumentChannel
class, that has a _parent
property, make sure that this property gets set before calling the super().__init__
method, so that the full name of the instrument gets resolved correctly for the logging.
The Weinschel 8320 driver is about as basic a driver as you can get. It only defines one parameter, "attenuation". All the comments here are my additions to describe what's happening.
class Weinschel_8320(VisaInstrument):
"""
QCoDeS driver for the stepped attenuator
Weinschel is formerly known as Aeroflex/Weinschel
"""
# all instrument constructors should accept **kwargs and pass them on to
# super().__init__
def __init__(self, name, address, **kwargs):
# supplying the terminator means you don't need to remove it from every response
super().__init__(name, address, terminator='\r', **kwargs)
self.add_parameter('attenuation', unit='dB',
# the value you set will be inserted in this command with
# regular python string substitution. This instrument wants
# an integer zero-padded to 2 digits. For robustness, don't
# assume you'll get an integer input though - try to allow
# floats (as opposed to {:0=2d})
set_cmd='ATTN ALL {:02.0f}',
get_cmd='ATTN? 1',
# setting any attenuation other than 0, 2, ... 60 will error.
vals=vals.Enum(*np.arange(0, 60.1, 2).tolist()),
# the return value of get() is a string, but we want to
# turn it into a (float) number
get_parser=float)
# it's a good idea to call connect_message at the end of your constructor.
# this calls the 'IDN' parameter that the base Instrument class creates for
# every instrument (you can override the `get_idn` method if it doesn't work
# in the standard VISA form for your instrument) which serves two purposes:
# 1) verifies that you are connected to the instrument
# 2) gets the ID info so it will be included with metadata snapshots later.
self.connect_message()
# instantiating and using this instrument (commented out because I can't actually do it!)
#
# from qcodes.instrument_drivers.weinschel.Weinschel_8320 import Weinschel_8320
# weinschel = Weinschel_8320('w8320_1', 'TCPIP0::172.20.2.212::inst0::INSTR')
# weinschel.attenuation(40)
The Keithley 2600 sourcemeter driver uses two channels. The actual driver is quite long, so here we show an abridged version that has:
Channel
. All the Parameter
s of the Channel
go here.class KeithleyChannel(InstrumentChannel):
"""
Class to hold the two Keithley channels, i.e.
SMUA and SMUB.
"""
def __init__(self, parent: Instrument, name: str, channel: str) -> None:
"""
Args:
parent: The Instrument instance to which the channel is
to be attached.
name: The 'colloquial' name of the channel
channel: The name used by the Keithley, i.e. either
'smua' or 'smub'
"""
if channel not in ['smua', 'smub']:
raise ValueError('channel must be either "smub" or "smua"')
super().__init__(parent, name)
self.model = self._parent.model
vranges = self._parent._vranges
iranges = self._parent._iranges
self.add_parameter('volt',
get_cmd='{}.measure.v()'.format(channel),
get_parser=float,
set_cmd='{}.source.levelv={}'.format(channel,
'{:.12f}'),
# note that the set_cmd is either the following format string
#'smua.source.levelv={:.12f}' or 'smub.source.levelv={:.12f}'
# depending on the value of `channel`
label='Voltage',
unit='V')
self.add_parameter('curr',
get_cmd='{}.measure.i()'.format(channel),
get_parser=float,
set_cmd='{}.source.leveli={}'.format(channel,
'{:.12f}'),
label='Current',
unit='A')
self.add_parameter('mode',
get_cmd='{}.source.func'.format(channel),
get_parser=float,
set_cmd='{}.source.func={}'.format(channel, '{:d}'),
val_mapping={'current': 0, 'voltage': 1},
docstring='Selects the output source.')
self.add_parameter('output',
get_cmd='{}.source.output'.format(channel),
get_parser=float,
set_cmd='{}.source.output={}'.format(channel,
'{:d}'),
val_mapping={'on': 1, 'off': 0})
self.add_parameter('nplc',
label='Number of power line cycles',
set_cmd='{}.measure.nplc={}'.format(channel,
'{:.4f}'),
get_cmd='{}.measure.nplc'.format(channel),
get_parser=float,
vals=vals.Numbers(0.001, 25))
self.channel = channel
class Keithley_2600(VisaInstrument):
"""
This is the qcodes driver for the Keithley_2600 Source-Meter series,
tested with Keithley_2614B
"""
def __init__(self, name: str, address: str, **kwargs) -> None:
"""
Args:
name: Name to use internally in QCoDeS
address: VISA ressource address
"""
super().__init__(name, address, terminator='\n', **kwargs)
model = self.ask('localnode.model')
knownmodels = ['2601B', '2602B', '2604B', '2611B', '2612B',
'2614B', '2635B', '2636B']
if model not in knownmodels:
kmstring = ('{}, '*(len(knownmodels)-1)).format(*knownmodels[:-1])
kmstring += 'and {}.'.format(knownmodels[-1])
raise ValueError('Unknown model. Known model are: ' + kmstring)
# Add the channel to the instrument
for ch in ['a', 'b']:
ch_name = 'smu{}'.format(ch)
channel = KeithleyChannel(self, ch_name, ch_name)
self.add_submodule(ch_name, channel)
# display parameter
# Parameters NOT specific to a channel still belong on the Instrument object
# In this case, the Parameter controls the text on the display
self.add_parameter('display_settext',
set_cmd=self._display_settext,
vals=vals.Strings())
self.connect_message()
As mentioned above, drivers subclassing VisaInstrument
have the nice property that they may be connected to a simulated version of the physical instrument. See the Creating Simulated PyVISA Instruments
notebook for more information. If you are writing a VisaInstrument
driver, please consider spending 20 minutes to also add a simulated instrument and a test.
The Alazar cards use their own DLL. C interfaces tend to need a lot of boilerplate, so I'm not going to include it all. The key is: use Instrument
directly, load the DLL, and have parameters interact with it.
class AlazarTech_ATS(Instrument):
dll_path = 'C:\\WINDOWS\\System32\\ATSApi'
def __init__(self, name, system_id=1, board_id=1, dll_path=None, **kwargs):
super().__init__(name, **kwargs)
# connect to the DLL
self._ATS_dll = ctypes.cdll.LoadLibrary(dll_path or self.dll_path)
self._handle = self._ATS_dll.AlazarGetBoardBySystemID(system_id,
board_id)
if not self._handle:
raise Exception('AlazarTech_ATS not found at '
'system {}, board {}'.format(system_id, board_id))
self.buffer_list = []
# the Alazar driver includes its own parameter class to hold values
# until later config is called, and warn if you try to read a value
# that hasn't been sent to config.
self.add_parameter(name='clock_source',
parameter_class=AlazarParameter,
label='Clock Source',
unit=None,
value='INTERNAL_CLOCK',
byte_to_value_dict={1: 'INTERNAL_CLOCK',
4: 'SLOW_EXTERNAL_CLOCK',
5: 'EXTERNAL_CLOCK_AC',
7: 'EXTERNAL_CLOCK_10_MHz_REF'})
# etc...
A totally manual instrument (like the ithaco 1211) will contain only ManualParameter
s. Some instruments may have a mix of manual and standard parameters. Here we also define a new CurrentParameter
class that uses the ithaco parameters to convert a measured voltage to a current. When subclassing a parameter class (Parameter
, MultiParameter
, ...), the functions for setting and getting should be called get_raw
and set_raw
, respectively.
class CurrentParameter(MultiParameter):
"""
Current measurement via an Ithaco preamp and a measured voltage.
To be used when you feed a current into the Ithaco, send the Ithaco's
output voltage to a lockin or other voltage amplifier, and you have
the voltage reading from that amplifier as a qcodes parameter.
``CurrentParameter.get()`` returns ``(voltage_raw, current)``
Args:
measured_param (Parameter): a gettable parameter returning the
voltage read from the Ithaco output.
c_amp_ins (Ithaco_1211): an Ithaco instance where you manually
maintain the present settings of the real Ithaco amp.
name (str): the name of the current output. Default 'curr'.
Also used as the name of the whole parameter.
"""
def __init__(self, measured_param, c_amp_ins, name='curr'):
p_name = measured_param.name
p_label = getattr(measured_param, 'label', None)
p_unit = getattr(measured_param, 'units', None)
super().__init__(name=name, names=(p_name+'_raw', name),
shapes=((), ()),
labels=(p_label, 'Current'),
units=(p_unit, 'A'))
self._measured_param = measured_param
self._instrument = c_amp_ins
def get_raw(self):
volt = self._measured_param.get()
current = (self._instrument.sens.get() *
self._instrument.sens_factor.get()) * volt
if self._instrument.invert.get():
current *= -1
value = (volt, current)
self._save_val(value)
return value
class Ithaco_1211(Instrument):
"""
This is the qcodes driver for the Ithaco 1211 Current-preamplifier.
This is a virtual driver only and will not talk to your instrument.
"""
def __init__(self, name, **kwargs):
super().__init__(name, **kwargs)
# ManualParameter has an "initial_value" kwarg, but if you use this
# you must be careful to check that it's correct before relying on it.
# if you don't set initial_value, it will start out as None.
self.add_parameter('sens',
parameter_class=ManualParameter,
initial_value=1e-8,
label='Sensitivity',
units='A/V',
vals=vals.Enum(1e-11, 1e-10, 1e-09, 1e-08, 1e-07,
1e-06, 1e-05, 1e-4, 1e-3))
self.add_parameter('invert',
parameter_class=ManualParameter,
initial_value=True,
label='Inverted output',
vals=vals.Bool())
self.add_parameter('sens_factor',
parameter_class=ManualParameter,
initial_value=1,
label='Sensitivity factor',
units=None,
vals=vals.Enum(0.1, 1, 10))
self.add_parameter('suppression',
parameter_class=ManualParameter,
initial_value=1e-7,
label='Suppression',
units='A',
vals=vals.Enum(1e-10, 1e-09, 1e-08, 1e-07, 1e-06,
1e-05, 1e-4, 1e-3))
self.add_parameter('risetime',
parameter_class=ManualParameter,
initial_value=0.3,
label='Rise Time',
units='msec',
vals=vals.Enum(0.01, 0.03, 0.1, 0.3, 1, 3, 10, 30,
100, 300, 1000))
def get_idn(self):
return {'vendor': 'Ithaco (DL Instruments)', 'model': '1211',
'serial': None, 'firmware': None}
When you call:
self.add_parameter(name, **kwargs)
you create a Parameter
. But with the parameter_class
kwarg you can invoke any class you want:
self.add_parameter(name, parameter_class=OtherClass, **kwargs)
Parameter
handles most common instrument settings and measurements.ask
and write
methods, or functions/methods. The set and get commands may also be set to False
and None
. False
corresponds to "no get/set method available" (example: the reading of a voltmeter is not settable, so we set set_cmd=False
). None
corresponds to a manually updated parameter (example: an instrument with no remote interface).add_parameter
, if it accepts name
and instrument
as constructor kwargs. Generally these should subclasses of Parameter
, ParameterWithSetpoints
, ArrayParameter
, or MultiParameter
.ParameterWithSetpoints
is specifically designed to handle the situations where the instrument returns an array of data with assosiated setpoints. An example of how to use it can be found in the notebook Simple Example of ParameterWithSetpoints
ArrayParameter
is an older alternative that does the same thing. However, it is significantly less flexible and much harder to use correct but used in a significant number of drivers. It is not recommended for any new driver.
MultiParameter
is designed to for the situation where multiple different types of data is captured from the same instrument command.
Frequently, an instrument has parameters which can be expressed in terms of "something is on or off". Moreover, usually it is not easy to translate the lingo of the instrument to something that can have simply the value of True
or False
(which are typical in software). Even further, it may be difficult to find consensus between users on a convention: is it on
/off
, or ON
/OFF
, or python True
/False
, or 1
/0
, or else?
This case becomes even more complex if the instrument's API (say, corresponding VISA command) uses unexpected values for such a parameter, for example, turning an output "on" corresponds to a VISA command DEV:CH:BLOCK 0
which means "set blocking of the channel to 0 where 0 has the meaning of the boolean value False, and alltogether this command actually enables the output on this channel".
This results in inconsistency among instrument drivers where for some instrument, say, a display
parameter has 'on'/'off' values for input, while for a different instrument a similar display
parameter has 'ON'
/'OFF'
values or 1
/0
.
Note that this particular example of a display
parameter is trivial because the ambiguity and inconsistency for "this kind" of parameters can be solved by having the name of the parameter be display_enabled
and the allowed input values to be python bool
True
/False
.
Anyway, when defining parameters where the solution does not come trivially, please, consider setting val_mapping
of a parameter to the output of create_on_off_val_mapping(on_val=<>, off_val=<>)
function from qcodes.utils.helpers
package. The function takes care of creating a val_mapping
dictionary that maps given instrument-side values of on_val
and off_val
to True
/False
, 'ON'
/'OFF'
, 'on'
/'off'
, and other commonly used ones. Note that when getting a value of such a parameter, the user will not get 'ON'
or 'off'
or 'oFF'
- instead, True
/False
will be returned.
Sometimes when conditions change (for example, the mode of operation of the instrument is changed from current to voltage measurement) you want different parameters to be available.
To delete existing parameters:
del self.parameters[name_to_delete]
And to add more, do the same thing as you did initially:
self.add_parameter(new_name, **kwargs)
Your drivers do not need to be part of QCoDeS in order to use them with QCoDeS, but we strongly encourage you to contribute them to the project. That way we prevent duplication of effort, and you will likely get help making the driver better, with more features and better code.
Make one driver per module, inside a directory named for the company (or institution), within the instrument_drivers
directory, following the convention:
instrument_drivers.<company>.<model>.<company>_<model>
instrument_drivers.AlazarTech.ATS9870.AlazarTech_ATS9870
Although the class name can be just the model if it is globally unambiguous. For example:
instrument_drivers.stanford_research.SR560.SR560
And note that due to mergers, some drivers may not be in the folder you expect:
instrument_drivers.tektronix.Keithley_2600.Keithley_2600_Channels
A driver should be documented in the following ways.
docs/example/driver_examples/Qcodes example with <company> <model>.ipynb
Note that we execute notebooks by default as part of the docs build. That is usually not possible for instrument examples so we want to disable the execution. This can be done as described here editing the notebooks metadata accessible via Edit/Edit Notebook Metadata
from the notebook interface.