{ "cells": [ { "attachments": {}, "cell_type": "markdown", "metadata": {}, "source": [ "# Creating QCoDeS instrument drivers" ] }, { "cell_type": "code", "execution_count": 1, "metadata": {}, "outputs": [], "source": [ "# most of the drivers only need a couple of these... moved all up here for clarity below\n", "import ctypes # only for DLL-based instrument\n", "import sys\n", "import time\n", "from typing import TYPE_CHECKING, Any\n", "\n", "if TYPE_CHECKING:\n", " from typing_extensions import (\n", " Unpack, # can be imported from typing if python >= 3.12\n", " )\n", "\n", "import numpy as np\n", "\n", "from qcodes import validators as vals\n", "from qcodes.instrument import (\n", " Instrument,\n", " InstrumentBaseKWArgs,\n", " InstrumentChannel,\n", " VisaInstrument,\n", " VisaInstrumentKWArgs,\n", ")\n", "from qcodes.instrument_drivers.AlazarTech.utils import TraceParameter\n", "from qcodes.parameters import ManualParameter, MultiParameter, Parameter" ] }, { "attachments": {}, "cell_type": "markdown", "metadata": {}, "source": [ "## Base Classes\n", "\n", "There are 3 available:\n", "- `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.\n", "- `IPInstrument` - a deprecated driver just for ethernet connections. Do not use this; use `VisaInstrument` instead.\n", "- `Instrument` - superclass of both `VisaInstrument` and `IPInstrument`, use this if you do not communicate over a text channel, for example:\n", " - PCI cards with their own DLLs\n", " - Instruments with only manual controls.\n", " \n", "If possible, please use a `VisaInstrument`, as this allows for the creation of a simulated instrument. (See the [Creating Simulated PyVISA Instruments](Creating-Simulated-PyVISA-Instruments.ipynb) notebook) " ] }, { "attachments": {}, "cell_type": "markdown", "metadata": {}, "source": [ "## Parameters and Channels\n", "\n", "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`.\n", "\n", "### What's a Parameter?\n", "\n", "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:\n", "\n", " * `name`, the name used internally by QCoDeS, e.g. 'input_impedance'\n", " * `instrument`, the instrument this parameter belongs to, if any.\n", " * `label`, the label to use for plotting this parameter\n", " * `unit`, the physical unit. ALWAYS use SI units if a unit is applicable\n", " * `set_cmd`, the command to set the parameter. Either a SCPI string with a single '{}', or a function taking one argument (see examples below)\n", " * `get_cmd`, the command to get the parameter. Follows the same scheme as `set_cmd`\n", " * `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.\n", " * `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.\n", " * `max_val_age`: Max time (in seconds) to trust a value stored in cache. If the parameter has not been set or measured more recently than this, an additional measurement will be performed in order to update the cached value. If it is ``None``, this behavior is disabled. ``max_val_age`` should not be used for a parameter that does not have a get function.\n", " * `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`\n", " * `docstring` A short string describing the function of the parameter\n", " \n", "Golden rule: if a `Parameter` is settable, it must always accept its own output as input.\n", "\n", "There are two different ways of adding parameters to instruments. They are almost equivalent but comes with some trade-offs. We will show both below.\n", "You may either declare the parameter as an attribute directly on the instrument or add it via the via the `add_parameter` method on the instrument class.\n", "\n", "Declaring a parameter as an attribute directly on the instrument enables Sphinx, IDEs such as VSCode and static tools such as Mypy to work more fluently with \n", "the parameter than if it is created via `add_parameter` however you must take care to remember to pass `instrument=self` to the parameter such that the\n", "parameter will know which instrument it belongs to. \n", "Instrument.add_parameter is better suited for when you want to dynamically or programmatically add a parameter to an instrument. For historical reasons most \n", "instruments currently use `add_parameter`.\n", "\n", "\n", "### Functions\n", "\n", "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**.\n", "\n", "### What's an InstrumentModule, then?\n", "\n", "An `InstrumentModule` is a submodule of the instrument 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)\n", "or when it makes sense to group a particular set of parameters into their own module (such as a trigger module containing trigger related settings) \n", "\n", "`InstrumentChannel` is a subclass of `InstrumentModule` which behaves identically to `InstrumentModule` you should chose either one depending on if you are implementing a module or a channel. As a rule of thumb you should use `InstrumentChannel` for something that the instrument has more than one of.\n" ] }, { "attachments": {}, "cell_type": "markdown", "metadata": {}, "source": [ "## Naming Instruments \n", "\n", "We are aiming to organize drivers in QCoDeS in a consistent way for easy discovery.\n", "Note that not all drivers in QCoDeS are currently named consistently. \n", "However, we aim to gradually update all drivers to be named as outlined above and any new driver \n", "should be named in the way outlined below.\n", "\n", "The same rules should apply for QCoDeS-contrib-drivers with the exception that all drivers are stored in subfolders of the drivers folder. \n", "\n", "### Naming the Instrument class\n", "A driver for an instrument with model `Model` and from the vendor `Vendor` should be stored in the file:\n", "\n", "```\n", "qcodes\\instrument_drivers\\{Vendor}\\{Vendor}_{Model}.py \n", "```\n", "using snake case with an underscore between the vendor and model name but starting the\n", "vendor name with upper case.\n", "\n", "The primary instrument class should be named as follows:\n", "```\n", "class {Vendor}{Model}\n", " ...\n", "```\n", "E.g Vendor followed by Model number in CamelCase.\n", "\n", "Note that we use vendor names starting with upper case for both folders and file names.\n", "\n", "It is also fine to use an acronym for instrument vendors when there are well established. E.g. drivers for `American Magnetics Inc.` instruments\n", "may use the acronym `AMI` to refer to the vendor.\n", "\n", "As an example the driver for the Weinschel 8320 should be stored in the file `qcodes\\instrument_drivers\\Weinschel\\Weinschel_8320.py` and the \n", "class named `Weinschel8320` \n", "\n", "### Naming InstrumentModule classes\n", "\n", "`InstrumentModule`s and `InstrumentChannel`s should be defined in the same file as the driver that they are part of. The classes should preferable be named such that it is clear\n", "from the name which instrument it belongs to. E.g a hypothetical `InstrumentChannel` belonging to a Weinschel 8320 should be named `Weinschel8320Channel` or similar.\n", "\n", "\n", "### Driver supporting multiple models.\n", "\n", "Often instrument vendors supply multiple instruments in a family with very similar specs only different in limits such as voltage ranges or \n", "highest supported frequency.\n", "\n", "\n", "As an example have a look at the Keysight 344xxA series of digital multi meters. To implement drivers for such instruments it is preferable\n", "to implement a private base class such as `_Keysight344xxA`. This class should be stored either in a `private` subfolder of the Vendor folder or\n", "in a file starting with an underscore i.e. `_Keysight344xxA.py`. If possible, we prefer a format where `x` is used to signal the parts of the model numbers that \n", "may change. Along with this class subclasses for each of the supported models should be implemented. These may either make small modifications to the baseclass as needed \n", "or be empty subclasses if no modifications are needed. \n", "\n", "E.g. subclasses of the Keysight 344xxA driver for the specific model `34410A` should be named as `Keysight34410A` and stored in `Keysight34410A.py`.\n", "\n" ] }, { "attachments": {}, "cell_type": "markdown", "metadata": {}, "source": [ "## Logging\n", "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:\n", "```\n", "log = logging.getLogger(__name__)\n", "```\n", "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\n", "```\n", "self.log.info(f\"Could not connect at {address}\")\n", "```\n", "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](../logging/logging_example.ipynb) notebook of the logger module for more info.\n", "\n", "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.\n" ] }, { "attachments": {}, "cell_type": "markdown", "metadata": {}, "source": [ "## VisaInstrument: Simple example\n", "\n", "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." ] }, { "cell_type": "code", "execution_count": 3, "metadata": {}, "outputs": [], "source": [ "class Weinschel8320(VisaInstrument):\n", " \"\"\"\n", " QCoDeS driver for the stepped attenuator\n", " Weinschel is formerly known as Aeroflex/Weinschel\n", " \"\"\"\n", "\n", " # all instrument constructors should accept **kwargs and pass them on to\n", " # super().__init__ By using Unpack[VisaKWArgs] we ensure that the instrument class takes exactly the same keyword args as the base class\n", " # Visa instruments are also required to take name, address and terminator as arguments.\n", " # name and address should be positional arguments. To overwrite the default terminator or timeout\n", " # the attribute default_terminator or default_timeout should be defined as a class attribute in the instrument class\n", " # as shown below for the default_terminator\n", " default_terminator = \"\\r\"\n", "\n", " def __init__(\n", " self, name: str, address: str, **kwargs: \"Unpack[VisaInstrumentKWArgs]\"\n", " ):\n", " super().__init__(name, address, **kwargs)\n", "\n", " self.attenuation = Parameter(\n", " \"attenuation\",\n", " unit=\"dB\",\n", " # the value you set will be inserted in this command with\n", " # regular python string substitution. This instrument wants\n", " # an integer zero-padded to 2 digits. For robustness, don't\n", " # assume you'll get an integer input though - try to allow\n", " # floats (as opposed to {:0=2d})\n", " set_cmd=\"ATTN ALL {:02.0f}\",\n", " get_cmd=\"ATTN? 1\",\n", " # setting any attenuation other than 0, 2, ... 60 will error.\n", " vals=vals.Enum(*np.arange(0, 60.1, 2).tolist()),\n", " # the return value of get() is a string, but we want to\n", " # turn it into a (float) number\n", " get_parser=float,\n", " instrument=self,\n", " )\n", " \"\"\"Control the attenuation\"\"\"\n", " # The docstring below the Parameter declaration makes Sphinx document the attribute and it is therefore\n", " # possible to see from the documentation that the instrument has this parameter. It is strongly encouraged to\n", " # add a short docstring like this.\n", "\n", " # it's a good idea to call connect_message at the end of your constructor.\n", " # this calls the 'IDN' parameter that the base Instrument class creates for\n", " # every instrument (you can override the `get_idn` method if it doesn't work\n", " # in the standard VISA form for your instrument) which serves two purposes:\n", " # 1) verifies that you are connected to the instrument\n", " # 2) gets the ID info so it will be included with metadata snapshots later.\n", " self.connect_message()\n", "\n", "\n", "# instantiating and using this instrument (commented out because I can't actually do it!)\n", "#\n", "# from qcodes.instrument_drivers.weinschel.Weinschel_8320 import Weinschel8320\n", "# weinschel = Weinschel8320('w8320_1', 'TCPIP0::172.20.2.212::inst0::INSTR')\n", "# weinschel.attenuation(40)" ] }, { "attachments": {}, "cell_type": "markdown", "metadata": {}, "source": [ "## VisaInstrument: a more involved example\n", "\n", "The Keithley 2600 sourcemeter driver uses two channels. The actual driver is quite long, so here we show an abridged version that has:\n", "\n", "- A class defining a `Channel`. All the `Parameter`s of the `Channel` go here. \n", "- A nifty way to look up the model number, allowing it to be a driver for many different Keithley models\n", "\n", "\n" ] }, { "cell_type": "code", "execution_count": 4, "metadata": {}, "outputs": [], "source": [ "class KeithleyChannel(InstrumentChannel):\n", " \"\"\"\n", " Class to hold the two Keithley channels, i.e.\n", " SMUA and SMUB.\n", " \"\"\"\n", "\n", " def __init__(self, parent: Instrument, name: str, channel: str, **kwargs: \"Unpack[InstrumentBaseKWArgs]\") -> None:\n", " \"\"\"\n", " Args:\n", " parent: The Instrument instance to which the channel is\n", " to be attached.\n", " name: The 'colloquial' name of the channel\n", " channel: The name used by the Keithley, i.e. either\n", " 'smua' or 'smub'\n", " **kwargs: Forwarded to base class.\n", " \"\"\"\n", "\n", " if channel not in [\"smua\", \"smub\"]:\n", " raise ValueError('channel must be either \"smub\" or \"smua\"')\n", "\n", " super().__init__(parent, name, **kwargs)\n", " self.model = self._parent.model\n", "\n", " self.volt = Parameter(\n", " \"volt\",\n", " get_cmd=f\"{channel}.measure.v()\",\n", " get_parser=float,\n", " set_cmd=f\"{channel}.source.levelv={{:.12f}}\",\n", " # note that the set_cmd is either the following format string\n", " #'smua.source.levelv={:.12f}' or 'smub.source.levelv={:.12f}'\n", " # depending on the value of `channel`\n", " label=\"Voltage\",\n", " unit=\"V\",\n", " instrument=self,\n", " )\n", "\n", " self.curr = Parameter(\n", " \"curr\",\n", " get_cmd=f\"{channel}.measure.i()\",\n", " get_parser=float,\n", " set_cmd=f\"{channel}.source.leveli={{:.12f}}\",\n", " label=\"Current\",\n", " unit=\"A\",\n", " instrument=self,\n", " )\n", "\n", " self.mode = Parameter(\n", " \"mode\",\n", " get_cmd=f\"{channel}.source.func\",\n", " get_parser=float,\n", " set_cmd=f\"{channel}.source.func={{:d}}\",\n", " val_mapping={\"current\": 0, \"voltage\": 1},\n", " docstring=\"Selects the output source.\",\n", " instrument=self,\n", " )\n", "\n", " self.output = Parameter(\n", " \"output\",\n", " get_cmd=f\"{channel}.source.output\",\n", " get_parser=float,\n", " set_cmd=f\"{channel}.source.output={{:d}}\",\n", " val_mapping={\"on\": 1, \"off\": 0},\n", " instrument=self,\n", " )\n", "\n", " self.nplc = Parameter(\n", " \"nplc\",\n", " label=\"Number of power line cycles\",\n", " set_cmd=f\"{channel}.measure.nplc={{:.4f}}\",\n", " get_cmd=f\"{channel}.measure.nplc\",\n", " get_parser=float,\n", " vals=vals.Numbers(0.001, 25),\n", " instrument=self,\n", " )\n", "\n", " self.channel = channel\n", "\n", "\n", "class Keithley2600(VisaInstrument):\n", " \"\"\"\n", " This is the qcodes driver for the Keithley2600 Source-Meter series,\n", " tested with Keithley2614B\n", " \"\"\"\n", " default_terminator = \"\\n\"\n", "\n", " def __init__(self, name: str, address: str, **kwargs: \"Unpack[VisaInstrumentKWArgs]\") -> None:\n", " \"\"\"\n", " Args:\n", " name: Name to use internally in QCoDeS\n", " address: VISA ressource address\n", " **kwargs: kwargs are forwarded to the base class.\n", " \"\"\"\n", " super().__init__(name, address, **kwargs)\n", "\n", " model = self.ask(\"localnode.model\")\n", "\n", " knownmodels = [\n", " \"2601B\",\n", " \"2602B\",\n", " \"2604B\",\n", " \"2611B\",\n", " \"2612B\",\n", " \"2614B\",\n", " \"2635B\",\n", " \"2636B\",\n", " ]\n", " if model not in knownmodels:\n", " kmstring = (\"{}, \" * (len(knownmodels) - 1)).format(*knownmodels[:-1])\n", " kmstring += f\"and {knownmodels[-1]}.\"\n", " raise ValueError(\"Unknown model. Known model are: \" + kmstring)\n", "\n", " # Add the channel to the instrument\n", " for ch in [\"a\", \"b\"]:\n", " ch_name = f\"smu{ch}\"\n", " channel = KeithleyChannel(self, ch_name, ch_name)\n", " self.add_submodule(ch_name, channel)\n", "\n", " # display parameter\n", " # Parameters NOT specific to a channel still belong on the Instrument object\n", " # In this case, the Parameter controls the text on the display\n", " self.display_settext = Parameter(\n", " \"display_settext\",\n", " set_cmd=self._display_settext,\n", " vals=vals.Strings(),\n", " instrument=self,\n", " )\n", "\n", " self.connect_message()" ] }, { "attachments": {}, "cell_type": "markdown", "metadata": {}, "source": [ "## VisaInstruments: Simulating the instrument\n", "\n", "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](Creating-Simulated-PyVISA-Instruments.ipynb) 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." ] }, { "attachments": {}, "cell_type": "markdown", "metadata": {}, "source": [ "## DLL-based instruments\n", "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." ] }, { "cell_type": "code", "execution_count": 4, "metadata": {}, "outputs": [], "source": [ "class AlazarTechATS(Instrument):\n", " dll_path = \"C:\\\\WINDOWS\\\\System32\\\\ATSApi\"\n", "\n", " def __init__(self, name, system_id=1, board_id=1, dll_path=None, **kwargs: \"Unpack[InstrumentBaseKWArgs]\"):\n", " super().__init__(name, **kwargs)\n", "\n", " # connect to the DLL\n", " self._ATS_dll = ctypes.cdll.LoadLibrary(dll_path or self.dll_path)\n", "\n", " self._handle = self._ATS_dll.AlazarGetBoardBySystemID(system_id, board_id)\n", " if not self._handle:\n", " raise Exception(\n", " f\"AlazarTech_ATS not found at system {system_id}, board {board_id}\"\n", " )\n", "\n", " self.buffer_list = []\n", "\n", " # the Alazar driver includes its own parameter class to hold values\n", " # until later config is called, and warn if you try to read a value\n", " # that hasn't been sent to config.\n", " self.add_parameter(\n", " name=\"clock_source\",\n", " parameter_class=TraceParameter,\n", " label=\"Clock Source\",\n", " unit=None,\n", " value=\"INTERNAL_CLOCK\",\n", " byte_to_value_dict={\n", " 1: \"INTERNAL_CLOCK\",\n", " 4: \"SLOW_EXTERNAL_CLOCK\",\n", " 5: \"EXTERNAL_CLOCK_AC\",\n", " 7: \"EXTERNAL_CLOCK_10MHz_REF\",\n", " },\n", " )\n", "\n", " # etc..." ] }, { "attachments": {}, "cell_type": "markdown", "metadata": {}, "source": [ "It's very typical for DLL based instruments to only be supported on Windows. In such a driver care should be taken to ensure that the driver raises a clear error message if it is initialized on a different platform. This is typically best done by \n", "by checking `sys.platform` as below. In this example we are using `ctypes.windll` to interact with the DLL. `windll` is only defined on on Windows.\n", "\n", "QCoDeS is automatically typechecked with MyPy, this may give some complications for drivers that are not compatible with multiple OSes as there is no supported way to disabling the typecheck on a per platform basis for a specific submodule. \n", "Specifically MyPy will correctly notice that `self.dll` does not exist on non Windows platforms unless we add the line `self.dll: Any = None` to the example below. By giving `self.dll` the type `Any` we effectively disable any typecheck related to `self.dll` on non Windows platforms which is exactly what we want. This works because MyPy knows how to interprete the `sys.platform` check and allows `self.dll` to have different types on different OSes." ] }, { "cell_type": "code", "execution_count": 5, "metadata": {}, "outputs": [], "source": [ "class SomeDLLInstrument(Instrument):\n", " dll_path = \"C:\\\\WINDOWS\\\\System32\\\\ATSApi\"\n", "\n", " def __init__(self, name, dll_path=None, **kwargs: \"Unpack[InstrumentBaseKWArgs]\"):\n", " super().__init__(name, **kwargs)\n", "\n", " if sys.platform != \"win32\":\n", " self.dll: Any = None\n", " raise OSError(\"SomeDLLInsrument only works on Windows\")\n", " else:\n", " self.dll = ctypes.windll.LoadLibrary(dll_path)\n", "\n", " # etc..." ] }, { "attachments": {}, "cell_type": "markdown", "metadata": {}, "source": [ "## Manual instruments\n", "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." ] }, { "cell_type": "code", "execution_count": 5, "metadata": {}, "outputs": [], "source": [ "class CurrentParameter(MultiParameter):\n", " \"\"\"\n", " Current measurement via an Ithaco preamp and a measured voltage.\n", "\n", " To be used when you feed a current into the Ithaco, send the Ithaco's\n", " output voltage to a lockin or other voltage amplifier, and you have\n", " the voltage reading from that amplifier as a qcodes parameter.\n", "\n", " ``CurrentParameter.get()`` returns ``(voltage_raw, current)``\n", "\n", " Args:\n", " measured_param (Parameter): a gettable parameter returning the\n", " voltage read from the Ithaco output.\n", "\n", " c_amp_ins (Ithaco_1211): an Ithaco instance where you manually\n", " maintain the present settings of the real Ithaco amp.\n", "\n", " name (str): the name of the current output. Default 'curr'.\n", " Also used as the name of the whole parameter.\n", " \"\"\"\n", "\n", " def __init__(self, measured_param, c_amp_ins, name=\"curr\", **kwargs):\n", " p_name = measured_param.name\n", "\n", " p_label = getattr(measured_param, \"label\", None)\n", " p_unit = getattr(measured_param, \"units\", None)\n", "\n", " super().__init__(\n", " name=name,\n", " names=(p_name + \"_raw\", name),\n", " shapes=((), ()),\n", " labels=(p_label, \"Current\"),\n", " units=(p_unit, \"A\"),\n", " instrument=c_amp_ins,\n", " **kwargs,\n", " )\n", "\n", " self._measured_param = measured_param\n", "\n", " def get_raw(self):\n", " volt = self._measured_param.get()\n", " current = (\n", " self.instrument.sens.get() * self.instrument.sens_factor.get()\n", " ) * volt\n", "\n", " if self.instrument.invert.get():\n", " current *= -1\n", "\n", " value = (volt, current)\n", " return value\n", "\n", "\n", "class Ithaco1211(Instrument):\n", " \"\"\"\n", " This is the qcodes driver for the Ithaco 1211 Current-preamplifier.\n", "\n", " This is a virtual driver only and will not talk to your instrument.\n", " \"\"\"\n", "\n", " def __init__(self, name: str, **kwargs: \"Unpack[InstrumentBaseKWArgs]\"):\n", " super().__init__(name, **kwargs)\n", "\n", " # ManualParameter has an \"initial_value\" kwarg, but if you use this\n", " # you must be careful to check that it's correct before relying on it.\n", " # if you don't set initial_value, it will start out as None.\n", " self.add_parameter(\n", " \"sens\",\n", " parameter_class=ManualParameter,\n", " initial_value=1e-8,\n", " label=\"Sensitivity\",\n", " units=\"A/V\",\n", " vals=vals.Enum(1e-11, 1e-10, 1e-09, 1e-08, 1e-07, 1e-06, 1e-05, 1e-4, 1e-3),\n", " )\n", "\n", " self.add_parameter(\n", " \"invert\",\n", " parameter_class=ManualParameter,\n", " initial_value=True,\n", " label=\"Inverted output\",\n", " vals=vals.Bool(),\n", " )\n", "\n", " self.add_parameter(\n", " \"sens_factor\",\n", " parameter_class=ManualParameter,\n", " initial_value=1,\n", " label=\"Sensitivity factor\",\n", " units=None,\n", " vals=vals.Enum(0.1, 1, 10),\n", " )\n", "\n", " self.add_parameter(\n", " \"suppression\",\n", " parameter_class=ManualParameter,\n", " initial_value=1e-7,\n", " label=\"Suppression\",\n", " units=\"A\",\n", " vals=vals.Enum(1e-10, 1e-09, 1e-08, 1e-07, 1e-06, 1e-05, 1e-4, 1e-3),\n", " )\n", "\n", " self.add_parameter(\n", " \"risetime\",\n", " parameter_class=ManualParameter,\n", " initial_value=0.3,\n", " label=\"Rise Time\",\n", " units=\"msec\",\n", " vals=vals.Enum(0.01, 0.03, 0.1, 0.3, 1, 3, 10, 30, 100, 300, 1000),\n", " )\n", "\n", " def get_idn(self):\n", " return {\n", " \"vendor\": \"Ithaco (DL Instruments)\",\n", " \"model\": \"1211\",\n", " \"serial\": None,\n", " \"firmware\": None,\n", " }" ] }, { "attachments": {}, "cell_type": "markdown", "metadata": {}, "source": [ "## Custom Parameter classes\n", "\n", "When you call:\n", "```\n", "self.add_parameter(name, **kwargs)\n", "```\n", "you create a `Parameter`. But with the `parameter_class` kwarg you can invoke any class you want:\n", "```\n", "self.add_parameter(name, parameter_class=OtherClass, **kwargs)\n", "```\n", "\n", "- `Parameter` handles most common instrument settings and measurements.\n", " - Accepts get and/or set commands as either strings for the instrument's `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).\n", " - Has options for translating between instrument codes and more meaningful data values\n", " - Supports software-controlled ramping\n", "- Any other parameter class may be used in `add_parameter`, if it accepts `name` and `instrument` as constructor kwargs. Generally these should subclasses of `Parameter`, `ParameterWithSetpoints`, `ArrayParameter`, or `MultiParameter`." ] }, { "attachments": {}, "cell_type": "markdown", "metadata": {}, "source": [ "`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](../Parameters/Simple-Example-of-ParameterWithSetpoints.ipynb)\n", "\n", "`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.**\n", "\n", "`MultiParameter` is designed to for the situation where multiple different types of data is captured from the same instrument command.\n", "\n", "It is important that parameters subclass forwards the `name`, `label(s)`, `unit(s)` and `instrument` along with any unknown `**kwargs` to the superclasss. " ] }, { "attachments": {}, "cell_type": "markdown", "metadata": {}, "source": [ "### On/Off parameters\n", "\n", "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?\n", "\n", "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\".\n", "\n", "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`.\n", "\n", "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`.\n", "\n", "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.parameters` 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." ] }, { "attachments": {}, "cell_type": "markdown", "metadata": {}, "source": [ "## Dynamically adding and removing parameters\n", "\n", "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.\n", "\n", "To delete existing parameters:\n", "```\n", "del self.parameters[name_to_delete]\n", "```\n", "And to add more, do the same thing as you did initially:\n", "```\n", "self.add_parameter(new_name, **kwargs)\n", "```" ] }, { "attachments": {}, "cell_type": "markdown", "metadata": {}, "source": [ "## Handling interruption of measurements\n", "\n" ] }, { "attachments": {}, "cell_type": "markdown", "metadata": {}, "source": [ "A QCoDeS driver should be prepared for interruptions of the measurement triggered by a KeyboardInterrupt from the enduser. \n", "If an interrupt happens at an unfortunate time i.e. while communicating with the instrument or writing results of a measurement this may leave the program in an inconsistent state e.g. with a command in the output buffer of a VISA instrument. To protect against this QCoDeS ships with a context manager that intercepts KeyBoardInterrupts and delays them until it is safe to stop the program. By default QCoDeS protects writing to the database and communicating with VISA instruments in this way. \n", "\n", "\n", "However, there may be situations where a driver needs additional protection around a critical piece of code. The following example shows how a critical piece of code can be protected. The reader is encouraged to experiment with this using the `interrupt the kernel` button in this notebook. Note how the first KeyBoardInterrupt triggers a message to the screen and then executes the code within the context manager but not the code outside. Furthermore 2 KeyBoardInterrupts rapidly after each other will trigger an immediate interrupt that does not complete the code within the context manager. The context manager can therefore be wrapped around any piece of code that the end user should not normally be allowed to interrupt.\n" ] }, { "cell_type": "code", "execution_count": 7, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "0\n", "1\n", "2\n", "3\n", "4\n", "5\n", "6\n", "7\n", "8\n", "9\n", "Loop completed\n", "Executing code after context manager\n" ] } ], "source": [ "from qcodes.utils.delaykeyboardinterrupt import DelayedKeyboardInterrupt\n", "\n", "with DelayedKeyboardInterrupt():\n", " for i in range(10):\n", " time.sleep(0.2)\n", " print(i)\n", " print(\"Loop completed\")\n", "print(\"Executing code after context manager\")" ] }, { "attachments": {}, "cell_type": "markdown", "metadata": {}, "source": [ "## Organization\n", "\n", "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 [qcodes contrib drivers](https://github.com/QCoDeS/Qcodes_contrib_drivers) project. That way we prevent duplication of effort, and you will likely get help making the driver better, with more features and better code. \n", "\n", "Make one driver per module, inside a directory named for the company (or institution), within the `instrument_drivers` directory, following the convention:\n", "\n", "`instrument_drivers..._`\n", "- example: `instrument_drivers.AlazarTech.ATS9870.AlazarTech_ATS9870`\n", "\n", "Although the class name can be just the model if it is globally unambiguous. For example:\n", "- example: `instrument_drivers.stanford_research.SR560.SR560`\n", "\n", "And note that due to mergers, some drivers may not be in the folder you expect:\n", "- example: `instrument_drivers.tektronix.Keithley_2600.Keithley_2600_Channels`" ] }, { "attachments": {}, "cell_type": "markdown", "metadata": {}, "source": [ "## Documentation\n", "\n", "A driver should be documented in the following ways. \n", "\n", "* All methods of the driver class should be documented including the arguments and return type of the function. QCoDeS docstrings uses the [Google style](https://sphinxcontrib-napoleon.readthedocs.io/en/latest/example_google.html)\n", "* Parameters should have a meaningful docstring if the usage of the parameter is not obvious.\n", "* An IPython notebook that documents the usage of the instrument should be added to `docs/example/driver_examples/Qcodes example with .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](https://nbsphinx.readthedocs.io/en/latest/never-execute.html) editing the notebooks metadata accessible via `Edit/Edit Notebook Metadata` from the notebook interface.\n", "\n" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [] } ], "metadata": { "kernelspec": { "display_name": "Python 3 (ipykernel)", "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.8.12" }, "toc": { "base_numbering": 1, "nav_menu": {}, "number_sections": true, "sideBar": true, "skip_h1_title": false, "title_cell": "Table of Contents", "title_sidebar": "Contents", "toc_cell": false, "toc_position": {}, "toc_section_display": true, "toc_window_display": false }, "varInspector": { "cols": { "lenName": 16, "lenType": 16, "lenVar": 40 }, "kernels_config": { "python": { "delete_cmd_postfix": "", "delete_cmd_prefix": "del ", "library": "var_list.py", "varRefreshCmd": "print(var_dic_list())" }, "r": { "delete_cmd_postfix": ") ", "delete_cmd_prefix": "rm(", "library": "var_list.r", "varRefreshCmd": "cat(var_dic_list()) " } }, "types_to_exclude": [ "module", "function", "builtin_function_or_method", "instance", "_Feature" ], "window_display": false }, "widgets": { "application/vnd.jupyter.widget-state+json": { "state": {}, "version_major": 2, "version_minor": 0 } } }, "nbformat": 4, "nbformat_minor": 4 }