{ "cells": [ { "cell_type": "markdown", "metadata": {}, "source": [ "# Creating Simulated PyVISA Instruments\n", "\n", "When developing stuff in a large codebase like QCoDeS, it is often uncanningly easy to submit a change that breaks stuff. Therefore, _continuous integration_ is performed in the form of automated tests that run before new code is allowed into the codebase. The many tests of QCoDeS can be found in the `tests` folder. \n", "\n", "But how about drivers? They constitute the majority of the codebase, but how can we test them? Wouldn't that require a physical copy each instrument to be present on the California server where we run our tests? It used to be so, but not anymore! For drivers utilising PyVISA (i.e. `VisaInstrument` drivers), we may create simulated instruments to which the drivers may connect.\n", "\n", "## What?\n", "\n", "This way, we may instantiate drivers and run simple tests on them. Tests like:\n", "\n", " * Can the driver even instantiate? This is very relevant when underlying APIs change.\n", " * Is the drivers (e.g.) \"voltage-to-bytecode\" converter working properly?\n", "\n", "## Not!\n", "\n", "It is not feasible to simulate any but the most trivial features of the instrument. Simulated instruments can not and should not perform tests like:\n", "\n", " * Do we wait sufficiently long for this oscilloscope's trace to be acquired?\n", " * Does our driver handle overlapping commands of this AWG correctly?\n", " \n", "## How?\n", "\n", "The basic scheme goes as follows:\n", "\n", " * Write a `.yaml` file for the simulated instrument. The instructions for that may be found here: https://pyvisa-sim.readthedocs.io/en/latest/ and specifically here: https://pyvisa-sim.readthedocs.io/en/latest/definitions.html#definitions\n", " * Then write a test for your instrument and put it in `tests/drivers`. The file should have the name `test_.py`. \n", " * Check that all is well by running `$ pytest test_.py`.\n", " \n", "Below is an example.\n" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Example: Weinschel8320\n", "\n", "The Weinschel 8320 is a very simple driver." ] }, { "cell_type": "code", "execution_count": 1, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Logging hadn't been started.\n", "Activating auto-logging. Current session state plus future input saved.\n", "Filename : C:\\Users\\jenielse\\.qcodes\\logs\\command_history.log\n", "Mode : append\n", "Output logging : True\n", "Raw input log : False\n", "Timestamping : True\n", "State : active\n", "Qcodes Logfile : C:\\Users\\jenielse\\.qcodes\\logs\\221108-14924-qcodes.log\n" ] } ], "source": [ "import numpy as np\n", "\n", "import qcodes.validators as vals\n", "from qcodes.instrument.visa import VisaInstrument\n", "\n", "\n", "class Weinschel8320(VisaInstrument):\n", " \"\"\"\n", " QCoDeS driver for the stepped attenuator\n", " Weinschel is formerly known as Aeroflex/Weinschel\n", " \"\"\"\n", "\n", " def __init__(self, name, address, **kwargs):\n", " super().__init__(name, address, terminator='\\r', **kwargs)\n", "\n", " self.add_parameter('attenuation', unit='dB',\n", " set_cmd='ATTN ALL {:02.0f}',\n", " get_cmd='ATTN? 1',\n", " vals=vals.Enum(*np.arange(0, 60.1, 2).tolist()),\n", " get_parser=float)\n", "\n", " self.connect_message()" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### The `.yaml` file" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "The simplest `.yaml` file that is still useful, reads, in all its glory:" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "```\n", "spec: \"1.0\"\n", "devices:\n", " device 1:\n", " eom:\n", " GPIB INSTR:\n", " q: \"\\r\" # MAKE SURE! that this matches the terminator of the driver!\n", " r: \"\\r\"\n", " error: ERROR\n", " dialogues:\n", " - q: \"*IDN?\"\n", " r: \"QCoDeS, Weinschel 8320 (Simulated), 1337, 0.0.01\"\n", "\n", "\n", "resources: \n", " GPIB::1::INSTR:\n", " device: device 1\n", "\n", "```" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Note that since no physical connection is made, it doesn't matter what interface we pretend to use (GPIB, USB, ethernet, serial, ...). As a convention, we always write GPIB in the `.yaml` files.\n", "This simulates an instrument with no settable parameter; only an `*IDN?` response. This is enough to instantiate the instrument.\n", "\n", "We save the above file as `qcodes/instrument/sims/Weinschel_8320.yaml`. Note that in this example we have cheated a bit and already written the file in that location.\n", "\n", "Then we may connect to the simulated instrument.\n", "\n", "There are two different ways we may tell pyvisa-sim which yaml file to make use of. \n", "We can either follow the official instructions and pass visalib as a string of the form\n", "\"pathtoyamlfile@sim\" where ``sim`` indicates that we make use of the simulated backend.\n", "This is however not very convenient when the yaml file is part of a package since we\n", "would need to know the absolute path. It is therefor also possible to use the argument\n", "pyvisa_sim_file. This can be given in two forms either just the name of the file such\n", "as ``Weinschel_8320.yaml`` in which case we search for the file in the ``qcodes.instrument.sims`` folder.\n", "Or alternatively in the format ``mymodule.mysubmodule:name_of_file`` e.g. ``qcodes.instrument.sims:Weinschel_8320.yaml``\n", "in which case we search for the file in the supplied module.\n" ] }, { "cell_type": "code", "execution_count": 2, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Connected to: QCoDeS Weinschel 8320 (Simulated) (serial:1337, firmware:0.0.01) in 0.07s\n" ] } ], "source": [ "wein_sim = Weinschel8320('wein_sim',\n", " address='GPIB::1::INSTR', # This matches the address in the .yaml file\n", " pyvisa_sim_file=\"Weinschel_8320.yaml\"\n", " )" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### The test" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Now we can write a useful test!" ] }, { "cell_type": "code", "execution_count": 3, "metadata": {}, "outputs": [], "source": [ "import pytest\n", "\n", "from qcodes.instrument_drivers.weinschel import Weinschel8320\n", "\n", "\n", "# The following decorator makes the driver\n", "# available to all the functions in this module\n", "@pytest.fixture(scope='function', name=\"weinschel_driver_1\")\n", "def _weinschel_driver_1():\n", " wein_sim = Weinschel8320('wein_sim',\n", " address='GPIB::1::65535::INSTR',\n", " pyvisa_sim_file=\"Weinschel_8320.yaml\"\n", " )\n", " yield wein_sim\n", "\n", " wein_sim.close()\n", "\n", "\n", "def test_init_v1(weinschel_driver_1):\n", " \"\"\"\n", " Test that simple initialisation works\n", " \"\"\"\n", "\n", " # There is not that much to do, really.\n", " # We can check that the IDN string reads back correctly\n", "\n", " idn_dict = weinschel_driver_1.IDN()\n", "\n", " assert idn_dict['vendor'] == 'QCoDeS'\n" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Save the test as `tests/drivers/test_weinschel_8320.py`. " ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Open a command line/console/terminal, navigate to the `tests/drivers/` folder and run\n", "```\n", ">> pytest test_weinschel_8320.py\n", "```\n", "\n", "This should give you an output similar to\n", "```\n", "========================================= 1 passed in 0.73 seconds ==========================================\n", "```" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Congratulations! That was it." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Bonus example: including parameters in the simulated instrument" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "It is also possible to add queriable parameters to the `.yaml` file, but testing that you can read those back is of limited value. You should only add them if your driver needs them to instantiate, e.g. if it checks that some range or impedance is configured correctly on startup, or - more generally - if a part of your driver code that you'd like to test needs it to run.\n", "\n", "For the sake of this example, let us add a test that the driver's parameter's validator will reject an attenuation of less than 0 dBm. Note that this concrete test is redundant, since we have separate tests for validators. It is, however, an excellent example to learn from.\n", "\n", "First we update the `.yaml` file to contain a property matching the parameter." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "```\n", "spec: \"1.0\"\n", "devices:\n", " device 1:\n", " eom:\n", " GPIB INSTR:\n", " q: \"\\r\" # MAKE SURE! that this matches the terminator of the driver!\n", " r: \"\\r\"\n", " error: ERROR\n", " dialogues:\n", " - q: \"*IDN?\"\n", " r: \"QCoDeS, Weinschel 8320 (Simulated), 1337, 0.0.01\"\n", "\n", " properties:\n", "\n", " attenuation:\n", " default: 0\n", " getter:\n", " q: \"ATTN? 1\" # the set/get commands have to simply be copied over from the driver\n", " r: \"{:02.0f}\"\n", " setter:\n", " q: \"ATTN ALL {:02.0f}\"\n", "\n", "resources:\n", " GPIB::1::INSTR:\n", " device: device 1\n", "\n", "```" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Notice that we don't include the the\n", "```r: OK```\n", "as the response of setting a property. This is in contrast to what https://pyvisa-sim.readthedocs.io/en/latest/definitions.html#properties does. The response of a successful setting of a parameter will not return 'OK'.\n", "\n", "Again we have cheated and already added that file to the above location." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Next we update the test script." ] }, { "cell_type": "code", "execution_count": 4, "metadata": {}, "outputs": [], "source": [ "import pytest\n", "\n", "from qcodes.instrument_drivers.weinschel import Weinschel8320\n", "\n", "\n", "# The following decorator makes the driver\n", "# available to all the functions in this module\n", "@pytest.fixture(scope='function', name=\"weinschel_driver_2\")\n", "def _weinschel_driver():\n", " wein_sim = Weinschel8320('wein_sim',\n", " address='GPIB::1::INSTR',\n", " pyvisa_sim_file=\"Weinschel_8320.yaml\"\n", " )\n", " yield wein_sim\n", "\n", " wein_sim.close()\n", "\n", "\n", "def test_init_v2(driver):\n", " \"\"\"\n", " Test that simple initialisation works\n", " \"\"\"\n", "\n", " # There is not that much to do, really.\n", " # We can check that the IDN string reads back correctly\n", "\n", " idn_dict = driver.IDN()\n", "\n", " assert idn_dict['vendor'] == 'QCoDeS'\n", "\n", "\n", "def test_attenuation_validation(weinschel_driver_2):\n", " \"\"\"\n", " Test that incorrect values are rejected\n", " \"\"\"\n", "\n", " bad_values = [-1, 1, 1.5]\n", "\n", " for bv in bad_values:\n", " with pytest.raises(ValueError):\n", " weinschel_driver_2.attenuation(bv)\n" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Open a command line/console/terminal, navigate to the `tests/drivers/` folder and run\n", "```\n", ">> pytest test_weinschel_8320.py\n", "```\n", "\n", "This should give you an output similar to\n", "```\n", "========================================= 2 passed in 0.73 seconds ==========================================\n", "```" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## That's it!" ] } ], "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.10.6" }, "nbsphinx": { "execute": "never" }, "widgets": { "application/vnd.jupyter.widget-state+json": { "state": {}, "version_major": 2, "version_minor": 0 } } }, "nbformat": 4, "nbformat_minor": 4 }