{ "cells": [ { "cell_type": "markdown", "metadata": {}, "source": [ "# Tutorial 3) Defining Complex Measurements\n", "\n", "This tutorial covers two approaches to building complex measurement routines in arbok-driver:\n", "\n", "1. **Dictionary-based composition** — defining the full sequence hierarchy from a nested dictionary in a single call\n", "2. **Experiment blueprints** — using pre-defined `Experiment` classes to standardise frequently used routines across devices and team members\n", "\n", "All code cells are intended to be run in succession. This tutorial assumes you have already set up a `Device` and `ArbokDriver` instance as shown in Tutorial 1." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## 0. Prerequisites\n", "\n", "Make sure you have completed Tutorial 1 (Initial Setup) and that the following objects are available in your session:" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "from arbok_driver import ArbokDriver, Device, Measurement\n", "from arbok_driver.examples.configurations.hardware import opx1000_config\n", "\n", "device = Device('device_8q', opx_config=opx1000_config, divider_config={})\n", "my_driver = ArbokDriver('my_driver', device)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "---\n", "\n", "## 1. The Problem: Growing Measurement Complexity\n", "\n", "Simple measurements like a charge stability map consist of one or two sub-sequences. As experiments become more sophisticated, a single measurement routine quickly accumulates many sub-sequences with interdependent parameters. A typical coherence measurement such as a **CPMG sequence** (Carr-Purcell-Meiboom-Gill) requires:\n", "\n", "- A state **initialization** stage (parity initialization)\n", "- A **transit** to the control point in gate voltage space\n", "- A **spin control** stage containing multiple nested operations:\n", " - An initial pi/2 rotation to place the qubit on the equator\n", " - The CPMG refocusing pulses\n", " - A final state projection\n", "- A **transit** back from the control point\n", "- A **readout** stage\n", "\n", "Adding these components one by one, managing their interdependencies, and ensuring consistent configuration across measurements quickly becomes error-prone, especially when the same routine needs to be deployed on a new device or by a new team member.\n", "\n", "arbok-driver provides two solutions to this problem, covered in the following sections." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "---\n", "\n", "## 2. Dictionary-Based Composition\n", "\n", "Rather than instantiating and adding sub-sequences individually, arbok-driver allows the entire hierarchy to be defined as a **nested dictionary**. Each entry specifies:\n", "\n", "- `sequence`: the `SubSequence` class to instantiate\n", "- `kwargs`: optional keyword arguments passed to the constructor\n", "- `sub_sequences`: a nested dictionary of further child sub-sequences\n", "- `config`: a device configuration dictionary (used for readout and initialization stages)\n", "\n", "The full hierarchy is then constructed in a single call to `add_subsequences_from_dict`." ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "from arbok_driver.examples.sequences import (\n", " ToControlPoint, FromControlPoint,\n", " Xstrict, Cpmg, StateProjection\n", ")\n", "from arbok_driver.examples.configurations.sequence import (\n", " parity_init_conf, parity_read_conf\n", ")\n", "\n", "cpmg_conf = {\n", " # Initialization stage — device-specific, provided via config\n", " 'parity_init': {'config': parity_init_conf},\n", " # Move to the control point in gate voltage space\n", " 'to_control': {'sequence': ToControlPoint},\n", " # Spin control stage — contains nested sub-sequences\n", " 'spin_control': {\n", " 'sub_sequences': {\n", " # Initial pi/2 rotation to place qubit on the Bloch sphere equator\n", " 'x_strict': {\n", " 'sequence': Xstrict,\n", " 'kwargs': {\n", " 'target_qubit': 'Q1',\n", " 'control_pulse': 'control_pi2'\n", " }\n", " },\n", " # CPMG refocusing pulses\n", " 'cpmg': {\n", " 'sequence': Cpmg,\n", " 'kwargs': {\n", " 'target_qubit': 'Q1',\n", " 'repetitions': 10, # number of refocusing pulses\n", " 't_equator_wait': int(1e3) # free evolution time in ns\n", " }\n", " },\n", " # Final state projection before readout\n", " 'state_projection': {\n", " 'sequence': StateProjection,\n", " 'kwargs': {'target_qubit': 'Q1'}\n", " }\n", " },\n", " # check_step_requirements enables asynchronous conditional execution\n", " # (see Tutorial 4: Asynchronous Measurements)\n", " 'kwargs': {'check_step_requirements': True}\n", " },\n", " # Move back from the control point\n", " 'from_control': {'sequence': FromControlPoint},\n", " # Readout stage — device-specific, provided via config\n", " 'parity_read': {'config': parity_read_conf}\n", "}\n", "\n", "# Instantiate the measurement and build the hierarchy from the dictionary\n", "cpmg_meas = Measurement(my_driver, 'cpmg_meas')\n", "cpmg_meas.add_subsequences_from_dict(cpmg_conf)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "The full parameter space of the measurement is now available through the QCoDeS interface. You can inspect it with:" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "cpmg_meas.print_readable_snapshot()" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "All parameters, including `t_equator_wait`, `repetitions`, and any voltage offsets defined in the initialization and readout configurations, are registered as QCoDeS parameters and can be swept or updated directly." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "---\n", "\n", "## 3. The Problem with Raw Dictionaries at Scale\n", "\n", "The dictionary approach works well for one-off measurements or exploratory work. However, in a team environment or across multiple devices, raw dictionaries introduce a maintenance problem:\n", "\n", "- The same dictionary may be copied and modified independently by different users\n", "- There is no guarantee that two instances of nominally the same measurement are actually identical\n", "- When a sub-sequence is updated or renamed, all copies of the dictionary need to be found and updated manually\n", "\n", "For measurements that are run routinely, arbok-driver provides the `Experiment` class to address this." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "---\n", "\n", "## 4. Experiment Blueprints\n", "\n", "An `Experiment` object encodes the **invariant structure** of a measurement — the sub-sequence hierarchy, class choices, and default parameters — while leaving the **device-specific components** as required arguments. This means:\n", "\n", "- The experimental logic is defined once and versioned centrally\n", "- Different devices only need to provide their own initialization and readout configurations\n", "- New team members can deploy the same routine on a new device by providing two arguments\n", "\n", "The `CpmgExperiment` blueprint encodes exactly the same structure as the dictionary above:" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "from arbok_driver.examples.experiments import CpmgExperiment\n", "\n", "# Create the identical CPMG measurement from a pre-defined blueprint\n", "# Only the device-specific components need to be provided\n", "cpmg_meas2 = my_driver.create_measurement_from_experiment(\n", " CpmgExperiment(\n", " target_qubit='Q1',\n", " parity_init=parity_init_conf,\n", " parity_read=parity_read_conf\n", " )\n", ")" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "The resulting `cpmg_meas2` object is identical to `cpmg_meas` in every respect. Verify this by comparing their parameter snapshots:" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "cpmg_meas2.print_readable_snapshot()" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "---\n", "\n", "## 5. Deploying on a Different Device\n", "\n", "The real benefit of the `Experiment` pattern becomes apparent when the same measurement needs to run on a different device. Only the configuration objects change while the sequence structure and all internal logic remain exactly as defined in the blueprint:" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "# Deploy the same CPMG routine on a different device\n", "# No changes to the experiment blueprint are needed\n", "# cpmg_meas_device2 = my_driver.create_measurement_from_experiment(\n", "# CpmgExperiment(\n", "# target_qubit='Q1',\n", "# init_config=parity_init_conf_device2,\n", "# read_config=parity_read_conf_device2\n", "# )\n", "# )" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "---\n", "\n", "## 6. Summary\n", "\n", "| Approach | Best suited for |\n", "|---|---|\n", "| `add_subsequences_from_dict` | Exploratory measurements, one-off routines, prototyping new sequences |\n", "| `Experiment` blueprint | Established routines, multi-device campaigns, team environments |\n", "\n", "Both approaches produce identical measurement objects and the same QUA program. The `Experiment` pattern adds a layer of standardisation that reduces the risk of inconsistencies and makes it easier for new users to get started without understanding the full internal structure of the sequence.\n", "\n", "**Next steps:**\n", "- Tutorial 4: Asynchronous Measurements — using step requirements for heralding and conditional execution\n", "- Tutorial 5: Parameter Input Streaming — adaptive measurements with the `GenericTuningInterface`" ] } ], "metadata": { "kernelspec": { "display_name": "Python 3", "language": "python", "name": "python3" }, "language_info": { "name": "python", "version": "3.10.0" } }, "nbformat": 4, "nbformat_minor": 4 }