diff --git a/docs/examples/QDevil/QDAC2/Chaining.ipynb b/docs/examples/QDevil/QDAC2/Chaining.ipynb index 9b63d919f..848b4fa10 100644 --- a/docs/examples/QDevil/QDAC2/Chaining.ipynb +++ b/docs/examples/QDevil/QDAC2/Chaining.ipynb @@ -318,7 +318,7 @@ ], "metadata": { "kernelspec": { - "display_name": "qcodespip311", + "display_name": "Python 3 (ipykernel)", "language": "python", "name": "python3" }, @@ -332,7 +332,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.11.0 | packaged by conda-forge | (main, Jan 16 2023, 14:12:30) [MSC v.1916 64 bit (AMD64)]" + "version": "3.10.12" }, "nbsphinx": { "execute": "never" diff --git a/docs/examples/QDevil/QSwitch/Debugging.ipynb b/docs/examples/QDevil/QSwitch/Debugging.ipynb new file mode 100644 index 000000000..51be13b19 --- /dev/null +++ b/docs/examples/QDevil/QSwitch/Debugging.ipynb @@ -0,0 +1,160 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "c08d6367", + "metadata": {}, + "source": [ + "# QSwitch debugging\n" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "id": "85f2a1e0", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Connected to: Quantum Machines QSwitch (serial:5, firmware:0.160) in 0.11s\n" + ] + } + ], + "source": [ + "import pprint\n", + "pp = pprint.PrettyPrinter()\n", + "from qcodes_contrib_drivers.drivers.QDevil import QSwitch\n", + "qswitch_addr = '192.168.8.18'\n", + "qswitch = QSwitch.QSwitch('switch', visalib='@py', address=f'TCPIP::{qswitch_addr}::5025::SOCKET')" + ] + }, + { + "cell_type": "markdown", + "id": "5c6c00b2", + "metadata": {}, + "source": [ + " You can record the underlying SCPI commands that are sent to the QSwitch:" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "id": "d0c7fc28", + "metadata": {}, + "outputs": [], + "source": [ + "qswitch.start_recording_scpi()" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "id": "64f0cbe6", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "'0,\"No error\"'" + ] + }, + "execution_count": 4, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "qswitch.reset()\n", + "qswitch.arrange(\n", + " breakouts={'DMM': 5, 'VNA': 7},\n", + " lines={'plunger': 23, 'sensor': 5})\n", + "qswitch.connect('sensor')\n", + "qswitch.breakout('sensor', 'DMM')\n", + "qswitch.errors()" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "id": "0d8a7693", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "['*rst', 'stat?', 'clos (@5!9)', 'open (@5!0)', 'clos (@5!5)', 'all?']\n" + ] + } + ], + "source": [ + "pp.pprint(qswitch.get_recorded_scpi_commands())" + ] + }, + { + "cell_type": "markdown", + "id": "4a00d73e-88e0-439c-9b80-15aaffaf2064", + "metadata": {}, + "source": [ + "If you are connecting via USB, then you need `pyserial` installed." + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "id": "37ac76f2", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "('ASRL/dev/ttyS0::INSTR', 'ASRL/dev/ttyACM0::INSTR')" + ] + }, + "execution_count": 6, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "import pyvisa\n", + "rm = pyvisa.ResourceManager('@py')\n", + "rm.list_resources()" + ] + }, + { + "cell_type": "markdown", + "id": "d6bdb22e", + "metadata": {}, + "source": [ + "See https://github.com/QDevil/qdac2-tools for more debugging tools." + ] + } + ], + "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.12" + }, + "nbsphinx": { + "execute": "never" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/docs/examples/QDevil/QSwitch/Usage.ipynb b/docs/examples/QDevil/QSwitch/Usage.ipynb new file mode 100644 index 000000000..89b7e189b --- /dev/null +++ b/docs/examples/QDevil/QSwitch/Usage.ipynb @@ -0,0 +1,495 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "5e62a6a0-a4e4-4928-a276-d761d509c2a4", + "metadata": {}, + "source": [ + "# QSwitch usage" + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "id": "4bea1adc-81e2-4388-a9e0-bf8ebc3e4f83", + "metadata": {}, + "outputs": [], + "source": [ + "import pprint\n", + "pp = pprint.PrettyPrinter()\n", + "import numpy as np\n", + "import qcodes\n", + "from qcodes_contrib_drivers.drivers.QDevil import QSwitch" + ] + }, + { + "cell_type": "markdown", + "id": "e2b87dde-0057-4b87-b957-ee44166592cd", + "metadata": {}, + "source": [ + "Connect via USB:" + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "id": "5787c333-0d69-47db-bfe3-37bead44a7c5", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Connected to: Quantum Machines QSwitch (serial:4, firmware:0.160) in 0.07s\n" + ] + } + ], + "source": [ + "qswitch_addr = '/dev/cu.usbmodem14101'\n", + "qswitch = QSwitch.QSwitch('switch', visalib='@py', address=f'ASRL{qswitch_addr}::INSTR')" + ] + }, + { + "cell_type": "markdown", + "id": "89bf3106-0ffa-4a05-9632-5a7aba24c51e", + "metadata": {}, + "source": [ + "Or connect via Ethernet:" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "id": "a4724b86-62f4-4749-8398-06c32f42f53a", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Connected to: Quantum Machines QSwitch (serial:5, firmware:0.160) in 0.13s\n" + ] + } + ], + "source": [ + "qswitch_addr = '192.168.8.16'\n", + "qswitch = QSwitch.QSwitch('switch', visalib='@py', address=f'TCPIP::{qswitch_addr}::5025::SOCKET')" + ] + }, + { + "cell_type": "markdown", + "id": "1866cc7c-eb2d-463f-b16e-ce20125437ac", + "metadata": {}, + "source": [ + "When the QSwitch is reset, all lines are grounded:" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "id": "a1eb56b0-20cc-4dbd-a603-5c1d3069bcb7", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "'(@1!0:24!0)'" + ] + }, + "execution_count": 3, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "qswitch.reset()\n", + "qswitch.state()" + ] + }, + { + "cell_type": "markdown", + "id": "2fc0260b-8052-457a-b820-15edd7a699d3", + "metadata": {}, + "source": [ + "Or expressed as a Python array:" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "id": "4519574b-8427-4412-a156-bc64169e7f22", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "[(1, 0),\n", + " (2, 0),\n", + " (3, 0),\n", + " (4, 0),\n", + " (5, 0),\n", + " (6, 0),\n", + " (7, 0),\n", + " (8, 0),\n", + " (9, 0),\n", + " (10, 0),\n", + " (11, 0),\n", + " (12, 0),\n", + " (13, 0),\n", + " (14, 0),\n", + " (15, 0),\n", + " (16, 0),\n", + " (17, 0),\n", + " (18, 0),\n", + " (19, 0),\n", + " (20, 0),\n", + " (21, 0),\n", + " (22, 0),\n", + " (23, 0),\n", + " (24, 0)]" + ] + }, + "execution_count": 5, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "qswitch.closed_relays()" + ] + }, + { + "cell_type": "markdown", + "id": "733345b3-f9e4-4edc-a352-7615360a7299", + "metadata": {}, + "source": [ + "Beep and blink on SCPI command errors:" + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "id": "d0e47647-f672-40e8-a9d3-86ac1a5b90c3", + "metadata": {}, + "outputs": [], + "source": [ + "qswitch.error_indicator('on')" + ] + }, + { + "cell_type": "markdown", + "id": "8c9ee245", + "metadata": {}, + "source": [ + "## Manipulation by numbers" + ] + }, + { + "cell_type": "markdown", + "id": "630d55d3", + "metadata": {}, + "source": [ + "Connect and unground line 23:" + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "id": "e2376ee8", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "'(@1!0:22!0,24!0,23!9)'" + ] + }, + "execution_count": 7, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "qswitch.close_relay(23, 9)\n", + "qswitch.open_relay(23, 0)\n", + "qswitch.state()" + ] + }, + { + "cell_type": "markdown", + "id": "ff299d44", + "metadata": {}, + "source": [ + "Tap off line 23 to BNC 2:" + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "id": "c3998fd0", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "'(@1!0:22!0,24!0,23!9,23!2)'" + ] + }, + "execution_count": 8, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "qswitch.close_relay(23, 2)\n", + "qswitch.state()" + ] + }, + { + "cell_type": "markdown", + "id": "08690b84", + "metadata": {}, + "source": [ + "Multiple relays at once:" + ] + }, + { + "cell_type": "code", + "execution_count": 9, + "id": "c2909bd6", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "'(@1!0:21!0,23!9,23!2)'" + ] + }, + "execution_count": 9, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "qswitch.open_relays([(22, 0), (24, 0)])\n", + "qswitch.state()" + ] + }, + { + "cell_type": "markdown", + "id": "6fea1be8", + "metadata": {}, + "source": [ + "## Arrangements" + ] + }, + { + "cell_type": "markdown", + "id": "f3ac66ed", + "metadata": {}, + "source": [ + "At a higher level of abstraction, lines and break-out BNCs can be given names." + ] + }, + { + "cell_type": "code", + "execution_count": 14, + "id": "9d3d65c5", + "metadata": {}, + "outputs": [], + "source": [ + "qswitch.arrange(\n", + " breakouts={'DMM': 2, 'VNA': 7}, # BNC 2 connected to DMM & BNC 7 to VNA\n", + " lines={'plunger': 23, 'sensor': 5}) # Give names to lines 23 & 5" + ] + }, + { + "cell_type": "markdown", + "id": "add499e2", + "metadata": {}, + "source": [ + "There are specialised functions for manipulating relays by name:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "3f7f92ca", + "metadata": {}, + "outputs": [], + "source": [ + "qswitch.connect('plunger')" + ] + }, + { + "cell_type": "markdown", + "id": "8e57837f", + "metadata": {}, + "source": [ + "which is equivalent to" + ] + }, + { + "cell_type": "code", + "execution_count": 13, + "id": "45ae7eea", + "metadata": {}, + "outputs": [], + "source": [ + "qswitch.close_relay(23, 9)\n", + "qswitch.open_relay(23, 0)" + ] + }, + { + "cell_type": "markdown", + "id": "6550e6ea", + "metadata": {}, + "source": [ + "that is, connect the device-under-test to input line 23, and then unground it." + ] + }, + { + "cell_type": "markdown", + "id": "4af6640a", + "metadata": {}, + "source": [ + "For the BNC breakout:" + ] + }, + { + "cell_type": "code", + "execution_count": 15, + "id": "ac31add0", + "metadata": {}, + "outputs": [], + "source": [ + "qswitch.breakout('sensor', 'DMM')" + ] + }, + { + "cell_type": "markdown", + "id": "be900d2c", + "metadata": {}, + "source": [ + "which is equivalent to" + ] + }, + { + "cell_type": "code", + "execution_count": 13, + "id": "86cfd2da", + "metadata": {}, + "outputs": [], + "source": [ + "qswitch.close_relay(5, 2)\n", + "qswitch.open_relay(5, 0)" + ] + }, + { + "cell_type": "markdown", + "id": "8a1d8733", + "metadata": {}, + "source": [ + "that is, connect the device-under-test line 5 to BNC 2, and then unground it." + ] + }, + { + "cell_type": "markdown", + "id": "8b788de6", + "metadata": {}, + "source": [ + "For convenience, the default name of a line or BNC is just its number, so eg." + ] + }, + { + "cell_type": "code", + "execution_count": 12, + "id": "e0ad0e89", + "metadata": {}, + "outputs": [], + "source": [ + "qswitch.breakout('24', '1')" + ] + }, + { + "cell_type": "markdown", + "id": "ded54735", + "metadata": {}, + "source": [ + "is equivalent to" + ] + }, + { + "cell_type": "code", + "execution_count": 16, + "id": "291c4da7", + "metadata": {}, + "outputs": [], + "source": [ + "qswitch.close_relay(24, 1)\n", + "qswitch.open_relay(24, 0)" + ] + }, + { + "cell_type": "markdown", + "id": "e36b17b1", + "metadata": {}, + "source": [ + "Moreover, any breakouts are also disconnected when grounding:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "245cd62c", + "metadata": {}, + "outputs": [], + "source": [ + "qswitch.ground('sensor')" + ] + }, + { + "cell_type": "markdown", + "id": "db813c1f", + "metadata": {}, + "source": [ + "will be equivalent to" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "54374fb6", + "metadata": {}, + "outputs": [], + "source": [ + "qswitch.close_relay(5, 0)\n", + "qswitch.open_relays([(5, 2), (5, 9)])" + ] + } + ], + "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.12" + }, + "nbsphinx": { + "execute": "never" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/docs/examples/QDevil/QSwitch/index.rst b/docs/examples/QDevil/QSwitch/index.rst new file mode 100644 index 000000000..841974e4a --- /dev/null +++ b/docs/examples/QDevil/QSwitch/index.rst @@ -0,0 +1,7 @@ +QCoDeS examples of how to use QSwitch +===================================== + +.. toctree:: + :glob: + + * \ No newline at end of file diff --git a/docs/examples/QDevil/index.rst b/docs/examples/QDevil/index.rst index 4b37c7023..bcdb684d2 100644 --- a/docs/examples/QDevil/index.rst +++ b/docs/examples/QDevil/index.rst @@ -5,3 +5,4 @@ QDevil drivers QDAC1/index QDAC2/index + QSwitch/index diff --git a/qcodes_contrib_drivers/drivers/QDevil/QSwitch.py b/qcodes_contrib_drivers/drivers/QDevil/QSwitch.py new file mode 100644 index 000000000..45b66d870 --- /dev/null +++ b/qcodes_contrib_drivers/drivers/QDevil/QSwitch.py @@ -0,0 +1,449 @@ +import re +import itertools +from time import sleep as sleep_s +from qcodes.instrument.parameter import DelegateParameter +from qcodes.instrument.visa import VisaInstrument +from qcodes.utils import validators +from pyvisa.errors import VisaIOError +from typing import ( + Tuple, Sequence, List, Dict, Set, Union, Optional) +from packaging.version import parse + +# Version 0.4.0 + +State = Sequence[Tuple[int, int]] + + +def _line_tap_split(input: str) -> Tuple[int, int]: + pair = input.split('!') + if len(pair) != 2: + raise ValueError(f'Expected channel pair, got {input}') + if not pair[0].isdecimal(): + raise ValueError(f'Expected channel, got {pair[0]}') + if not pair[1].isdecimal(): + raise ValueError(f'Expected channel, got {pair[1]}') + return int(pair[0]), int(pair[1]) + + +def channel_list_to_state(channel_list: str) -> State: + outer = re.match(r'\(@([0-9,:! ]*)\)', channel_list) + if not outer: + raise ValueError(f'Expected channel list, got {channel_list}') + result: List[Tuple[int, int]] = [] + sequences = outer[1].split(',') + if sequences == ['']: + return result + for sequence in sequences: + limits = sequence.split(':') + if limits == ['']: + raise ValueError(f'Expected channel sequence, got {limits}') + line_start, tap_start = _line_tap_split(limits[0]) + line_stop, tap_stop = line_start, tap_start + if len(limits) == 2: + line_stop, tap_stop = _line_tap_split(limits[1]) + if len(limits) > 2: + raise ValueError(f'Expected channel sequence, got {limits}') + if tap_start != tap_stop: + raise ValueError( + f'Expected same breakout in sequence, got {limits}') + for line in range(line_start, line_stop+1): + result.append((line, tap_start)) + return result + + +def state_to_expanded_list(state: State) -> str: + return \ + '(@' + \ + ','.join([f'{line}!{tap}' for (line, tap) in state]) + \ + ')' + + +def state_to_compressed_list(state: State) -> str: + tap_to_line: Dict[int, Set[int]] = dict() + for line, tap in state: + tap_to_line.setdefault(tap, set()).add(line) + taps = list(tap_to_line.keys()) + taps.sort() + intervals = [] + for tap in taps: + start_line = None + end_line = None + lines = list(tap_to_line[tap]) + lines.sort() + for line in lines: + if not start_line: + start_line = line + end_line = line + continue + if line == end_line + 1: + end_line = line + continue + if start_line == end_line: + intervals.append(f'{start_line}!{tap}') + else: + intervals.append(f'{start_line}!{tap}:{end_line}!{tap}') + start_line = line + end_line = line + if start_line == end_line: + intervals.append(f'{start_line}!{tap}') + else: + intervals.append(f'{start_line}!{tap}:{end_line}!{tap}') + return '(@' + ','.join(intervals) + ')' + + +def expand_channel_list(channel_list: str) -> str: + return state_to_expanded_list(channel_list_to_state(channel_list)) + + +def compress_channel_list(channel_list: str) -> str: + return state_to_compressed_list(channel_list_to_state(channel_list)) + + +relay_lines = 24 +relays_per_line = 9 + + +def _state_diff(before: State, after: State) -> Tuple[State, State, State]: + initial = frozenset(before) + target = frozenset(after) + return list(target - initial), list(initial - target), list(target) + + +class QSwitch(VisaInstrument): + + def __init__(self, name: str, address: str, **kwargs) -> None: + """Connect to a QSwitch + + Args: + name (str): Name for instrument + address (str): Visa identification string + **kwargs: additional argument to the Visa driver + """ + self._check_instrument_name(name) + super().__init__(name, address, terminator='\n', **kwargs) + self._set_up_serial() + self._set_up_debug_settings() + self._set_up_simple_functions() + self.connect_message() + self._check_for_wrong_model() + self._check_for_incompatiable_firmware() + self._set_default_names() + self.state_force_update() + self.add_parameter( + name='state', + label='relays', + set_cmd=self._set_state, + get_cmd=self._get_state, + ) + self.add_parameter( + name='closed_relays', + source=self.state, + set_parser=state_to_compressed_list, + get_parser=channel_list_to_state, + parameter_class=DelegateParameter, + snapshot_value=False, + ) + self.add_parameter( + name='auto_save', + set_cmd='aut {0}'.format('{}'), + get_cmd='aut?', + get_parser=str, + vals=validators.Enum('on', 'off'), + snapshot_value=False, + ) + self.add_parameter( + name='error_indicator', + set_cmd='beep:stat {0}'.format('{}'), + get_cmd='beep:stat?', + get_parser=str, + vals=validators.Enum('on', 'off'), + snapshot_value=False, + ) + self._add_monitor_pseudo_parameters() + + # ----------------------------------------------------------------------- + # Instrument-wide functions + # ----------------------------------------------------------------------- + + def reset(self) -> None: + self._write('*rst') + sleep_s(0.6) + self.state_force_update() + + def errors(self) -> str: + """Retrieve and clear all previous errors + + Returns: + str: Comma separated list of errors or '0, "No error"' + """ + return self.ask('all?') + + def error(self) -> str: + """Retrieve next error + + Returns: + str: The next error or '0, "No error"' + """ + return self.ask('next?') + + def state_force_update(self) -> None: + self._set_state_raw(self.ask('stat?')) + + # ----------------------------------------------------------------------- + # Direct manipulation of the relays + # ----------------------------------------------------------------------- + + def close_relays(self, relays: State) -> None: + currently = channel_list_to_state(self._state) + union = list(itertools.chain(currently, relays)) + self._effectuate(union) + + def close_relay(self, line: int, tap: int) -> None: + self.close_relays([(line, tap)]) + + def open_relays(self, relays: State) -> None: + currently = frozenset(channel_list_to_state(self._state)) + subtraction = frozenset(relays) + self._effectuate(list(currently - subtraction)) + + def open_relay(self, line: int, tap: int) -> None: + self.open_relays([(line, tap)]) + + # ----------------------------------------------------------------------- + # Manipulation by name + # ----------------------------------------------------------------------- + + OneOrMore = Union[str, Sequence[str]] + + def ground(self, lines: OneOrMore) -> None: + connections: List[Tuple[int, int]] = [] + if isinstance(lines, str): + line = self._to_line(lines) + self.close_relay(line, 0) + taps = range(1, relays_per_line + 1) + connections = list(itertools.zip_longest([], taps, fillvalue=line)) + self.open_relays(connections) + else: + numbers = map(self._to_line, lines) + grounds = list(itertools.zip_longest(numbers, [], fillvalue=0)) + self.close_relays(grounds) + for tap in range(1, relays_per_line + 1): + connections += itertools.zip_longest( + map(self._to_line, lines), [], fillvalue=tap) + self.open_relays(connections) + + def connect(self, lines: OneOrMore) -> None: + if isinstance(lines, str): + self.close_relay(self._to_line(lines), 9) + self.open_relay(self._to_line(lines), 0) + else: + numbers = map(self._to_line, lines) + pairs = list(itertools.zip_longest(numbers, [], fillvalue=9)) + self.close_relays(pairs) + numbers = map(self._to_line, lines) + connections = list(itertools.zip_longest(numbers, [], fillvalue=0)) + self.open_relays(connections) + + def breakout(self, line: str, tap: str) -> None: + self.close_relay(self._to_line(line), self._to_tap(tap)) + self.open_relay(self._to_line(line), 0) + + def arrange(self, breakouts: Optional[Dict[str, int]] = None, + lines: Optional[Dict[str, int]] = None) -> None: + """An arrangement of names for lines and breakouts + + Args: + breakouts (Dict[str, int]): Name/breakout pairs + lines (Dict[str, int]): Name/line pairs + """ + if lines: + for name, line in lines.items(): + self._line_names[name] = line + if breakouts: + for name, tap in breakouts.items(): + self._tap_names[name] = tap + + # ----------------------------------------------------------------------- + # Debugging and testing + + def start_recording_scpi(self) -> None: + """Record all SCPI commands sent to the instrument + + Any previous recordings are removed. To inspect the SCPI commands sent + to the instrument, call get_recorded_scpi_commands(). + """ + self._scpi_sent: List[str] = list() + self._record_commands = True + + def get_recorded_scpi_commands(self) -> List[str]: + """ + Returns: + Sequence[str]: SCPI commands sent to the instrument + """ + commands = self._scpi_sent + self._scpi_sent = list() + return commands + + def clear_read_queue(self) -> Sequence[str]: + """Flush the VISA message queue of the instrument + + Takes at least _message_flush_timeout_ms to carry out. + + Returns: + Sequence[str]: Messages lingering in queue + """ + lingering = list() + original_timeout = self.visa_handle.timeout + self.visa_handle.timeout = self._message_flush_timeout_ms + while True: + try: + message = self.visa_handle.read() + except VisaIOError: + break + else: + lingering.append(message) + self.visa_handle.timeout = original_timeout + return lingering + + # ----------------------------------------------------------------------- + # Override communication methods to make it possible to check for errors + # and to record the communication with the instrument. + + def write(self, cmd: str) -> None: + """Send SCPI command to instrument + + Args: + cmd (str): SCPI command + """ + try: + self._write(cmd) + sleep_s(0.075) + errors = super().ask('all?') + except Exception as error: + raise ValueError(f'Error: {repr(error)} after executing {cmd}') + if errors == '0,"No error"': + return + raise ValueError(f'Error: {errors} after executing {cmd}') + + def ask(self, cmd: str) -> str: + """Send SCPI query to instrument + + Args: + cmd (str): SCPI query + + Returns: + str: SCPI answer + """ + if self._record_commands: + self._scpi_sent.append(cmd) + answer = super().ask(cmd) + return answer + + # ----------------------------------------------------------------------- + + def _write(self, cmd: str) -> None: + if self._record_commands: + self._scpi_sent.append(cmd) + super().write(cmd) + + def _channel_list_to_overview(self, channel_list: str) -> dict[str, List[str]]: + state = channel_list_to_state(channel_list) + line_names: dict[int, str] = dict() + for name, line in self._line_names.items(): + line_names[line] = name + tap_names: dict[int, str] = dict() + for name, tap in self._tap_names.items(): + tap_names[tap] = name + result: dict[str, List[str]] = dict() + for line, _ in state: + line_name = line_names[line] + result[line_name] = list() + for line, tap in state: + line_name = line_names[line] + if tap == 0: + result[line_name].append('grounded') + elif tap == 9: + result[line_name].append('connected') + else: + tap_name = f'breakout {tap_names[tap]}' + result[line_name].append(tap_name) + return result + + def _to_line(self, name: str) -> int: + try: + return self._line_names[name] + except KeyError: + raise ValueError(f'Unknown line "{name}"') + + def _to_tap(self, name: str) -> int: + try: + return self._tap_names[name] + except KeyError: + raise ValueError(f'Unknown tap "{name}"') + + def _get_state(self) -> str: + self.state_force_update() + return self._state + + def _set_state_raw(self, channel_list: str) -> None: + self._state = channel_list + + def _set_state(self, channel_list: str) -> None: + self._effectuate(channel_list_to_state(channel_list)) + + def _effectuate(self, state: State) -> None: + currently = channel_list_to_state(self._state) + positive, negative, total = _state_diff(currently, state) + if positive: + self.write(f'clos {state_to_compressed_list(positive)}') + if negative: + self.write(f'open {state_to_compressed_list(negative)}') + self._set_state_raw(state_to_compressed_list(total)) + + def _set_up_debug_settings(self) -> None: + self._record_commands = False + self._scpi_sent = list() + self._message_flush_timeout_ms = 1 + self._round_off = None + + def _set_up_serial(self) -> None: + # No harm in setting the speed even if the connection is not serial. + self.visa_handle.baud_rate = 9600 # type: ignore + + def _check_for_wrong_model(self) -> None: + model = self.IDN()['model'] + if model != 'QSwitch': + raise ValueError(f'Unknown model {model}. Are you using the right' + ' driver for your instrument?') + + def _check_for_incompatiable_firmware(self) -> None: + firmware = self.IDN()['firmware'] + least_compatible_fw = '0.155' + if parse(firmware) < parse(least_compatible_fw): + raise ValueError(f'Incompatible firmware {firmware}. You need at ' + f'least {least_compatible_fw}') + + def _set_up_simple_functions(self) -> None: + self.add_function('abort', call_cmd='abor') + + def _set_default_names(self) -> None: + lines = range(1, relay_lines+1) + taps = range(1, relays_per_line) + self._line_names = dict(zip(map(str, lines), lines)) + self._tap_names = dict(zip(map(str, taps), taps)) + + def _add_monitor_pseudo_parameters(self) -> None: + self.add_parameter( + name='overview', + source=self.state, + get_parser=self._channel_list_to_overview, + parameter_class=DelegateParameter, + snapshot_value=False, + ) + + def _check_instrument_name(self, name: str) -> None: + if name.isidentifier(): + return + raise ValueError( + f'Instrument name "{name}" is incompatible with QCoDeS parameter ' + 'generation (no spaces, punctuation, prepended numbers, etc)') diff --git a/qcodes_contrib_drivers/sims/QDAC2.yaml b/qcodes_contrib_drivers/sims/QDAC2.yaml index f703a2735..5e5f79122 100644 --- a/qcodes_contrib_drivers/sims/QDAC2.yaml +++ b/qcodes_contrib_drivers/sims/QDAC2.yaml @@ -78,7 +78,7 @@ devices: setter: q: "outp:trig{ch_id}:sour {}" specs: - valid: ["bus", "hold", "int1", "int2", "int3", "int4", "int5", "int6", "int7", "int8", "int9", "int10", "int11", "int12", "int13", "int14", int15", "int16", "ext1", "ext2", "ext3", "ext4"] + valid: ["bus", "hold", "int1", "int2", "int3", "int4", "int5", "int6", "int7", "int8", "int9", "int10", "int11", "int12", "int13", "int14", "int15", "int16", "ext1", "ext2", "ext3", "ext4"] type: str trigger_width: default: 10e-6 @@ -238,7 +238,7 @@ devices: q: "sour{ch_id}:dc:trig:sour?" r: "{}" specs: - valid: ["bus", "hold", "int1", "int2", "int3", "int4", "int5", "int6", "int7", "int8", "int9", "int10", "int11", "int12", "int13", "int14", int15", "int16", "ext1", "ext2", "ext3", "ext4", "ext5", "imm"] + valid: ["bus", "hold", "int1", "int2", "int3", "int4", "int5", "int6", "int7", "int8", "int9", "int10", "int11", "int12", "int13", "int14", "int15", "int16", "ext1", "ext2", "ext3", "ext4", "ext5", "imm"] type: str voltage: default: 0.0 @@ -255,7 +255,7 @@ devices: q: "sour{ch_id}:squ:trig:sour?" r: "{}" specs: - valid: ["bus", "hold", "int1", "int2", "int3", "int4", "int5", "int6", "int7", "int8", "int9", "int10", "int11", "int12", "int13", "int14", int15", "int16", "ext1", "ext2", "ext3", "ext4", "ext5", "imm"] + valid: ["bus", "hold", "int1", "int2", "int3", "int4", "int5", "int6", "int7", "int8", "int9", "int10", "int11", "int12", "int13", "int14", "int15", "int16", "ext1", "ext2", "ext3", "ext4", "ext5", "imm"] type: str sine_trigger_source: default: "imm" @@ -265,7 +265,7 @@ devices: q: "sour{ch_id}:sine:trig:sour?" r: "{}" specs: - valid: ["bus", "hold", "int1", "int2", "int3", "int4", "int5", "int6", "int7", "int8", "int9", "int10", "int11", "int12", "int13", "int14", int15", "int16", "ext1", "ext2", "ext3", "ext4", "ext5", "imm"] + valid: ["bus", "hold", "int1", "int2", "int3", "int4", "int5", "int6", "int7", "int8", "int9", "int10", "int11", "int12", "int13", "int14", "int15", "int16", "ext1", "ext2", "ext3", "ext4", "ext5", "imm"] type: str triangle_trigger_source: default: "imm" @@ -275,7 +275,7 @@ devices: q: "sour{ch_id}:tri:trig:sour?" r: "{}" specs: - valid: ["bus", "hold", "int1", "int2", "int3", "int4", "int5", "int6", "int7", "int8", "int9", "int10", "int11", "int12", "int13", "int14", int15", "int16", "ext1", "ext2", "ext3", "ext4", "ext5", "imm"] + valid: ["bus", "hold", "int1", "int2", "int3", "int4", "int5", "int6", "int7", "int8", "int9", "int10", "int11", "int12", "int13", "int14", "int15", "int16", "ext1", "ext2", "ext3", "ext4", "ext5", "imm"] type: str awg_trigger_source: default: "imm" @@ -285,7 +285,7 @@ devices: q: "sour{ch_id}:awg:trig:sour?" r: "{}" specs: - valid: ["bus", "hold", "int1", "int2", "int3", "int4", "int5", "int6", "int7", "int8", "int9", "int10", "int11", "int12", "int13", "int14", int15", "int16", "ext1", "ext2", "ext3", "ext4", "ext5", "imm"] + valid: ["bus", "hold", "int1", "int2", "int3", "int4", "int5", "int6", "int7", "int8", "int9", "int10", "int11", "int12", "int13", "int14", "int15", "int16", "ext1", "ext2", "ext3", "ext4", "ext5", "imm"] type: str current_aperture: default: 0.02 @@ -326,7 +326,7 @@ devices: setter: q: "sens{ch_id}:trig:sour {}" specs: - valid: ["bus", "hold", "int1", "int2", "int3", "int4", "int5", "int6", "int7", "int8", "int9", "int10", "int11", "int12", "int13", "int14", int15", "int16", "ext1", "ext2", "ext3", "ext4", "ext5", "imm"] + valid: ["bus", "hold", "int1", "int2", "int3", "int4", "int5", "int6", "int7", "int8", "int9", "int10", "int11", "int12", "int13", "int14", "int15", "int16", "ext1", "ext2", "ext3", "ext4", "ext5", "imm"] type: str current_range: default: "low" diff --git a/qcodes_contrib_drivers/sims/QSwitch.yaml b/qcodes_contrib_drivers/sims/QSwitch.yaml new file mode 100644 index 000000000..e249ddb61 --- /dev/null +++ b/qcodes_contrib_drivers/sims/QSwitch.yaml @@ -0,0 +1,93 @@ +spec: "1.1" +devices: + + qswitch_after_rst: + eom: + GPIB INSTR: + q: "\n" + r: "\n" + error: "-113,\"Undefined header\"" + dialogues: + - q: "*IDN?" + r: "Quantum Machines,QSwitch,123,1.0" + - q: "*idn?" + r: "Quantum Machines,QSwitch,123,1.0" + - q: "syst:comm:lan:mac?" + r: "\"049162C01016\"" + - q: "syst:err:all?" + r: "0,\"No error\"" + - q: "all?" + r: "0,\"No error\"" + - q: "stat?" + r: "(@1!0:24!0)" + - q: "*rst" + properties: + beeper: + default: "off" + setter: + q: "beep:stat {}" + getter: + q: "beep:stat?" + r: "{}" + specs: + valid: ["on", "off", "0", "1"] + type: str + autosave: + default: "off" + setter: + q: "aut {}" + getter: + q: "aut?" + r: "{}" + specs: + valid: ["on", "off", "0", "1"] + type: str + channels: + line: + ids: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24] + can_select: True + dialogues: + - q: "clos (@{ch_id}!0)" + - q: "clos (@{ch_id}!1)" + - q: "clos (@{ch_id}!8)" + - q: "clos (@{ch_id}!9)" + - q: "open (@{ch_id}!0)" + - q: "open (@{ch_id}!2)" + - q: "open (@{ch_id}!7)" + - q: "open (@{ch_id}!9)" + - q: "clos (@20!6,22!7,24!8,1!9)" + - q: "clos (@14!1,22!7)" + - q: "clos (@22!7)" + - q: "open (@1!0,3!0:24!0)" + - q: "open (@14!0,22!0)" + - q: "open (@14!0:15!0)" + - q: "clos (@14!0:15!0)" + - q: "clos (@14!9:15!9)" + - q: "open (@14!9:15!9)" + - q: "open (@15!1,15!9)" + - q: "open (@14!1:15!1,14!9:15!9)" + wrong_model: + eom: + GPIB INSTR: + q: "\n" + r: "\n" + dialogues: + - q: "*IDN?" + r: "QDevil,QAJAX,B009876,0.0.1" + + incompatible_firmware: + eom: + GPIB INSTR: + q: "\n" + r: "\n" + dialogues: + - q: "*IDN?" + r: "Quantum Machines,QSwitch,123,0.0" + +resources: + GPIB::4::INSTR: + device: qswitch_after_rst + GPIB::5::INSTR: + device: wrong_model + GPIB::6::INSTR: + device: incompatible_firmware diff --git a/qcodes_contrib_drivers/tests/QDevil/readme.md b/qcodes_contrib_drivers/tests/QDevil/readme.md index 8e56206cb..e5cd683cd 100644 --- a/qcodes_contrib_drivers/tests/QDevil/readme.md +++ b/qcodes_contrib_drivers/tests/QDevil/readme.md @@ -9,7 +9,7 @@ This directory tests the QDevil QDAC-II driver. Simulated instrument: $ source venv/bin/activate - $ pytest qcodes_contrib_drivers/tests/QDevil/test_sim_qdac2_*.py + $ pytest qcodes_contrib_drivers/tests/QDevil/test_sim_*_*.py Real instrument: @@ -19,7 +19,7 @@ Real instrument: Static types: - $ mypy --no-incremental qcodes_contrib_drivers/drivers/QDevil/{QDAC2,QDAC2_Array}.py + $ mypy --no-incremental qcodes_contrib_drivers/drivers/QDevil/{QDAC2,QDAC2_Array,QSwitch}.py ### One-time setup diff --git a/qcodes_contrib_drivers/tests/QDevil/sim_qswitch_fixtures.py b/qcodes_contrib_drivers/tests/QDevil/sim_qswitch_fixtures.py new file mode 100644 index 000000000..0a3594395 --- /dev/null +++ b/qcodes_contrib_drivers/tests/QDevil/sim_qswitch_fixtures.py @@ -0,0 +1,20 @@ +import pytest +from unittest.mock import patch +import uuid +from qcodes_contrib_drivers.drivers.QDevil import QSwitch +import qcodes_contrib_drivers.sims as sims + +# Use simulated instruments for the tests. +visalib = sims.__file__.replace('__init__.py', 'QSwitch.yaml@sim') + + +@pytest.fixture(scope='function') +def qswitch(): + name = ('switch' + str(uuid.uuid4())).replace('-', '') + switch = QSwitch.QSwitch(name, address='GPIB::4::INSTR', visalib=visalib) + switch.start_recording_scpi() + patch('qcodes_contrib_drivers.drivers.QDevil.QSwitch.sleep_s') + yield switch + lingering = switch.clear_read_queue() + if lingering: + raise ValueError(f'Lingering messages in visa queue: {lingering}') diff --git a/qcodes_contrib_drivers/tests/QDevil/test_sim_qdac2_leakage.py b/qcodes_contrib_drivers/tests/QDevil/test_sim_qdac2_leakage.py index 8f1595f23..6722ed27f 100644 --- a/qcodes_contrib_drivers/tests/QDevil/test_sim_qdac2_leakage.py +++ b/qcodes_contrib_drivers/tests/QDevil/test_sim_qdac2_leakage.py @@ -1,7 +1,6 @@ import pytest -from unittest.mock import MagicMock, call +from unittest.mock import call import numpy as np -import itertools import math from .sim_qdac2_fixtures import qdac # noqa from qcodes_contrib_drivers.drivers.QDevil.QDAC2 import diff_matrix diff --git a/qcodes_contrib_drivers/tests/QDevil/test_sim_qdac2_virtual_gates.py b/qcodes_contrib_drivers/tests/QDevil/test_sim_qdac2_virtual_gates.py index cb6178461..bbc137247 100644 --- a/qcodes_contrib_drivers/tests/QDevil/test_sim_qdac2_virtual_gates.py +++ b/qcodes_contrib_drivers/tests/QDevil/test_sim_qdac2_virtual_gates.py @@ -315,7 +315,6 @@ def test_arrangement_detune(qdac): # noqa ] -@pytest.mark.wip def test_arrangement_sweep_outer_trigger(qdac): # noqa qdac.free_all_triggers() arrangement = qdac.arrange( diff --git a/qcodes_contrib_drivers/tests/QDevil/test_sim_qswitch_direct_operation.py b/qcodes_contrib_drivers/tests/QDevil/test_sim_qswitch_direct_operation.py new file mode 100644 index 000000000..a1988bb78 --- /dev/null +++ b/qcodes_contrib_drivers/tests/QDevil/test_sim_qswitch_direct_operation.py @@ -0,0 +1,146 @@ +import pytest +from .common import assert_items_equal +from .sim_qswitch_fixtures import qswitch # noqa + + +def test_cached_state_can_be_updated(qswitch): # noqa + # ----------------------------------------------------------------------- + qswitch.state_force_update() + # ----------------------------------------------------------------------- + commands = qswitch.get_recorded_scpi_commands() + assert commands == ['stat?'] + + +def test_get_state(qswitch): # noqa + # ----------------------------------------------------------------------- + state = qswitch.state() + # ----------------------------------------------------------------------- + commands = qswitch.get_recorded_scpi_commands() + assert commands == ['stat?'] + assert state == '(@1!0:24!0)' + + +def test_set_state_changes_the_state(qswitch, mocker): # noqa + mocker.patch.object(qswitch, 'state_force_update') + # ----------------------------------------------------------------------- + qswitch.closed_relays( + [(24, 8), (24, 8), (22, 7), (20, 6), (1, 9), (2, 0)]) + # ----------------------------------------------------------------------- + assert qswitch.state() == '(@2!0,20!6,22!7,24!8,1!9)' + + +def test_set_state_only_sends_diff(qswitch): # noqa + # ----------------------------------------------------------------------- + qswitch.closed_relays( + [(24, 8), (24, 8), (22, 7), (20, 6), (1, 9), (2, 0)]) + # ----------------------------------------------------------------------- + commands = qswitch.get_recorded_scpi_commands() + assert commands == [ + 'clos (@20!6,22!7,24!8,1!9)', + 'open (@1!0,3!0:24!0)' + ] + + +def test_set_state_ignores_empty_diff(qswitch): # noqa + qswitch.closed_relays( + [(24, 8), (24, 8), (22, 7), (20, 6), (1, 9), (2, 0)]) + qswitch.start_recording_scpi() + # ----------------------------------------------------------------------- + qswitch.closed_relays([(24, 8), (22, 7), (20, 6), (1, 9), (2, 0)]) + # ----------------------------------------------------------------------- + commands = qswitch.get_recorded_scpi_commands() + assert commands == [] + + +def test_states_are_sanitised(qswitch, mocker): # noqa + mocker.patch.object(qswitch, 'state_force_update') + # ----------------------------------------------------------------------- + qswitch.closed_relays( + [(24, 8), (22, 7), (20, 6), (1, 9), (2, 0), (24, 8), (20, 6)]) + # ----------------------------------------------------------------------- + assert_items_equal( + qswitch.closed_relays(), + [(1, 9), (2, 0), (20, 6), (22, 7), (24, 8)] + ) + + +def test_individual_relays_can_be_closed(qswitch): # noqa + # ----------------------------------------------------------------------- + qswitch.close_relays([(14, 1), (22, 7)]) + # ----------------------------------------------------------------------- + commands = qswitch.get_recorded_scpi_commands() + assert commands == ['clos (@14!1,22!7)'] + + +def test_individual_relay_can_be_closed(qswitch): # noqa + # ----------------------------------------------------------------------- + qswitch.close_relay(22, 7) + # ----------------------------------------------------------------------- + commands = qswitch.get_recorded_scpi_commands() + assert commands == ['clos (@22!7)'] + + +def test_individual_relays_can_be_opened(qswitch): # noqa + # ----------------------------------------------------------------------- + qswitch.open_relays([(14, 0), (22, 0), (1, 1)]) + # ----------------------------------------------------------------------- + commands = qswitch.get_recorded_scpi_commands() + assert commands == ['open (@14!0,22!0)'] + + +def test_individual_relay_can_be_opened(qswitch): # noqa + # ----------------------------------------------------------------------- + qswitch.open_relay(14, 0) + # ----------------------------------------------------------------------- + commands = qswitch.get_recorded_scpi_commands() + assert commands == ['open (@14!0)'] + + +def test_beeper_can_be_turned_on(qswitch): # noqa + # ----------------------------------------------------------------------- + qswitch.error_indicator('on') + # ----------------------------------------------------------------------- + commands = qswitch.get_recorded_scpi_commands() + assert commands == ['beep:stat on'] + + +def test_beeper_can_be_turned_off(qswitch): # noqa + # ----------------------------------------------------------------------- + qswitch.error_indicator('off') + # ----------------------------------------------------------------------- + commands = qswitch.get_recorded_scpi_commands() + assert commands == ['beep:stat off'] + + +def test_beeper_state_can_be_queried(qswitch): # noqa + # ----------------------------------------------------------------------- + state = qswitch.error_indicator() + # ----------------------------------------------------------------------- + commands = qswitch.get_recorded_scpi_commands() + assert commands == ['beep:stat?'] + assert state == 'off' + + +def test_autosave_can_be_turned_on(qswitch): # noqa + # ----------------------------------------------------------------------- + qswitch.auto_save('on') + # ----------------------------------------------------------------------- + commands = qswitch.get_recorded_scpi_commands() + assert commands == ['aut on'] + + +def test_autosave_can_be_turned_off(qswitch): # noqa + # ----------------------------------------------------------------------- + qswitch.auto_save('off') + # ----------------------------------------------------------------------- + commands = qswitch.get_recorded_scpi_commands() + assert commands == ['aut off'] + + +def test_autosave_state_can_be_queried(qswitch): # noqa + # ----------------------------------------------------------------------- + state = qswitch.auto_save() + # ----------------------------------------------------------------------- + commands = qswitch.get_recorded_scpi_commands() + assert commands == ['aut?'] + assert state == 'off' diff --git a/qcodes_contrib_drivers/tests/QDevil/test_sim_qswitch_errors.py b/qcodes_contrib_drivers/tests/QDevil/test_sim_qswitch_errors.py new file mode 100644 index 000000000..e5eaf1012 --- /dev/null +++ b/qcodes_contrib_drivers/tests/QDevil/test_sim_qswitch_errors.py @@ -0,0 +1,18 @@ +import pytest +from .sim_qswitch_fixtures import qswitch # noqa + + +def test_unknown_line_name_gives_error(qswitch): # noqa + # ----------------------------------------------------------------------- + with pytest.raises(ValueError) as error: + qswitch.breakout('plunger', '1') + # ----------------------------------------------------------------------- + assert 'Unknown line' in repr(error) + + +def test_unknown_tap_name_gives_error(qswitch): # noqa + # ----------------------------------------------------------------------- + with pytest.raises(ValueError) as error: + qswitch.breakout('1', 'VNA') + # ----------------------------------------------------------------------- + assert 'Unknown tap' in repr(error) diff --git a/qcodes_contrib_drivers/tests/QDevil/test_sim_qswitch_ieee_std.py b/qcodes_contrib_drivers/tests/QDevil/test_sim_qswitch_ieee_std.py new file mode 100644 index 000000000..99eef2cb2 --- /dev/null +++ b/qcodes_contrib_drivers/tests/QDevil/test_sim_qswitch_ieee_std.py @@ -0,0 +1,23 @@ +import pytest +import re +from .sim_qswitch_fixtures import qswitch # noqa + + +def test_idn(qswitch): # noqa + # ----------------------------------------------------------------------- + idn_dict = qswitch.IDN() + # ----------------------------------------------------------------------- + assert idn_dict['vendor'] == 'Quantum Machines' + assert idn_dict['model'] == 'QSwitch' + assert re.fullmatch('[0-9]+', idn_dict['serial']) + assert re.fullmatch('[0-9]+\\.[0-9]+', idn_dict['firmware']) + + +def test_reset_syncs_and_wait(qswitch, mocker): # noqa + sleep_fn = mocker.patch('qcodes_contrib_drivers.drivers.QDevil.QSwitch.sleep_s') + # ----------------------------------------------------------------------- + qswitch.reset() + # ----------------------------------------------------------------------- + commands = qswitch.get_recorded_scpi_commands() + assert commands == ['*rst', 'stat?'] + sleep_fn.assert_any_call(0.6) diff --git a/qcodes_contrib_drivers/tests/QDevil/test_sim_qswitch_init.py b/qcodes_contrib_drivers/tests/QDevil/test_sim_qswitch_init.py new file mode 100644 index 000000000..febc63234 --- /dev/null +++ b/qcodes_contrib_drivers/tests/QDevil/test_sim_qswitch_init.py @@ -0,0 +1,28 @@ +import pytest +from qcodes_contrib_drivers.drivers.QDevil.QSwitch import QSwitch +from .sim_qswitch_fixtures import visalib + + +def test_refuse_wrong_model(): + # ----------------------------------------------------------------------- + with pytest.raises(ValueError) as error: + QSwitch('dmm', address='GPIB::5::INSTR', visalib=visalib) + # ----------------------------------------------------------------------- + assert 'Unknown model' in repr(error) + + +def test_refuse_incompatible_firmware(): + # ----------------------------------------------------------------------- + with pytest.raises(ValueError) as error: + QSwitch('qswitch', address='GPIB::6::INSTR', visalib=visalib) + # ----------------------------------------------------------------------- + assert 'Incompatible firmware' in repr(error) + + +def test_refuse_qcodes_incompatible_name(): + # ----------------------------------------------------------------------- + with pytest.raises(ValueError) as error: + QSwitch('QSwitch-1', address='GPIB::4::INSTR', visalib=visalib) + # ----------------------------------------------------------------------- + assert 'QSwitch-1' in repr(error) + assert 'incompatible with QCoDeS parameter' in repr(error) diff --git a/qcodes_contrib_drivers/tests/QDevil/test_sim_qswitch_internal.py b/qcodes_contrib_drivers/tests/QDevil/test_sim_qswitch_internal.py new file mode 100644 index 000000000..08084e00e --- /dev/null +++ b/qcodes_contrib_drivers/tests/QDevil/test_sim_qswitch_internal.py @@ -0,0 +1,54 @@ +import pytest +from qcodes_contrib_drivers.drivers.QDevil.QSwitch import ( + _state_diff, + channel_list_to_state, + compress_channel_list, + expand_channel_list) + + +@pytest.mark.parametrize(('input', 'output'), [ + ('(@)', []), + ('(@1!0,2!0)', [(1,0), (2,0)]), + ('(@1!0:2!0)', [(1,0), (2,0)]), + ('(@1!0,1!9)', [(1,0), (1,9)]), + ('(@1!0:3!0,4!9,23!7:24!7)', [(1,0), (2,0), (3,0), (4,9), (23,7), (24,7)]), +]) +def test_channel_list_to_map(input, output): # noqa + # ----------------------------------------------------------------------- + unpacked = channel_list_to_state(input) + # ----------------------------------------------------------------------- + assert unpacked == output + + +def test_channel_list_can_be_unpacked(): # noqa + # ----------------------------------------------------------------------- + unpacked = expand_channel_list('(@1!0:3!0,4!9,23!7:24!7)') + # ----------------------------------------------------------------------- + assert unpacked == '(@1!0,2!0,3!0,4!9,23!7,24!7)' + + +@pytest.mark.parametrize(('input', 'output'), [ + ('(@)', '(@)'), + ('(@1!2)', '(@1!2)'), + ('(@1!2,3!2)', '(@1!2,3!2)'), + ('(@1!0,2!0,3!0,4!9,23!7,24!7)', '(@1!0:3!0,23!7:24!7,4!9)') +]) +def test_channel_list_can_be_packed(input, output): # noqa + # ----------------------------------------------------------------------- + packed = compress_channel_list(input) + # ----------------------------------------------------------------------- + assert packed == output + + +@pytest.mark.parametrize(('before', 'after', 'positive', 'negative'), [ + ([], [], [], []), + ([], [(1,2)], [(1,2)], []), + ([(7,5)], [(1,2)], [(1,2)], [(7,5)]), + ([(7,5), (3,4)], [(1,2), (3,4)], [(1,2)], [(7,5)]), +]) +def test_state_diff(before, after, positive, negative): # noqa + # ----------------------------------------------------------------------- + pos, neg, _ = _state_diff(before, after) + # ----------------------------------------------------------------------- + assert pos == positive + assert neg == negative diff --git a/qcodes_contrib_drivers/tests/QDevil/test_sim_qswitch_operation.py b/qcodes_contrib_drivers/tests/QDevil/test_sim_qswitch_operation.py new file mode 100644 index 000000000..304ec892e --- /dev/null +++ b/qcodes_contrib_drivers/tests/QDevil/test_sim_qswitch_operation.py @@ -0,0 +1,94 @@ +import pytest +from unittest.mock import call +from .sim_qswitch_fixtures import qswitch # noqa + + +def test_ground_by_name(qswitch): # noqa + qswitch.close_relay(15, 9) + qswitch.open_relay(15, 0) + qswitch.start_recording_scpi() + # ----------------------------------------------------------------------- + qswitch.ground('15') + # ----------------------------------------------------------------------- + commands = qswitch.get_recorded_scpi_commands() + assert commands == ['clos (@15!0)', 'open (@15!9)'] + + +def test_ground_by_names(qswitch): # noqa + qswitch.close_relay(14, 9) + qswitch.open_relay(14, 0) + qswitch.close_relay(15, 9) + qswitch.open_relay(15, 0) + qswitch.start_recording_scpi() + # ----------------------------------------------------------------------- + qswitch.ground(['15', '14']) + # ----------------------------------------------------------------------- + commands = qswitch.get_recorded_scpi_commands() + assert commands == ['clos (@14!0:15!0)', 'open (@14!9:15!9)'] + + +def test_connect_by_name(qswitch): # noqa + # ----------------------------------------------------------------------- + qswitch.connect('15') + # ----------------------------------------------------------------------- + commands = qswitch.get_recorded_scpi_commands() + assert commands == ['clos (@15!9)', 'open (@15!0)'] + + +def test_connect_by_names(qswitch): # noqa + # ----------------------------------------------------------------------- + qswitch.connect(['14', '15']) + # ----------------------------------------------------------------------- + commands = qswitch.get_recorded_scpi_commands() + assert commands == ['clos (@14!9:15!9)', 'open (@14!0:15!0)'] + + +def test_breakout_by_name(qswitch): # noqa + # ----------------------------------------------------------------------- + qswitch.breakout('22', '7') + # ----------------------------------------------------------------------- + commands = qswitch.get_recorded_scpi_commands() + assert commands == ['clos (@22!7)', 'open (@22!0)'] + + +def test_arrangement_gives_names_to_connections(qswitch): # noqa + # ----------------------------------------------------------------------- + qswitch.arrange( + breakouts={'DMM': 2, 'VNA': 1}, + lines={'plunger': 14, 'sensor': 3} + ) + qswitch.breakout('plunger', 'VNA') + qswitch.connect('sensor') + # ----------------------------------------------------------------------- + commands = qswitch.get_recorded_scpi_commands() + assert commands == [ + 'clos (@14!1)', 'open (@14!0)', + 'clos (@3!9)', 'open (@3!0)'] + + +def test_ground_disconnects_everything(qswitch): # noqa + qswitch.close_relay(15, 9) + qswitch.close_relay(15, 1) + qswitch.open_relay(15, 0) + qswitch.start_recording_scpi() + # ----------------------------------------------------------------------- + qswitch.ground('15') + # ----------------------------------------------------------------------- + commands = qswitch.get_recorded_scpi_commands() + assert commands == ['clos (@15!0)', 'open (@15!1,15!9)'] + + +def test_ground_disconnects_multiple(qswitch): # noqa + qswitch.close_relay(14, 9) + qswitch.close_relay(14, 1) + qswitch.open_relay(14, 0) + qswitch.close_relay(15, 9) + qswitch.close_relay(15, 1) + qswitch.open_relay(15, 0) + qswitch.start_recording_scpi() + # ----------------------------------------------------------------------- + qswitch.ground(['15', '14']) + # ----------------------------------------------------------------------- + commands = qswitch.get_recorded_scpi_commands() + assert commands == ['clos (@14!0:15!0)', 'open (@14!1:15!1,14!9:15!9)'] + diff --git a/qcodes_contrib_drivers/tests/QDevil/test_sim_qswitch_snapshots.py b/qcodes_contrib_drivers/tests/QDevil/test_sim_qswitch_snapshots.py new file mode 100644 index 000000000..2151b321d --- /dev/null +++ b/qcodes_contrib_drivers/tests/QDevil/test_sim_qswitch_snapshots.py @@ -0,0 +1,19 @@ +import pytest +from .sim_qswitch_fixtures import qswitch # noqa + + +def test_initial_state_is_recorded_in_snapshot(qswitch): # noqa + # ----------------------------------------------------------------------- + snapshot = qswitch.snapshot(True) + # ----------------------------------------------------------------------- + relays = snapshot['parameters']['state']['value'] + assert relays == '(@1!0:24!0)' + + +def test_state_change_is_recorded_in_snapshot(qswitch): # noqa + # ----------------------------------------------------------------------- + qswitch.closed_relays([(24,8), (22,7), (20,6), (1,9), (2,0)]) + snapshot = qswitch.snapshot() + # ----------------------------------------------------------------------- + relays = snapshot['parameters']['state']['value'] + assert relays == '(@2!0,20!6,22!7,24!8,1!9)'