This page was generated from docs/examples/writing_drivers/Creating-Simulated-PyVISA-Instruments.ipynb. Interactive online version: .
Creating Simulated PyVISA Instruments¶
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.
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.
What?¶
This way, we may instantiate drivers and run simple tests on them. Tests like:
Can the driver even instantiate? This is very relevant when underlying APIs change.
Is the drivers (e.g.) “voltage-to-bytecode” converter working properly?
Not!¶
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:
Do we wait sufficiently long for this oscilloscope’s trace to be acquired?
Does our driver handle overlapping commands of this AWG correctly?
How?¶
The basic scheme goes as follows:
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#definitionsThen write a test for your instrument and put it in
tests/drivers
. The file should have the nametest_<nameofyourdriver>.py
.Check that all is well by running
$ pytest test_<nameofyourdriver>.py
.
Below is an example.
Example: Weinschel8320¶
The Weinschel 8320 is a very simple driver.
[1]:
from typing import TYPE_CHECKING
import numpy as np
import qcodes.validators as vals
from qcodes.instrument.visa import VisaInstrument, VisaInstrumentKWArgs
if TYPE_CHECKING:
from typing_extensions import Unpack
class Weinschel8320(VisaInstrument):
"""
QCoDeS driver for the stepped attenuator
Weinschel is formerly known as Aeroflex/Weinschel
"""
default_terminator = "\r"
def __init__(
self, name: str, address: str, **kwargs: "Unpack[VisaInstrumentKWArgs]"
):
super().__init__(name, address, **kwargs)
self.attenuation = self.add_parameter(
"attenuation",
unit="dB",
set_cmd="ATTN ALL {:02.0f}",
get_cmd="ATTN? 1",
vals=vals.Enum(*np.arange(0, 60.1, 2).tolist()),
get_parser=float,
)
"""Parameter attenuation"""
self.connect_message()
Logging hadn't been started.
Activating auto-logging. Current session state plus future input saved.
Filename : C:\Users\jenielse\.qcodes\logs\command_history.log
Mode : append
Output logging : True
Raw input log : False
Timestamping : True
State : active
Qcodes Logfile : C:\Users\jenielse\.qcodes\logs\221108-14924-qcodes.log
The .yaml
file¶
The simplest .yaml
file that is still useful, reads, in all its glory:
spec: "1.0"
devices:
device 1:
eom:
GPIB INSTR:
q: "\r" # MAKE SURE! that this matches the terminator of the driver!
r: "\r"
error: ERROR
dialogues:
- q: "*IDN?"
r: "QCoDeS, Weinschel 8320 (Simulated), 1337, 0.0.01"
resources:
GPIB::1::INSTR:
device: device 1
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. This simulates an instrument with no settable parameter; only an *IDN?
response. This is enough to instantiate the instrument.
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.
Then we may connect to the simulated instrument.
There are two different ways we may tell pyvisa-sim which yaml file to make use of. We can either follow the official instructions and pass visalib as a string of the form “pathtoyamlfile@sim” where sim
indicates that we make use of the simulated backend. This is however not very convenient when the yaml file is part of a package since we would need to know the absolute path. It is therefor also possible to use the argument pyvisa_sim_file. This can be given in two forms either just the name
of the file such as Weinschel_8320.yaml
in which case we search for the file in the qcodes.instrument.sims
folder. Or alternatively in the format mymodule.mysubmodule:name_of_file
e.g. qcodes.instrument.sims:Weinschel_8320.yaml
in which case we search for the file in the supplied module.
[2]:
wein_sim = Weinschel8320(
"wein_sim",
address="GPIB::1::INSTR", # This matches the address in the .yaml file
pyvisa_sim_file="Weinschel_8320.yaml",
)
Connected to: QCoDeS Weinschel 8320 (Simulated) (serial:1337, firmware:0.0.01) in 0.07s
The test¶
Now we can write a useful test!
[3]:
import pytest
from qcodes.instrument_drivers.weinschel import Weinschel8320
# The following decorator makes the driver
# available to all the functions in this module
@pytest.fixture(scope="function", name="weinschel_driver_1")
def _weinschel_driver_1():
wein_sim = Weinschel8320(
"wein_sim",
address="GPIB::1::65535::INSTR",
pyvisa_sim_file="Weinschel_8320.yaml",
)
yield wein_sim
wein_sim.close()
def test_init_v1(weinschel_driver_1):
"""
Test that simple initialisation works
"""
# There is not that much to do, really.
# We can check that the IDN string reads back correctly
idn_dict = weinschel_driver_1.IDN()
assert idn_dict["vendor"] == "QCoDeS"
Save the test as tests/drivers/test_weinschel_8320.py
.
Open a command line/console/terminal, navigate to the tests/drivers/
folder and run
>> pytest test_weinschel_8320.py
This should give you an output similar to
========================================= 1 passed in 0.73 seconds ==========================================
Congratulations! That was it.¶
Bonus example: including parameters in the simulated instrument¶
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.
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.
First we update the .yaml
file to contain a property matching the parameter.
spec: "1.0"
devices:
device 1:
eom:
GPIB INSTR:
q: "\r" # MAKE SURE! that this matches the terminator of the driver!
r: "\r"
error: ERROR
dialogues:
- q: "*IDN?"
r: "QCoDeS, Weinschel 8320 (Simulated), 1337, 0.0.01"
properties:
attenuation:
default: 0
getter:
q: "ATTN? 1" # the set/get commands have to simply be copied over from the driver
r: "{:02.0f}"
setter:
q: "ATTN ALL {:02.0f}"
resources:
GPIB::1::INSTR:
device: device 1
Notice that we don’t include the the r: OK
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’.
Again we have cheated and already added that file to the above location.
Next we update the test script.
[4]:
import pytest
from qcodes.instrument_drivers.weinschel import Weinschel8320
# The following decorator makes the driver
# available to all the functions in this module
@pytest.fixture(scope="function", name="weinschel_driver_2")
def _weinschel_driver():
wein_sim = Weinschel8320(
"wein_sim", address="GPIB::1::INSTR", pyvisa_sim_file="Weinschel_8320.yaml"
)
yield wein_sim
wein_sim.close()
def test_init_v2(driver):
"""
Test that simple initialisation works
"""
# There is not that much to do, really.
# We can check that the IDN string reads back correctly
idn_dict = driver.IDN()
assert idn_dict["vendor"] == "QCoDeS"
def test_attenuation_validation(weinschel_driver_2):
"""
Test that incorrect values are rejected
"""
bad_values = [-1, 1, 1.5]
for bv in bad_values:
with pytest.raises(ValueError):
weinschel_driver_2.attenuation(bv)
Open a command line/console/terminal, navigate to the tests/drivers/
folder and run
>> pytest test_weinschel_8320.py
This should give you an output similar to
========================================= 2 passed in 0.73 seconds ==========================================