{ "cells": [ { "cell_type": "markdown", "id": "title", "metadata": {}, "source": [ "# Developer Guide: Writing Readout Classes and Readout Sequences" ] }, { "cell_type": "markdown", "id": "intro", "metadata": {}, "source": [ "## 0. Overview" ] }, { "cell_type": "markdown", "id": "intro-body", "metadata": {}, "source": [ "This guide explains how to write your own `AbstractReadout` subclasses, `ReadSequence` subclasses, and how the configuration ties them together.\n", "\n", "After reading this guide you will be able to:\n", "\n", "- Write a minimal `AbstractReadout` that performs a hardware measurement and exposes its result as a `GettableParameter`\n", "- Write a processing-only `AbstractReadout` that consumes the result of an earlier readout\n", "- Add settable parameters to a readout class\n", "- Use temporary QUA variables inside a readout class without polluting the global scope\n", "- Write a `ReadSequence` that composes readout groups into a full measurement\n", "- Write the configuration dictionary that connects readout classes to signals and sequences\n", "\n", "The guide uses `DcAverage`, `Difference`, `Threshold` and `DcChoppedReadout` as concrete examples throughout." ] }, { "cell_type": "markdown", "id": "b6ce9483", "metadata": {}, "source": [ "Consider the block diagram while reading this guide to have a visual aid accompaning the content." ] }, { "cell_type": "code", "execution_count": 19, "id": "6e196483", "metadata": {}, "outputs": [ { "data": { "text/html": [ "" ], "text/plain": [ "" ] }, "metadata": {}, "output_type": "display_data" } ], "source": [ "from IPython.display import display, HTML\n", "\n", "display(HTML(''))" ] }, { "cell_type": "markdown", "id": "part1-heading", "metadata": {}, "source": [ "## 1. The `AbstractReadout` Contract" ] }, { "cell_type": "markdown", "id": "part1-body", "metadata": {}, "source": [ "Every readout class must inherit from `AbstractReadout`. The base class handles QCoDeS parameter registration, QUA variable declaration and stream management. As a developer you only need to implement three things:\n", "\n", "`__init__` receives and stores whatever the readout needs to do its job, calls the parent constructor, and then calls `_create_gettables`.\n", "\n", "`_create_gettables` calls `create_gettable` for every result this readout will produce, and calls `get_gettable_from_path` for every result it needs to consume from an earlier readout.\n", "\n", "`qua_measure` contains the QUA code that actually runs on the FPGA. It may only be called inside a `qua.program()` context.\n", "\n", "There is one optional override: `qua_declare_variables`, which is called before the main program body. Override it when you need QUA variables that are local to your readout and are not produced as gettables." ] }, { "cell_type": "markdown", "id": "part1-sig-heading", "metadata": {}, "source": [ "### 1.1 The mandatory `__init__` signature" ] }, { "cell_type": "markdown", "id": "part1-sig-body", "metadata": {}, "source": [ "The first five arguments of every `AbstractReadout.__init__` are fixed and must always be forwarded to `super().__init__` in this exact order. Any additional arguments specific to your readout come after these five." ] }, { "cell_type": "code", "execution_count": null, "id": "part1-sig-code", "metadata": {}, "outputs": [], "source": [ "from arbok_driver import AbstractReadout, ReadSequence, Signal\n", "\n", "class MyReadout(AbstractReadout):\n", " def __init__(\n", " self,\n", " name: str, # name of this readout entry, taken from the config key\n", " read_sequence: ReadSequence, # the ReadSequence that owns this readout\n", " signal: Signal, # the Signal this result belongs to\n", " save_results: bool, # whether to stream results to the host\n", " parameters: dict, # settable parameters dict from the config\n", " # --- your custom arguments go here ---\n", " qua_element: str,\n", " ):\n", " super().__init__(\n", " name=name,\n", " read_sequence=read_sequence,\n", " signal=signal,\n", " save_results=save_results,\n", " parameters=parameters,\n", " )\n", " self.qua_element = qua_element\n", " self._create_gettables()" ] }, { "cell_type": "markdown", "id": "part1-sig-note", "metadata": {}, "source": [ "The framework instantiates your class automatically when building the readout groups from the configuration. The five mandatory arguments are injected by the framework. Everything inside `kwargs` in the configuration entry is passed as additional keyword arguments. This is why `qua_element` above must appear in `kwargs` in the config.\n", "\n", "Always call `_create_gettables` at the end of `__init__`. The gettables must exist before `qua_declare_variables` is called by the framework at compile time." ] }, { "cell_type": "markdown", "id": "part2-heading", "metadata": {}, "source": [ "## 2. Creating and Consuming Gettables" ] }, { "cell_type": "markdown", "id": "part2-heading2", "metadata": {}, "source": [ "### 2.1 Producing a result with `create_gettable`" ] }, { "cell_type": "markdown", "id": "part2-body1", "metadata": {}, "source": [ "`create_gettable` allocates a QUA variable and registers it as a QCoDeS `GettableParameter` on the owning `Signal`. It takes two arguments, a name and a QUA variable type.\n", "\n", "The `gettable_name` you supply becomes the suffix of the full dotted path that the user sees and that other readouts can reference. For `DcAverage` the gettable is named after the readout entry itself (i.e. `self.name`), which gives paths like `parity_read.p1p2.ref__p1p2`. You may choose any name that is meaningful for your readout. `DcChoppedReadout` creates three gettables per sensor with suffixes `_read`, `_ref` and `_diff`. Check their source code to have a look." ] }, { "cell_type": "code", "execution_count": null, "id": "30200ef2", "metadata": {}, "outputs": [], "source": [ "from arbok_driver.examples.readout_classes import DcAverage, DcChoppedReadout" ] }, { "cell_type": "markdown", "id": "5139ea8d", "metadata": {}, "source": [ "The `var_type` argument accepts `qua.fixed` for a real-valued result, `int` for an integer or `bool` for a binary state." ] }, { "cell_type": "code", "execution_count": null, "id": "part2-code1", "metadata": {}, "outputs": [], "source": [ "from qm import qua\n", "\n", "# Inside _create_gettables:\n", "def _create_gettables(self):\n", " # A single real-valued result named after this readout entry\n", " self.current_gettable = self.create_gettable(\n", " gettable_name=self.name,\n", " var_type=qua.fixed,\n", " )\n", "\n", " # A binary classification result with a custom suffix\n", " self.state_gettable = self.create_gettable(\n", " gettable_name=f\"{self.name}_state\",\n", " var_type=bool,\n", " )" ] }, { "cell_type": "markdown", "id": "part2-heading3", "metadata": {}, "source": [ "### 2.2 Consuming an earlier result with `get_gettable_from_path`" ] }, { "cell_type": "markdown", "id": "part2-body2", "metadata": {}, "source": [ "When a readout needs to use the QUA variable produced by an earlier readout, call `get_gettable_from_path` with the full dotted path string. The framework resolves this to the actual `GettableParameter` object at construction time. You then access its QUA variable via `.qua_result_var` inside `qua_measure`.\n", "\n", "The dotted path follows the pattern `..__`. This path is what the user writes in the `kwargs` of the config, and what you pass to `get_gettable_from_path`." ] }, { "cell_type": "code", "execution_count": null, "id": "part2-code2", "metadata": {}, "outputs": [], "source": [ "# Inside _create_gettables of Difference:\n", "def _create_gettables(self):\n", " # Resolve the paths supplied in kwargs to actual GettableParameter objects\n", " self.minuend_gettable = self.get_gettable_from_path(self.minuend)\n", " self.subtrahend_gettable = self.get_gettable_from_path(self.subtrahend)\n", "\n", " # Produce a new gettable for the result of this readout\n", " self.difference_gettable = self.create_gettable(\n", " gettable_name=self.name,\n", " var_type=qua.fixed,\n", " )" ] }, { "cell_type": "markdown", "id": "part2-body3", "metadata": {}, "source": [ "There are two important rules here. First, the readout whose result you are consuming must appear in an earlier readout group in `qua_sequence`, so that its QUA variable is populated before your `qua_measure` runs. The framework does not enforce this ordering, it is the responsibility of the `ReadSequence` author. Second, you must not call `get_gettable_from_path` inside `qua_measure`. Path resolution happens once at construction time, not at compile time." ] }, { "cell_type": "markdown", "id": "part3-heading", "metadata": {}, "source": [ "## 3. Writing `qua_measure`" ] }, { "cell_type": "markdown", "id": "part3-body1", "metadata": {}, "source": [ "`qua_measure` is the only place where QUA commands should appear in your readout class. It is called by the `ReadSequence` when iterating over a readout group. The framework guarantees that all QUA variables have been declared before this method is called.\n", "\n", "Access the QUA variable of a gettable via `gettable.qua_result_var`. This is the raw `QuaVariable` object that QUA operates on. It is the authors responsibility to populate this variable such that arbok can save and process it." ] }, { "cell_type": "markdown", "id": "part3-heading2", "metadata": {}, "source": [ "### 3.1 A hardware measurement readout" ] }, { "cell_type": "markdown", "id": "part3-body2", "metadata": {}, "source": [ "`DcAverage` shows the minimal pattern for a readout that talks to hardware. A single `qua.measure` call integrates the signal and stores it in the gettable's QUA variable." ] }, { "cell_type": "code", "execution_count": null, "id": "part3-code1", "metadata": {}, "outputs": [], "source": [ "def qua_measure(self):\n", " outputs = [\n", " qua.integration.full('x_const', self.current_gettable.qua_result_var),\n", " ]\n", " qua.measure('measure', self.qua_element, *outputs)" ] }, { "cell_type": "markdown", "id": "part3-heading3", "metadata": {}, "source": [ "### 3.2 A processing-only readout" ] }, { "cell_type": "markdown", "id": "part3-body3", "metadata": {}, "source": [ "`Difference` and `Threshold` never call `qua.measure`. They perform pure arithmetic on QUA variables that earlier readouts have already populated. `Threshold` also reads from a `ParameterClass` attribute via `.qua`, which exposes the settable parameter as a QUA variable." ] }, { "cell_type": "code", "execution_count": null, "id": "part3-code2", "metadata": {}, "outputs": [], "source": [ "# Difference\n", "def qua_measure(self):\n", " qua.assign(\n", " self.difference_gettable.qua_result_var,\n", " self.minuend_gettable.qua_result_var - self.subtrahend_gettable.qua_result_var,\n", " )\n", "\n", "# Threshold\n", "def qua_measure(self):\n", " qua.assign(\n", " self.state_gettable.qua_result_var,\n", " self.current_gettable.qua_result_var > self.arbok_params.threshold.qua,\n", " )" ] }, { "cell_type": "markdown", "id": "part4-heading", "metadata": {}, "source": [ "## 4. Adding Settable Parameters" ] }, { "cell_type": "markdown", "id": "part4-body1", "metadata": {}, "source": [ "When a readout needs user-settable parameters such as a threshold voltage or a number of repetitions, declare them using a `ParameterClass` dataclass. This makes them visible in QCoDeS snapshots and accessible as QUA variables inside `qua_measure`.\n", "\n", "The pattern has three parts: define a frozen dataclass inheriting from `ParameterClass`, set `PARAMETER_CLASS` on your class, and annotate `arbok_params` with your dataclass type. After that, every field you declared is accessible as `self.arbok_params..qua` inside `qua_measure`." ] }, { "cell_type": "code", "execution_count": null, "id": "part4-code1", "metadata": {}, "outputs": [], "source": [ "from dataclasses import dataclass\n", "from arbok_driver import ParameterClass, GettableParameter\n", "from arbok_driver.parameter_types import Voltage, Int\n", "\n", "@dataclass(frozen=True)\n", "class MyReadoutParameters(ParameterClass):\n", " threshold: Voltage # will appear as a settable QCoDeS parameter\n", "\n", "class MyReadout(AbstractReadout):\n", " PARAMETER_CLASS = MyReadoutParameters\n", " arbok_params: MyReadoutParameters\n", " state_gettable: GettableParameter\n", " current_gettable: GettableParameter\n", "\n", " def qua_measure(self):\n", " # Access the threshold as a QUA variable\n", " qua.assign(\n", " self.state_gettable.qua_result_var,\n", " self.current_gettable.qua_result_var > self.arbok_params.threshold.qua,\n", " )" ] }, { "cell_type": "markdown", "id": "part4-body2", "metadata": {}, "source": [ "The values for these parameters are supplied in the `parameters` key of the readout's configuration entry. If a readout has no settable parameters, omit `PARAMETER_CLASS` and `arbok_params` entirely, as `DcAverage` and `Difference` do." ] }, { "cell_type": "markdown", "id": "part5-heading", "metadata": {}, "source": [ "## 5. Declaring Temporary QUA Variables" ] }, { "cell_type": "markdown", "id": "part5-body1", "metadata": {}, "source": [ "Sometimes a readout needs intermediate QUA variables that accumulate values across a loop but are not exposed as gettables. Declare these by overriding `qua_declare_variables`. Always call `super().qua_declare_variables()` first so that the base class can declare all gettable variables.\n", "\n", "`DcChoppedReadout` illustrates this. It runs a QUA `for_` loop over `n_chops` iterations, accumulating an average into temporary variables before writing the final result to the gettable variables." ] }, { "cell_type": "code", "execution_count": null, "id": "part5-code1", "metadata": {}, "outputs": [], "source": [ "def qua_declare_variables(self) -> None:\n", " # Always call super first\n", " super().qua_declare_variables()\n", " # Declare a loop counter\n", " self.qua_chop_nr = qua.declare(int)\n", " # Declare per-sensor accumulation buffers\n", " for sub_readout in self.readout_qua_elements:\n", " self.ref_temp_vars[sub_readout] = qua.declare(qua.fixed)\n", " self.read_temp_vars[sub_readout] = qua.declare(qua.fixed)" ] }, { "cell_type": "markdown", "id": "part5-body2", "metadata": {}, "source": [ "Variables declared here live in the top-level QUA variable scope. Give them names that are descriptive enough to avoid collisions if multiple instances of your class are used in the same measurement." ] }, { "cell_type": "markdown", "id": "part6-heading", "metadata": {}, "source": [ "## 6. A Readout That Handles Multiple Sensors Internally" ] }, { "cell_type": "markdown", "id": "part6-body1", "metadata": {}, "source": [ "The examples so far create one gettable per readout entry. `DcChoppedReadout` takes a different approach: it receives a dictionary `readout_qua_elements` mapping sensor names to QUA element names, and creates three gettables per sensor (`_ref`, `_read`, `_diff`) all within a single readout entry. This is appropriate when a group of measurements are tightly coupled and must run together inside a single loop.\n", "\n", "The key difference from simpler classes is that `_create_gettables` iterates over the sensors and calls `create_gettable` multiple times, storing the results in dictionaries keyed by sensor name." ] }, { "cell_type": "code", "execution_count": null, "id": "part6-code1", "metadata": {}, "outputs": [], "source": [ "def _create_gettables(self) -> None:\n", " for sub_readout, _ in self.readout_qua_elements.items():\n", " read = self.read_gettables[sub_readout] = self.create_gettable(\n", " gettable_name=f\"{sub_readout}_read\",\n", " var_type=qua.fixed,\n", " )\n", " ref = self.ref_gettables[sub_readout] = self.create_gettable(\n", " gettable_name=f\"{sub_readout}_ref\",\n", " var_type=qua.fixed,\n", " )\n", " diff = self.diff_gettables[sub_readout] = self.create_gettable(\n", " gettable_name=f\"{sub_readout}_diff\",\n", " var_type=qua.fixed,\n", " )" ] }, { "cell_type": "markdown", "id": "part6-body2", "metadata": {}, "source": [ "Inside `qua_measure`, the chopped readout runs a QUA `for_` loop, alternating between the `v_chop` and `v_home` voltage points and measuring at each step. After the loop it normalises the accumulated values and assigns them to the gettable variables. The important point is that all of this is self-contained within one `qua_measure` call. The `ReadSequence` calls it once and the entire chopped averaging protocol executes on the FPGA." ] }, { "cell_type": "markdown", "id": "part7-heading", "metadata": {}, "source": [ "## 7. Writing a `ReadSequence`" ] }, { "cell_type": "markdown", "id": "part7-body1", "metadata": {}, "source": [ "A `ReadSequence` is a `SubSequence` that knows about signals and readout groups. Its job is to define the physical timeline of the measurement: when gates move, when readout groups fire, and when processing groups run. It does not need to know which specific sensors or signals are present; that is entirely determined by the configuration.\n", "\n", "A `ReadSequence` subclass must provide `__init__` and `qua_sequence`. All additional hooks that are available to `SubSequence` are available:\n", "- `qua_declare`\n", "- `qua_before_sweep`\n", "- `qua_before_sequence`\n", "- `qua_after_sequence`." ] }, { "cell_type": "markdown", "id": "part7-heading2", "metadata": {}, "source": [ "### 7.1 `__init__`" ] }, { "cell_type": "markdown", "id": "part7-body2", "metadata": {}, "source": [ "The constructor forwards `parent`, `name` and `sequence_config` to the parent and performs any one-time setup. A common pattern is to collect the full list of QUA elements to be used in `qua.align` and `qua.wait` calls." ] }, { "cell_type": "code", "execution_count": null, "id": "part7-code1", "metadata": {}, "outputs": [], "source": [ "from arbok_driver import ReadSequence, SequenceBase\n", "\n", "class MyReadSequence(ReadSequence):\n", " def __init__(self, parent: SequenceBase, name: str, sequence_config: dict):\n", " super().__init__(\n", " parent=parent,\n", " name=name,\n", " sequence_config=sequence_config,\n", " )" ] }, { "cell_type": "markdown", "id": "part7-heading3", "metadata": {}, "source": [ "### 7.2 `qua_sequence`" ] }, { "cell_type": "markdown", "id": "part7-body3", "metadata": {}, "source": [ "This is where the physical measurement is described. The pattern for invoking a readout group is always the same: iterate over `self.readout_groups[group_name]` and call `qua_measure()` on each entry. Because every entry in a group is an `AbstractReadout`, this loop works regardless of how many sensors or signals are present in the configuration.\n", "\n", "The ordering of group invocations defines the processing chain. Hardware measurement groups must fire before processing groups that consume their results." ] }, { "cell_type": "code", "execution_count": null, "id": "part7-code2", "metadata": {}, "outputs": [], "source": [ "from arbok_driver import arbok\n", "\n", "def qua_sequence(self):\n", " qua.align(*self.elements)\n", "\n", " # Move gates to the reference point and take the reference measurement\n", " arbok.ramp(\n", " elements=self.arbok_params.gate_elements.get(),\n", " target=self.arbok_params.v_reference,\n", " operation='unit_ramp',\n", " )\n", " qua.align(*self.elements)\n", " for _, readout in self.readout_groups['ref'].items():\n", " readout.qua_measure()\n", "\n", " # Move gates to the read point and take the read measurement\n", " arbok.ramp(\n", " elements=self.arbok_params.gate_elements.get(),\n", " reference=self.arbok_params.v_reference,\n", " target=self.arbok_params.v_read,\n", " operation='unit_ramp',\n", " )\n", " qua.align(*self.elements)\n", " for _, readout in self.readout_groups['read'].items():\n", " readout.qua_measure()\n", "\n", " # On-FPGA processing: difference then threshold\n", " for _, readout in self.readout_groups['diff'].items():\n", " readout.qua_measure()\n", " for _, readout in self.readout_groups['state'].items():\n", " readout.qua_measure()\n", "\n", " qua.align()\n", "\n", " # Optional feedback group\n", " if 'set_feedback' in self.readout_groups:\n", " for _, readout in self.readout_groups['set_feedback'].items():\n", " readout.qua_measure()" ] }, { "cell_type": "markdown", "id": "part7-body4", "metadata": {}, "source": [ "To make a readout group optional (present only when the user includes it in the configuration) guard the iteration with a membership check." ] }, { "cell_type": "markdown", "id": "part7-heading4", "metadata": {}, "source": [ "### 7.3 Hooks like `qua_declare` and `qua_after_sequence`" ] }, { "cell_type": "markdown", "id": "part7-body5", "metadata": {}, "source": [ "`qua_declare` is called before the program body. Override it to declare QUA variables that belong to the sequence rather than to a specific readout. Always call `super().qua_declare()` to ensure base class declarations run.\n", "\n", "`qua_after_sequence` runs at the end of each shot. Its primary use is to trigger stream saving via `self.measurement.qua_check_step_requirements`. You do not need to call this manually for each gettable; the framework saves all registered gettables when this hook fires." ] }, { "cell_type": "code", "execution_count": null, "id": "part7-code4", "metadata": {}, "outputs": [], "source": [ " def qua_declare(self):\n", " return super().qua_declare()\n", "\n", " def qua_after_sequence(self):\n", " self.measurement.qua_check_step_requirements(self.save_variables)\n", "\n", " def save_variables(self):\n", " for _, readout in self.abstract_readouts.items():\n", " readout.qua_save_variables()" ] }, { "cell_type": "markdown", "id": "part8-heading", "metadata": {}, "source": [ "## 8. Writing the Configuration" ] }, { "cell_type": "markdown", "id": "part8-body1", "metadata": {}, "source": [ "The configuration dictionary is the single place where readout classes, signals and parameter values are connected. Its four top-level keys are `sequence`, `parameters`, `signals` and `readout_groups`." ] }, { "cell_type": "markdown", "id": "part8-heading2", "metadata": {}, "source": [ "### 8.1 Anatomy of a readout group entry" ] }, { "cell_type": "markdown", "id": "part8-body2", "metadata": {}, "source": [ "Each entry in a readout group is a dictionary with the following keys.\n", "\n", "`readout_class` is the `AbstractReadout` subclass to instantiate for this entry.\n", "\n", "`signal` is the name of the signal (from the `signals` list) that owns the result.\n", "\n", "`kwargs` is a dictionary of keyword arguments forwarded to the readout class constructor after the five mandatory arguments. Hardware element names and dotted gettable paths go here.\n", "\n", "`params` or `parameters` (the configuration accepts both) is a dictionary of settable parameter definitions. These are only required when the readout class declares a `PARAMETER_CLASS`.\n", "\n", "The key under which the entry appears in the group dictionary becomes both the `name` argument passed to the readout constructor and the suffix in the gettable path." ] }, { "cell_type": "code", "execution_count": null, "id": "part8-code1", "metadata": {}, "outputs": [], "source": [ "from arbok_driver.examples.readout_classes import DcAverage\n", "# Annotated example of a single readout group entry\n", "{\n", " 'p1p2': { # this string becomes `name` and the path suffix\n", " 'readout_class': DcAverage, # AbstractReadout subclass\n", " 'signal': 'p1p2', # must match an entry in 'signals'\n", " 'kwargs': {\n", " 'qua_element': 'SET1', # forwarded as keyword argument to __init__\n", " },\n", " 'params': {\n", " 'some_param': {'type': Int, 'value': 1}, # only if PARAMETER_CLASS is set\n", " },\n", " },\n", "}" ] }, { "cell_type": "markdown", "id": "part8-heading3", "metadata": {}, "source": [ "### 8.2 Writing gettable paths in `kwargs`" ] }, { "cell_type": "markdown", "id": "part8-body3", "metadata": {}, "source": [ "When a readout consumes the result of an earlier readout, the path is supplied as a string in `kwargs`. The full path is:\n", "\n", "```\n", "..__\n", "```\n", "\n", "You need the `readsequence_name` because multiple `ReadSequence`s can coexist in a `Measurement`. If you skip the `readsequence_name` only signals within the own readout sequence are being considered. The `signal_name` narrows the search to a specific signal. The final segment `__` identifies the specific gettable within that signal. For `DcChoppedReadout`, which creates multiple gettables per entry, the suffix also includes the per-sensor suffix chosen in `_create_gettables`, for example `chop__p1p2_diff`." ] }, { "cell_type": "code", "execution_count": null, "id": "part8-code2", "metadata": {}, "outputs": [], "source": [ "from arbok_driver.examples.readout_classes import Difference, Threshold\n", "readout_sequence_config = {\n", "'parameters': {},\n", "'signals': ['p1p2'],\n", "'readout_groups': {\n", " # A Difference entry consuming two DcAverage results\n", " 'diff': {\n", " 'p1p2': {\n", " 'readout_class': Difference,\n", " 'signal': 'p1p2',\n", " 'kwargs': {\n", " 'minuend': 'my_sequence.p1p2.ref__p1p2', # group=ref, entry=p1p2\n", " 'subtrahend': 'my_sequence.p1p2.read__p1p2', # group=read, entry=p1p2\n", " },\n", " },\n", " },\n", "\n", " # A Threshold entry consuming the Difference result\n", " 'state': {\n", " 'p1p2': {\n", " 'readout_class': Threshold,\n", " 'signal': 'p1p2',\n", " 'kwargs': {\n", " 'charge_readout': 'my_sequence.p1p2.diff__p1p2', # group=diff, entry=p1p2\n", " },\n", " 'parameters': {\n", " 'threshold': {'type': Voltage, 'value': 0.001}\n", " },\n", " },\n", " }\n", " }\n", "}" ] }, { "cell_type": "markdown", "id": "part9-heading", "metadata": {}, "source": [ "## 9. Checklist for a New Readout Class" ] }, { "cell_type": "markdown", "id": "part9-body", "metadata": {}, "source": [ "Use this checklist when writing a new `AbstractReadout` subclass.\n", "\n", "The `__init__` signature starts with exactly `name, read_sequence, signal, save_results, parameters` followed by any custom arguments. `super().__init__` is called with those five arguments. `_create_gettables` is the last call in `__init__`.\n", "\n", "`_create_gettables` calls `create_gettable` for every result this readout produces. It calls `get_gettable_from_path` for every result it consumes from an earlier readout. No QUA code appears here.\n", "\n", "`qua_measure` uses only `qua.*` calls and accesses gettable variables via `.qua_result_var`. It does not call `create_gettable` or `get_gettable_from_path`.\n", "\n", "If the class needs settable parameters, a frozen `ParameterClass` dataclass is defined, assigned to `PARAMETER_CLASS`, and annotated as `arbok_params`. Values are accessed as `self.arbok_params..qua` inside `qua_measure`.\n", "\n", "If the class needs temporary QUA variables, `qua_declare_variables` is overridden and `super().qua_declare_variables()` is called first.\n", "\n", "The readout group name used in `qua_sequence` to invoke this class matches a key in the `readout_groups` section of the configuration.\n", "\n", "Any gettable path string passed via `kwargs` in the configuration matches the full path `..__` of a gettable produced by an earlier group." ] }, { "cell_type": "markdown", "id": "part10-heading", "metadata": {}, "source": [ "## 10. Complete Minimal Example" ] }, { "cell_type": "markdown", "id": "part10-body1", "metadata": {}, "source": [ "The following cells bring everything together as the smallest possible working example: a single hardware readout followed by a threshold classification." ] }, { "cell_type": "code", "execution_count": 1, "id": "part10-code1", "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "2026-04-16 01:37:20,556 - qm - INFO - Starting session: 7c17f2d0-7b2f-4790-9dd8-758496fe78e8\n" ] } ], "source": [ "# --- Minimal hardware readout class ---\n", "from arbok_driver import AbstractReadout\n", "from qm import qua\n", "\n", "class DcAverage(AbstractReadout):\n", " def __init__(\n", " self,name, read_sequence, signal, save_results, parameters, qua_element):\n", " super().__init__(\n", " name=name,\n", " read_sequence=read_sequence,\n", " signal=signal,\n", " save_results=save_results,\n", " parameters=parameters\n", " )\n", " self.qua_element = qua_element\n", " self._create_gettables()\n", "\n", " def _create_gettables(self):\n", " self.current_gettable = self.create_gettable(\n", " gettable_name=self.name, var_type=qua.fixed)\n", "\n", " def qua_measure(self):\n", " qua.measure(\n", " 'measure', self.qua_element,\n", " qua.integration.full('x_const', self.current_gettable.qua_result_var),\n", " )" ] }, { "cell_type": "code", "execution_count": 2, "id": "part10-code2", "metadata": {}, "outputs": [], "source": [ "# --- Minimal ReadSequence ---\n", "from arbok_driver import ReadSequence, SequenceBase\n", "\n", "class MinimalReadout(ReadSequence):\n", " def __init__(self, parent: SequenceBase, name: str, sequence_config: dict):\n", " super().__init__(parent=parent, name=name, sequence_config=sequence_config)\n", "\n", " def qua_sequence(self):\n", " qua.align(*self.elements)\n", " for _, readout in self.readout_groups['measure'].items():\n", " readout.qua_measure()\n", " for _, readout in self.readout_groups['state'].items():\n", " readout.qua_measure()\n", " qua.align()\n", "\n", " def qua_after_sequence(self):\n", " self.measurement.qua_check_step_requirements(self.save_variables)\n", "\n", " def save_variables(self):\n", " for _, readout in self.abstract_readouts.items():\n", " readout.qua_save_variables()" ] }, { "cell_type": "code", "execution_count": 3, "id": "part10-code3", "metadata": {}, "outputs": [], "source": [ "# --- Configuration ---\n", "from arbok_driver.parameter_types import Voltage, List\n", "from arbok_driver.examples.readout_classes import Threshold\n", "\n", "minimal_conf = {\n", " 'sequence': MinimalReadout,\n", " 'parameters': {\n", " 'gate_elements': {'type': List, 'value': ['P1', 'P2']},\n", " 'readout_elements': {'type': List, 'value': ['SET1']},\n", " },\n", " 'signals': ['q1'],\n", " 'readout_groups': {\n", " 'measure': {\n", " 'q1': {\n", " 'readout_class': DcAverage,\n", " 'signal': 'q1',\n", " 'kwargs': {'qua_element': 'SET1'},\n", " },\n", " },\n", " 'state': {\n", " 'q1': {\n", " 'readout_class': Threshold,\n", " 'signal': 'q1',\n", " 'kwargs': {'charge_readout': 'minimal_readout.q1.measure__q1'},\n", " 'parameters': {'threshold': {'type': Voltage, 'value': 0.001}},\n", " },\n", " },\n", " },\n", "}" ] }, { "cell_type": "code", "execution_count": 4, "id": "part10-code4", "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "qm_driver_mock_measurement_minimal_readout:\n", "\tparameter value\n", "--------------------------------------------------------------------------------\n", "gate_elements :\t['P1', 'P2'] (N/A)\n", "measure__q1 :\tNot available \n", "readout_elements :\t['SET1'] (N/A)\n", "state__q1 :\tNot available \n", "state__q1__threshold :\t0.001 (V)\n" ] }, { "data": { "text/html": [ "
\n",
       "# Single QUA script generated at 2026-04-16 01:37:22.412420\n",
       "# QUA library version: 1.2.5\n",
       "\n",
       "\n",
       "from qm import CompilerOptionArguments\n",
       "from qm.qua import *\n",
       "\n",
       "with program() as prog:\n",
       "    v1 = declare(int, value=0)\n",
       "    v2 = declare(fixed, )\n",
       "    v3 = declare(bool, )\n",
       "    with infinite_loop_():\n",
       "        pause()\n",
       "        assign(v1, 0)\n",
       "        align(\"P1\", \"P2\", \"P3\", \"P4\", \"P5\", \"P6\", \"P7\", \"P8\", \"P9\", \"P10\", \"P11\", \"P12\", \"J1\", \"J2\", \"J3\", \"J4\", \n",
       "\"J5\", \"J6\", \"J7\", \"J8\", \"J9\", \"J10\", \"J11\", \"SET1\", \"SET2\", \"SET3\", \"SET4\", \"SET5\", \"SET6\", \"qe1\", \"Q1\", \"Y1\", \n",
       "\"Q2\", \"Y2\", \"Q3\", \"Q4\", \"Q5\", \"Q6\", \"Q7\", \"Q8\")\n",
       "        measure(\"measure\", \"SET1\", integration.full(\"x_const\", v2, \"\"))\n",
       "        assign(v3, (v2>0.001))\n",
       "        align()\n",
       "        r2 = declare_stream()\n",
       "        save(v2, r2)\n",
       "        r3 = declare_stream()\n",
       "        save(v3, r3)\n",
       "        align()\n",
       "        assign(v1, (v1+1))\n",
       "        r1 = declare_stream()\n",
       "        save(v1, r1)\n",
       "        align()\n",
       "    with stream_processing():\n",
       "        r1.buffer(1).save(\"qm_driver_mock_measurement_shots\")\n",
       "        r2.buffer(1).save(\"qm_driver_mock_measurement_minimal_readout_measure__q1\")\n",
       "        r3.buffer(1).save(\"qm_driver_mock_measurement_minimal_readout_state__q1\")\n",
       "\n",
       "config = None\n",
       "\n",
       "loaded_config = None\n",
       "\n",
       "\n",
       "
\n" ], "text/plain": [ "\n", "# Single QUA script generated at \u001b[1;36m2026\u001b[0m-\u001b[1;36m04\u001b[0m-\u001b[1;36m16\u001b[0m \u001b[1;92m01:37:22\u001b[0m.\u001b[1;36m412420\u001b[0m\n", "# QUA library version: \u001b[1;36m1.2\u001b[0m.\u001b[1;36m5\u001b[0m\n", "\n", "\n", "from qm import CompilerOptionArguments\n", "from qm.qua import *\n", "\n", "with \u001b[1;35mprogram\u001b[0m\u001b[1m(\u001b[0m\u001b[1m)\u001b[0m as prog:\n", " v1 = \u001b[1;35mdeclare\u001b[0m\u001b[1m(\u001b[0mint, \u001b[33mvalue\u001b[0m=\u001b[1;36m0\u001b[0m\u001b[1m)\u001b[0m\n", " v2 = \u001b[1;35mdeclare\u001b[0m\u001b[1m(\u001b[0mfixed, \u001b[1m)\u001b[0m\n", " v3 = \u001b[1;35mdeclare\u001b[0m\u001b[1m(\u001b[0mbool, \u001b[1m)\u001b[0m\n", " with \u001b[1;35minfinite_loop_\u001b[0m\u001b[1m(\u001b[0m\u001b[1m)\u001b[0m:\n", " \u001b[1;35mpause\u001b[0m\u001b[1m(\u001b[0m\u001b[1m)\u001b[0m\n", " \u001b[1;35massign\u001b[0m\u001b[1m(\u001b[0mv1, \u001b[1;36m0\u001b[0m\u001b[1m)\u001b[0m\n", " \u001b[1;35malign\u001b[0m\u001b[1m(\u001b[0m\u001b[32m\"P1\"\u001b[0m, \u001b[32m\"P2\"\u001b[0m, \u001b[32m\"P3\"\u001b[0m, \u001b[32m\"P4\"\u001b[0m, \u001b[32m\"P5\"\u001b[0m, \u001b[32m\"P6\"\u001b[0m, \u001b[32m\"P7\"\u001b[0m, \u001b[32m\"P8\"\u001b[0m, \u001b[32m\"P9\"\u001b[0m, \u001b[32m\"P10\"\u001b[0m, \u001b[32m\"P11\"\u001b[0m, \u001b[32m\"P12\"\u001b[0m, \u001b[32m\"J1\"\u001b[0m, \u001b[32m\"J2\"\u001b[0m, \u001b[32m\"J3\"\u001b[0m, \u001b[32m\"J4\"\u001b[0m, \n", "\u001b[32m\"J5\"\u001b[0m, \u001b[32m\"J6\"\u001b[0m, \u001b[32m\"J7\"\u001b[0m, \u001b[32m\"J8\"\u001b[0m, \u001b[32m\"J9\"\u001b[0m, \u001b[32m\"J10\"\u001b[0m, \u001b[32m\"J11\"\u001b[0m, \u001b[32m\"SET1\"\u001b[0m, \u001b[32m\"SET2\"\u001b[0m, \u001b[32m\"SET3\"\u001b[0m, \u001b[32m\"SET4\"\u001b[0m, \u001b[32m\"SET5\"\u001b[0m, \u001b[32m\"SET6\"\u001b[0m, \u001b[32m\"qe1\"\u001b[0m, \u001b[32m\"Q1\"\u001b[0m, \u001b[32m\"Y1\"\u001b[0m, \n", "\u001b[32m\"Q2\"\u001b[0m, \u001b[32m\"Y2\"\u001b[0m, \u001b[32m\"Q3\"\u001b[0m, \u001b[32m\"Q4\"\u001b[0m, \u001b[32m\"Q5\"\u001b[0m, \u001b[32m\"Q6\"\u001b[0m, \u001b[32m\"Q7\"\u001b[0m, \u001b[32m\"Q8\"\u001b[0m\u001b[1m)\u001b[0m\n", " \u001b[1;35mmeasure\u001b[0m\u001b[1m(\u001b[0m\u001b[32m\"measure\"\u001b[0m, \u001b[32m\"SET1\"\u001b[0m, \u001b[1;35mintegration.full\u001b[0m\u001b[1m(\u001b[0m\u001b[32m\"x_const\"\u001b[0m, v2, \u001b[32m\"\"\u001b[0m\u001b[1m)\u001b[0m\u001b[1m)\u001b[0m\n", " \u001b[1;35massign\u001b[0m\u001b[1m(\u001b[0mv3, \u001b[1m(\u001b[0mv2>\u001b[1;36m0.001\u001b[0m\u001b[1m)\u001b[0m\u001b[1m)\u001b[0m\n", " \u001b[1;35malign\u001b[0m\u001b[1m(\u001b[0m\u001b[1m)\u001b[0m\n", " r2 = \u001b[1;35mdeclare_stream\u001b[0m\u001b[1m(\u001b[0m\u001b[1m)\u001b[0m\n", " \u001b[1;35msave\u001b[0m\u001b[1m(\u001b[0mv2, r2\u001b[1m)\u001b[0m\n", " r3 = \u001b[1;35mdeclare_stream\u001b[0m\u001b[1m(\u001b[0m\u001b[1m)\u001b[0m\n", " \u001b[1;35msave\u001b[0m\u001b[1m(\u001b[0mv3, r3\u001b[1m)\u001b[0m\n", " \u001b[1;35malign\u001b[0m\u001b[1m(\u001b[0m\u001b[1m)\u001b[0m\n", " \u001b[1;35massign\u001b[0m\u001b[1m(\u001b[0mv1, \u001b[1m(\u001b[0mv1+\u001b[1;36m1\u001b[0m\u001b[1m)\u001b[0m\u001b[1m)\u001b[0m\n", " r1 = \u001b[1;35mdeclare_stream\u001b[0m\u001b[1m(\u001b[0m\u001b[1m)\u001b[0m\n", " \u001b[1;35msave\u001b[0m\u001b[1m(\u001b[0mv1, r1\u001b[1m)\u001b[0m\n", " \u001b[1;35malign\u001b[0m\u001b[1m(\u001b[0m\u001b[1m)\u001b[0m\n", " with \u001b[1;35mstream_processing\u001b[0m\u001b[1m(\u001b[0m\u001b[1m)\u001b[0m:\n", " \u001b[1;35mr1.buffer\u001b[0m\u001b[1m(\u001b[0m\u001b[1;36m1\u001b[0m\u001b[1m)\u001b[0m\u001b[1;35m.save\u001b[0m\u001b[1m(\u001b[0m\u001b[32m\"qm_driver_mock_measurement_shots\"\u001b[0m\u001b[1m)\u001b[0m\n", " \u001b[1;35mr2.buffer\u001b[0m\u001b[1m(\u001b[0m\u001b[1;36m1\u001b[0m\u001b[1m)\u001b[0m\u001b[1;35m.save\u001b[0m\u001b[1m(\u001b[0m\u001b[32m\"qm_driver_mock_measurement_minimal_readout_measure__q1\"\u001b[0m\u001b[1m)\u001b[0m\n", " \u001b[1;35mr3.buffer\u001b[0m\u001b[1m(\u001b[0m\u001b[1;36m1\u001b[0m\u001b[1m)\u001b[0m\u001b[1;35m.save\u001b[0m\u001b[1m(\u001b[0m\u001b[32m\"qm_driver_mock_measurement_minimal_readout_state__q1\"\u001b[0m\u001b[1m)\u001b[0m\n", "\n", "config = \u001b[3;35mNone\u001b[0m\n", "\n", "loaded_config = \u001b[3;35mNone\u001b[0m\n", "\n", "\n" ] }, "metadata": {}, "output_type": "display_data" } ], "source": [ "# --- Instantiation and inspection ---\n", "from rich import print as rprint\n", "from arbok_driver import ArbokDriver, Device, Measurement\n", "from arbok_driver.examples.configurations.hardware import (\n", " opx1000_config, divider_config)\n", "\n", "mock_device = Device(\n", " name='mock_device',\n", " opx_config=opx1000_config,\n", " divider_config=divider_config,\n", ")\n", "qm_driver = ArbokDriver('qm_driver', mock_device)\n", "mock_measurement = Measurement(qm_driver, 'mock_measurement')\n", "\n", "minimal_readout = MinimalReadout(\n", " parent=mock_measurement,\n", " name='minimal_readout',\n", " sequence_config=minimal_conf,\n", ")\n", "\n", "minimal_readout.print_readable_snapshot()\n", "rprint(mock_measurement.get_qua_program_as_str())" ] } ], "metadata": { "kernelspec": { "display_name": "arbok-driver", "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.13.5" } }, "nbformat": 4, "nbformat_minor": 5 }