{ "cells": [ { "cell_type": "markdown", "id": "71d00eeb", "metadata": {}, "source": [ "# Tutorial 2. Advanced capabilities of the MeasurementControl\n", "\n", "```{seealso}\n", "The complete source code of this tutorial can be found in\n", "\n", "{nb-download}`Tutorial 2. Advanced capabilities of the MeasurementControl.ipynb`\n", "```\n", "\n", "Following this Tutorial requires familiarity with the **core concepts** of Quantify, we **highly recommended** to consult the (short) {ref}`User guide` before proceeding (see Quantify documentation). If you have some difficulties following the tutorial it might be worth reviewing the {ref}`User guide`!\n", "\n", "We **highly recommended** beginning with {ref}`Tutorial 1. Controlling a basic experiment using MeasurementControl` before proceeding.\n", "\n", "In this tutorial, we will explore the more advanced features of Quantify. By the end of this tutorial, we will have covered:\n", "\n", "- Using hardware to drive experiments\n", "- Software averaging\n", "- Interrupting an experiment" ] }, { "cell_type": "code", "execution_count": null, "id": "f5bfac56", "metadata": { "mystnb": { "code_prompt_show": "Imports and auxiliary utilities" }, "tags": [ "hide-cell" ] }, "outputs": [], "source": [ "import os\n", "import signal\n", "import sys\n", "import time\n", "\n", "import numpy as np\n", "from lmfit import Model\n", "from qcodes import ManualParameter\n", "\n", "import quantify_core.visualization.pyqt_plotmon as pqm\n", "from quantify_core.data.handling import set_datadir, default_datadir\n", "from quantify_core.measurement.control import MeasurementControl\n", "from quantify_core.visualization.instrument_monitor import InstrumentMonitor\n", "\n", "rng = np.random.default_rng(seed=222222) # random number generator" ] }, { "cell_type": "markdown", "id": "75b88a56", "metadata": {}, "source": [ "**Before instantiating any instruments or starting a measurement** we change the\n", "directory in which the experiments are saved using the\n", "{meth}`~quantify_core.data.handling.set_datadir`\n", "\\[{meth}`~quantify_core.data.handling.get_datadir`\\] functions.\n", "\n", "----------------------------------------------------------------------------------------\n", "\n", "⚠️ **Warning!**\n", "\n", "We recommend always setting the directory at the start of the Python kernel and sticking to a single common data directory for all notebooks/experiments within your\n", "measurement setup/PC.\n", "\n", "The cell below sets a default data directory (`~/quantify-data` on Linux/macOS or\n", "`$env:USERPROFILE\\\\quantify-data` on Windows) for tutorial purposes. Change it to your\n", "desired data directory. The utilities to find/search/extract data only work if\n", "all the experiment containers are located within the same directory.\n", "\n", "----------------------------------------------------------------------------------------" ] }, { "cell_type": "code", "execution_count": null, "id": "21867a1f", "metadata": {}, "outputs": [], "source": [ "set_datadir(default_datadir()) # change me!" ] }, { "cell_type": "code", "execution_count": null, "id": "d236c77d", "metadata": { "mystnb": { "remove-output": true } }, "outputs": [], "source": [ "meas_ctrl = MeasurementControl(\"meas_ctrl\")\n", "plotmon = pqm.PlotMonitor_pyqt(\"plotmon_meas_ctrl\")\n", "meas_ctrl.instr_plotmon(plotmon.name)\n", "insmon = InstrumentMonitor(\"InstrumentMonitor\")" ] }, { "cell_type": "markdown", "id": "75bac5f5", "metadata": {}, "source": [ "## A 1D Batched loop: Resonator Spectroscopy\n", "\n", "### Defining a simple model\n", "\n", "In this example, we want to find the resonance of some devices. We expect to find its resonance somewhere in the low 6 GHz range, but manufacturing imperfections make it impossible to know exactly without inspection.\n", "\n", "We first create `freq`: a {class}`.Settable` with a {class}`~qcodes.parameters.Parameter` to represent the frequency of the signal probing the resonator, followed by a custom {class}`.Gettable` to mock (i.e. emulate) the resonator.\n", "The {class}`!Resonator` will return a Lorentzian shape centered on the resonant frequency. Our {class}`.Gettable` will read the setpoints from `freq`, in this case a 1D array.\n", "\n", "```{note}\n", "The `Resonator` {class}`.Gettable` has a new attribute `.batched` set to `True`. This property informs the {class}`.MeasurementControl` that it will not be in charge of iterating over the setpoints, instead the `Resonator` manages its own data acquisition. Similarly, the `freq` {class}`.Settable` must have a `.batched=True` so that the {class}`.MeasurementControl` hands over the setpoints correctly.\n", "```" ] }, { "cell_type": "code", "execution_count": null, "id": "5c4c4fe7", "metadata": {}, "outputs": [], "source": [ "# Note that in an actual experimental setup `freq` will be a QCoDeS parameter\n", "# contained in a QCoDeS Instrument\n", "freq = ManualParameter(name=\"frequency\", unit=\"Hz\", label=\"Frequency\")\n", "freq.batched = True # Tells meas_ctrl that the setpoints are to be passed in batches\n", "\n", "\n", "def lorenz(amplitude: float, fwhm: float, x: int, x_0: float):\n", " \"\"\"Model of the frequency response.\"\"\"\n", " return amplitude * ((fwhm / 2.0) ** 2) / ((x - x_0) ** 2 + (fwhm / 2.0) ** 2)\n", "\n", "\n", "class Resonator:\n", " \"\"\"\n", " Note that the Resonator is a valid Gettable not because of inheritance,\n", " but because it has the expected attributes and methods.\n", " \"\"\"\n", "\n", " def __init__(self) -> None:\n", " self.name = \"resonator\"\n", " self.unit = \"V\"\n", " self.label = \"Amplitude\"\n", " self.batched = True\n", " self.delay = 0.0\n", "\n", " # hidden variables specifying the resonance\n", " self._test_resonance = 6.0001048e9 # in Hz\n", " self._test_width = 300 # FWHM in Hz\n", "\n", " def get(self) -> float:\n", " \"\"\"Emulation of the frequency response.\"\"\"\n", " time.sleep(self.delay)\n", " _lorenz = lambda x: lorenz(1, self._test_width, x, self._test_resonance)\n", " return 1 - np.array(list(map(_lorenz, freq())))\n", "\n", " def prepare(self) -> None:\n", " \"\"\"Adding this print statement is not required but added for illustrative\n", " purposes.\"\"\"\n", " print(\"\\nPrepared Resonator...\")\n", "\n", " def finish(self) -> None:\n", " \"\"\"Adding this print statement is not required but added for illustrative\n", " purposes.\"\"\"\n", " print(\"\\nFinished Resonator...\")\n", "\n", "\n", "gettable_res = Resonator()" ] }, { "cell_type": "markdown", "id": "3eba38ef", "metadata": {}, "source": [ "### Running the experiment\n", "\n", "Just like our Iterative 1D loop, our complete experiment is expressed in just four lines of code.\n", "\n", "The main difference is defining the `batched` property of our {class}`.Gettable` to `True`.\n", "The {class}`.MeasurementControl` will detect these settings and run in the appropriate mode.\n", "\n", "At this point the `freq` parameter is empty:" ] }, { "cell_type": "code", "execution_count": null, "id": "98c44659", "metadata": {}, "outputs": [], "source": [ "print(freq())" ] }, { "cell_type": "code", "execution_count": null, "id": "fbdd9d47", "metadata": { "mystnb": { "remove-output": true } }, "outputs": [], "source": [ "meas_ctrl.settables(freq)\n", "meas_ctrl.setpoints(np.arange(6.0001e9, 6.00011e9, 5))\n", "meas_ctrl.gettables(gettable_res)\n", "dset = meas_ctrl.run()" ] }, { "cell_type": "code", "execution_count": null, "id": "ecc5e58d", "metadata": {}, "outputs": [], "source": [ "plotmon.main_QtPlot" ] }, { "cell_type": "markdown", "id": "f04a96a0", "metadata": {}, "source": [ "As expected, we find a Lorentzian spike in the readout at the resonant frequency, finding the peak of which is trivial.\n", "\n", "#### Memory-limited Settables/Gettables\n", "\n", "Instruments (either physical or virtual) operating in `batched` mode have an upper limit on how many datapoints can be processed at once.\n", "When an experiment is comprised of more datapoints than the instrument can handle, the {class}`.MeasurementControl` takes care of fulfilling the measurement of all the requested setpoints by running and an internal loop.\n", "\n", "By default the {class}`.MeasurementControl` assumes no limitations and passes all setpoints to the `batched` settable. However, as a best practice, the instrument limitation must be reflected by the `.batch_size` attribute of the `batched` settables. This is illustrated below." ] }, { "cell_type": "code", "execution_count": null, "id": "11f48755", "metadata": { "mystnb": { "remove-output": true } }, "outputs": [], "source": [ "# Tells meas_ctrl that only 256 datapoints can be processed at once\n", "freq.batch_size = 256\n", "\n", "gettable_res.delay = 0.05 # short delay for plotting\n", "meas_ctrl.settables(freq)\n", "meas_ctrl.setpoints(np.arange(6.0001e9, 6.00011e9, 5))\n", "meas_ctrl.gettables(gettable_res)\n", "dset = meas_ctrl.run()" ] }, { "cell_type": "code", "execution_count": null, "id": "bd0cbdc5", "metadata": {}, "outputs": [], "source": [ "plotmon.main_QtPlot" ] }, { "cell_type": "markdown", "id": "a1a6c5b0", "metadata": {}, "source": [ "## Software Averaging: T1 Experiment\n", "\n", "In many cases it is desirable to run an experiment many times and average the result, such as when filtering noise on instruments or measuring probability.\n", "For this purpose, the {meth}`.MeasurementControl.run` provides the `soft_avg` argument.\n", "If set to *x*, the experiment will run *x* times whilst performing a running average over each setpoint.\n", "\n", "In this example, we want to find the relaxation time (aka T1) of a Qubit. As before, we define a {class}`.Settable` and {class}`.Gettable`, representing the varying timescales we will probe through and a mock Qubit emulated in software.\n", "The mock Qubit returns the expected decay sweep but with a small amount of noise (simulating the variable qubit characteristics). We set the qubit's T1 to 60 ms - obviously in a real experiment we would be trying to determine this, but for this illustration purposes in this tutorial we set it to a known value to verify our fit later on.\n", "\n", "Note that in this example meas_ctrl is still running in Batched mode." ] }, { "cell_type": "code", "execution_count": null, "id": "a8fd9739", "metadata": {}, "outputs": [], "source": [ "def decay(t, tau):\n", " \"\"\"T1 experiment decay model.\"\"\"\n", " return np.exp(-t / tau)\n", "\n", "\n", "time_par = ManualParameter(name=\"time\", unit=\"s\", label=\"Measurement Time\")\n", "# Tells meas_ctrl that the setpoints are to be passed in batches\n", "time_par.batched = True\n", "\n", "\n", "class MockQubit:\n", " \"\"\"A mock qubit.\"\"\"\n", "\n", " def __init__(self):\n", " self.name = \"qubit\"\n", " self.unit = \"%\"\n", " self.label = \"High V\"\n", " self.batched = True\n", "\n", " self.delay = 0.01 # sleep time in secs\n", " self.test_relaxation_time = 60e-6\n", "\n", " def get(self):\n", " \"\"\"Adds a delay to be able to appreciate the data acquisition.\"\"\"\n", " time.sleep(self.delay)\n", " rel_time = self.test_relaxation_time\n", " _func = lambda x: decay(x, rel_time) + rng.uniform(-0.1, 0.1)\n", " return np.array(list(map(_func, time_par())))" ] }, { "cell_type": "markdown", "id": "b0ad9691", "metadata": {}, "source": [ "We will then sweep through 0 to 300 ms, getting our data from the mock Qubit. Let's first observe what a single run looks like:" ] }, { "cell_type": "code", "execution_count": null, "id": "a04da706", "metadata": { "mystnb": "remove-output:true" }, "outputs": [], "source": [ "meas_ctrl.settables(time_par)\n", "meas_ctrl.setpoints(np.linspace(0.0, 300.0e-6, 300))\n", "meas_ctrl.gettables(MockQubit())\n", "meas_ctrl.run(\"noisy\") # by default `.run` uses `soft_avg=1`" ] }, { "cell_type": "code", "execution_count": null, "id": "5e34df94", "metadata": {}, "outputs": [], "source": [ "plotmon.main_QtPlot" ] }, { "cell_type": "markdown", "id": "3cd212ae", "metadata": {}, "source": [ "Alas, the noise in the signal has made this result unusable! Let's set the `soft_avg` argument of the {meth}`.MeasurementControl.run` to 100, averaging the results and hopefully filtering out the noise." ] }, { "cell_type": "code", "execution_count": null, "id": "cace9ef8", "metadata": { "mystnb": { "remove-output": true } }, "outputs": [], "source": [ "dset = meas_ctrl.run(\"averaged\", soft_avg=100)" ] }, { "cell_type": "code", "execution_count": null, "id": "5aa93cbc", "metadata": {}, "outputs": [], "source": [ "plotmon.main_QtPlot" ] }, { "cell_type": "markdown", "id": "a7ffa625", "metadata": {}, "source": [ "Success! We now have a smooth decay curve based on the characteristics of our qubit. All that remains is to run a fit against the expected values and we can solve for T1." ] }, { "cell_type": "code", "execution_count": null, "id": "bd8301b1", "metadata": {}, "outputs": [], "source": [ "model = Model(decay, independent_vars=[\"t\"])\n", "fit_res = model.fit(dset[\"y0\"].values, t=dset[\"x0\"].values, tau=1)\n", "\n", "fit_res.plot_fit(show_init=True)\n", "fit_res.values" ] }, { "cell_type": "markdown", "id": "a3a36dec", "metadata": {}, "source": [ "## Interrupting\n", "Sometimes experiments, unfortunately, do not go as planned and it is desirable to interrupt and restart them with new parameters. In the following example, we have a long-running experiment where our Gettable is taking a long time to return data (maybe due to misconfiguration).\n", "Rather than waiting for this experiment to complete, instead we can interrupt any {class}`.MeasurementControl` loop using the standard interrupt signal.\n", "In a terminal environment this is usually achieved with a `ctrl` + `c` press on the keyboard or equivalent, whilst in a Jupyter environment interrupting the kernel (stop button) will cause the same result.\n", "\n", "When the {class}`.MeasurementControl` is interrupted, it will wait to obtain the results of the current iteration (or batch) and perform a final save of the data it has gathered, calling the `finish()` method on Settables & Gettables (if it exists) and return the partially completed dataset.\n", "\n", "```{note}\n", "The exact means of triggering an interrupt will differ depending on your platform and environment; the important part is to cause a `KeyboardInterrupt` exception to be raised in the Python process.\n", "```\n", "\n", "In case the current iteration is taking too long to complete (e.g. instruments not responding), you may force the execution of any python code to stop by signaling the same interrupt 5 times (e.g. pressing 5 times `ctrl` + `c`). Mind that performing this too fast might result in the `KeyboardInterrupt` not being properly handled and corrupting the dataset!" ] }, { "cell_type": "code", "execution_count": null, "id": "e1b46c8b", "metadata": { "tags": [ "raises-exception" ] }, "outputs": [], "source": [ "class SlowGettable:\n", " \"\"\"A mock slow gettables.\"\"\"\n", "\n", " def __init__(self):\n", " self.name = \"slow\"\n", " self.label = \"Amplitude\"\n", " self.unit = \"V\"\n", "\n", " def get(self):\n", " \"\"\"Get method.\"\"\"\n", " time.sleep(1.0)\n", " if time_par() == 4:\n", " # This same exception rises when pressing `ctrl` + `c`\n", " # or the \"Stop kernel\" button is pressed in a Jupyter(Lab) notebook\n", " if sys.platform == \"win32\":\n", " # Emulating the kernel interrupt on windows might have side effects\n", " raise KeyboardInterrupt\n", " os.kill(os.getpid(), signal.SIGINT)\n", " return time_par()\n", "\n", "\n", "time_par.batched = False\n", "meas_ctrl.settables(time_par)\n", "meas_ctrl.setpoints(np.arange(10))\n", "meas_ctrl.gettables(SlowGettable())\n", "# Try interrupting me!\n", "dset = meas_ctrl.run(\"slow\")" ] }, { "cell_type": "code", "execution_count": null, "id": "a33155c4", "metadata": {}, "outputs": [], "source": [ "plotmon.main_QtPlot" ] } ], "metadata": { "file_format": "mystnb", "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.9.17" } }, "nbformat": 4, "nbformat_minor": 5 }