{ "cells": [ { "cell_type": "markdown", "id": "9d5a1058", "metadata": {}, "source": [ "# Recording parameter access\n", "\n", "QCoDeS can record every `get` and `set` of every parameter that a piece of code makes. Each access is recorded as a `ParameterAccess`: the names of the parameter and its instrument, the value and raw value, when the access started, how long it took, and the error it raised, if any.\n", "\n", "This lets you see what a piece of code did to your instruments without changing the code or the drivers it uses. You can record what a measurement set and read, find out which parameters are queried most often, or keep a log of everything that was set.\n", "\n", "Recording only stores what happened:\n", "\n", "- Accesses are appended to a mutable sequence, such as a `list`, or logged to a fixed QCoDeS logger. Recording calls none of your code, so it cannot be used to react to an access, for example by setting another parameter. If you pass in a sequence of your own, its `append` must only store the access. Logging an access runs the logging handlers and filters you configured, as any log call does, but accesses they make are not logged again, and an error they raise does not affect the access.\n", "- A record holds data only: the names of the parameter and its instrument rather than the objects, and a description of an error rather than the exception.\n", "- Only accesses made in the thread that started recording are recorded, and only until the recording ends." ] }, { "cell_type": "code", "execution_count": 1, "id": "cb341e86", "metadata": {}, "outputs": [], "source": [ "import logging\n", "import sys\n", "import tempfile\n", "import threading\n", "from collections import Counter\n", "from pathlib import Path\n", "\n", "from qcodes.dataset import (\n", " do1d,\n", " initialise_or_create_database_at,\n", " load_or_create_experiment,\n", ")\n", "from qcodes.instrument_drivers.mock_instruments import (\n", " DummyInstrument,\n", " DummyInstrumentWithMeasurement,\n", ")\n", "from qcodes.parameters import (\n", " Parameter,\n", " ParameterAccess,\n", " log_parameter_accesses,\n", " record_parameter_accesses,\n", ")" ] }, { "cell_type": "markdown", "id": "d2d38547", "metadata": {}, "source": [ "We use two mock instruments: a DAC with two channels, and a DMM whose readings depend on the DAC's voltages." ] }, { "cell_type": "code", "execution_count": 2, "id": "361ea797", "metadata": {}, "outputs": [], "source": [ "dac = DummyInstrument(\"dac\", gates=[\"ch1\", \"ch2\"])\n", "dmm = DummyInstrumentWithMeasurement(\"dmm\", setter_instr=dac)" ] }, { "cell_type": "markdown", "id": "deed3b96", "metadata": {}, "source": [ "## Recording the accesses of a block of code\n", "\n", "`record_parameter_accesses` records the accesses made inside a `with` block in a list, in the order they complete." ] }, { "cell_type": "code", "execution_count": 3, "id": "7551cd51", "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "set dac_ch1: 0.5\n", "get dmm_v1: 4.3682652935244874\n" ] } ], "source": [ "with record_parameter_accesses() as accesses:\n", " dac.ch1(0.5)\n", " dmm.v1()\n", "\n", "for access in accesses:\n", " print(f\"{access.kind} {access.parameter_name}: {access.value}\")" ] }, { "cell_type": "markdown", "id": "d4c10c17", "metadata": {}, "source": [ "Accesses outside the block are not recorded:" ] }, { "cell_type": "code", "execution_count": 4, "id": "0eb5add7", "metadata": {}, "outputs": [ { "data": { "text/plain": [ "2" ] }, "execution_count": 4, "metadata": {}, "output_type": "execute_result" } ], "source": [ "dac.ch1(0.7)\n", "len(accesses)" ] }, { "cell_type": "markdown", "id": "4ca4a1bc", "metadata": {}, "source": [ "You can also pass in the sequence to record in, such as a `list` you want to keep adding to. Any mutable sequence works, for example a `collections.deque` with a `maxlen` that keeps only the most recent accesses. A sequence of your own may store accesses differently, but its `append` must only store the access: it must not run any other code, such as accessing a parameter or an instrument. Recording is not a way to react to an access." ] }, { "cell_type": "code", "execution_count": 5, "id": "6f5387c7", "metadata": {}, "outputs": [ { "data": { "text/plain": [ "[0.1, 0.2]" ] }, "execution_count": 5, "metadata": {}, "output_type": "execute_result" } ], "source": [ "everything: list[ParameterAccess] = []\n", "with record_parameter_accesses(everything):\n", " dac.ch2(0.1)\n", "with record_parameter_accesses(everything):\n", " dac.ch2(0.2)\n", "\n", "[access.value for access in everything]" ] }, { "cell_type": "markdown", "id": "ad911579", "metadata": {}, "source": [ "## What a record contains\n", "\n", "Each `ParameterAccess` describes one access:\n", "\n", "- `parameter_name`: the full name of the parameter.\n", "- `instrument_name`: the full name of the parameter's root instrument, or `None`.\n", "- `kind`: `\"get\"` or `\"set\"`.\n", "- `value`: the value that was read or written.\n", "- `raw_value`: the value passed to or returned by the instrument, before scaling, offsets or value mappings are applied.\n", "- `timestamp`: when the instrument was called (`get_raw` or `set_raw`), in seconds since the epoch.\n", "- `duration`: how long the access took, in seconds, including value conversions such as a `get_parser` or `set_parser`. Delays configured on the parameter, such as `inter_delay` and `post_delay`, are not included.\n", "- `error`: the type and message of the exception the access raised, or `None`.\n", "\n", "Metadata such as the unit is not recorded, because a driver may read it from the instrument and recording must not talk to instruments. Look it up on the parameter, or in the station snapshot, when you need it.\n", "\n", "The difference between `value` and `raw_value` shows what actually reached the instrument. For example, behind a 1:10 voltage divider, setting 2 V on the sample means setting 20 V on the source:" ] }, { "cell_type": "code", "execution_count": 6, "id": "6dbb149b", "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "value=2.0, raw_value=20.0, error=None\n" ] } ], "source": [ "divided = Parameter(\"divided\", unit=\"V\", set_cmd=None, get_cmd=None, scale=10)\n", "\n", "with record_parameter_accesses() as set_accesses:\n", " divided(2.0)\n", "\n", "(access,) = set_accesses\n", "print(f\"value={access.value}, raw_value={access.raw_value}, error={access.error}\")" ] }, { "cell_type": "markdown", "id": "b554d90e", "metadata": {}, "source": [ "## Ramped sets are recorded step by step\n", "\n", "A parameter with a `step` is set in several steps, each of which is a separate call to the instrument. Every step is recorded:" ] }, { "cell_type": "code", "execution_count": 7, "id": "cacd002a", "metadata": {}, "outputs": [ { "data": { "text/plain": [ "[0.3, 0.4, 0.5]" ] }, "execution_count": 7, "metadata": {}, "output_type": "execute_result" } ], "source": [ "dac.ch2.step = 0.1\n", "dac.ch2.inter_delay = 0\n", "\n", "with record_parameter_accesses() as ramp:\n", " dac.ch2(0.5)\n", "\n", "[round(access.value, 2) for access in ramp]" ] }, { "cell_type": "markdown", "id": "9caa7b47", "metadata": {}, "source": [ "## Failed accesses\n", "\n", "An access that raises is recorded too, with a description of the exception in `error`, and the exception then propagates to the caller as usual. A value that fails validation is rejected before the instrument is contacted, so it is not an access and is not recorded." ] }, { "cell_type": "code", "execution_count": 8, "id": "9c54d07b", "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "The caller still sees the error: the sensor did not answer\n", "Recorded: get sensor: TimeoutError: the sensor did not answer\n" ] } ], "source": [ "def read_unresponsive_sensor() -> float:\n", " raise TimeoutError(\"the sensor did not answer\")\n", "\n", "\n", "sensor = Parameter(\"sensor\", get_cmd=read_unresponsive_sensor)\n", "\n", "with record_parameter_accesses() as failures:\n", " try:\n", " sensor()\n", " except TimeoutError as error:\n", " print(f\"The caller still sees the error: {error.args[0]}\")\n", "\n", "(failure,) = failures\n", "print(f\"Recorded: {failure.kind} {failure.parameter_name}: {failure.error}\")" ] }, { "cell_type": "markdown", "id": "dc58e4a4", "metadata": {}, "source": [ "## What did a measurement do to the instruments?\n", "\n", "Recording sees all accesses made in its context, including those QCoDeS makes itself, so it can show what a measurement did. Here we run a 1D sweep and count the accesses per parameter." ] }, { "cell_type": "code", "execution_count": 9, "id": "0aa93da2", "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Starting experimental run with id: 1. Using 'qcodes.dataset.do1d'\n" ] }, { "data": { "text/plain": [ "Counter({('dac_ch1', 'set'): 11, ('dmm_v1', 'get'): 11})" ] }, "execution_count": 9, "metadata": {}, "output_type": "execute_result" } ], "source": [ "initialise_or_create_database_at(Path(tempfile.mkdtemp()) / \"recording.db\")\n", "load_or_create_experiment(\"recording_parameter_access\", sample_name=\"no sample\")\n", "\n", "with record_parameter_accesses() as sweep:\n", " do1d(dac.ch1, 0, 1, 11, 0.0, dmm.v1, do_plot=False)\n", "\n", "Counter((access.parameter_name, access.kind) for access in sweep)" ] }, { "cell_type": "markdown", "id": "840b7ca6", "metadata": {}, "source": [ "The sweep set the DAC 11 times and read the DMM 11 times. The accesses are in the order they happened, so we can also follow the sweep point by point:" ] }, { "cell_type": "code", "execution_count": 10, "id": "58960777", "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "set dac_ch1: 0.000\n", "get dmm_v1: 4.946\n", "set dac_ch1: 0.100\n", "get dmm_v1: 4.862\n", "set dac_ch1: 0.200\n", "get dmm_v1: 4.809\n" ] } ], "source": [ "for access in sweep[:6]:\n", " print(f\"{access.kind} {access.parameter_name}: {access.value:.3f}\")" ] }, { "cell_type": "markdown", "id": "63c58da9", "metadata": {}, "source": [ "To follow one instrument, filter on the instrument name:" ] }, { "cell_type": "code", "execution_count": 11, "id": "e4072ce8", "metadata": {}, "outputs": [ { "data": { "text/plain": [ "[0.0, 0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9, 1.0]" ] }, "execution_count": 11, "metadata": {}, "output_type": "execute_result" } ], "source": [ "dac_sets = [\n", " access.value\n", " for access in sweep\n", " if access.instrument_name == \"dac\" and access.kind == \"set\"\n", "]\n", "[round(float(value), 2) for value in dac_sets]" ] }, { "cell_type": "markdown", "id": "c5b922e6", "metadata": {}, "source": [ "## Recording is local to a thread\n", "\n", "Only accesses made by the thread that started recording are recorded: by the code in the block, and by asyncio tasks it starts while the block runs. Work in other threads is not recorded, even while the block runs, and even if it runs in a copy of the recording context, as `asyncio.to_thread` does:" ] }, { "cell_type": "code", "execution_count": 12, "id": "23a74c87", "metadata": {}, "outputs": [ { "data": { "text/plain": [ "[0.3]" ] }, "execution_count": 12, "metadata": {}, "output_type": "execute_result" } ], "source": [ "with record_parameter_accesses() as here:\n", " worker = threading.Thread(target=dac.ch1, args=(0.9,))\n", " worker.start()\n", " worker.join()\n", " dac.ch1(0.3)\n", "\n", "[access.value for access in here]" ] }, { "cell_type": "markdown", "id": "fe51b03d", "metadata": {}, "source": [ "Recordings can be nested. Each one receives every access made while it is active:" ] }, { "cell_type": "code", "execution_count": 13, "id": "13031c57", "metadata": {}, "outputs": [ { "data": { "text/plain": [ "([0.1, 0.2], [0.2])" ] }, "execution_count": 13, "metadata": {}, "output_type": "execute_result" } ], "source": [ "with record_parameter_accesses() as outer:\n", " dac.ch1(0.1)\n", " with record_parameter_accesses() as inner:\n", " dac.ch1(0.2)\n", "\n", "[access.value for access in outer], [access.value for access in inner]" ] }, { "cell_type": "markdown", "id": "5861ab28", "metadata": {}, "source": [ "## Logging accesses\n", "\n", "`log_parameter_accesses` logs the accesses made inside a `with` block to the `qcodes.parameters.parameter_access` logger, at `DEBUG` level unless you choose another. Each log record carries the `ParameterAccess` as its `parameter_access` attribute, so a handler can store it in structured form." ] }, { "cell_type": "code", "execution_count": 14, "id": "63ce152c", "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "set dac_ch1 = 0.25\n" ] }, { "name": "stdout", "output_type": "stream", "text": [ "get dmm_v2 = 0.4612975233041446\n" ] } ], "source": [ "access_log = logging.getLogger(\"qcodes.parameters.parameter_access\")\n", "access_log.setLevel(logging.DEBUG)\n", "access_log.propagate = False\n", "access_log.addHandler(logging.StreamHandler(sys.stdout))\n", "\n", "with log_parameter_accesses():\n", " dac.ch1(0.25)\n", " reading = dmm.v2()" ] }, { "cell_type": "markdown", "id": "ea5ce8be", "metadata": {}, "source": [ "## Good to know\n", "\n", "- Recording adds a little work to every access made while it is active, and none otherwise.\n", "- Values are recorded as the parameter returned or received them, not copied.\n", "- An access to a `DelegateParameter` is recorded twice: once for the delegate and once for its source.\n", "- Only parameters are recorded. Commands that a driver sends directly, for example with `Instrument.write` or `Instrument.ask` in a driver method, are not. Use the instrument's logger (`visa_log` for VISA instruments) to see the communication on the wire.\n", "- If a logging handler or filter raises while an access is logged, the access still completes as it would have without logging. The error is logged to the `qcodes.parameters.parameter_access_errors` logger instead." ] }, { "cell_type": "code", "execution_count": 15, "id": "0c6b690d", "metadata": {}, "outputs": [], "source": [ "dac.close()\n", "dmm.close()" ] } ], "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.12.15" } }, "nbformat": 4, "nbformat_minor": 5 }