{ "cells": [ { "cell_type": "markdown", "id": "title", "metadata": {}, "source": [ "# Developer Guide: Writing a SubSequence" ] }, { "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 `SubSequence` subclass from scratch.\n", "\n", "After reading this guide you will be able to:\n", "\n", "- Define the `ParameterClass` dataclass that declares which parameters your sequence needs\n", "- Write a minimal `SubSequence` that plays QUA code using those parameters\n", "- Use element-wise `ParameterMap` parameters to scale a sequence to multiple gates\n", "- Override the lifecycle hooks (`qua_declare`, `qua_before_sweep`, `qua_before_sequence`, `qua_after_sequence`) to structure more complex sequences\n", "- Compose sequences by nesting one `SubSequence` inside another\n", "- Write the configuration dictionary that connects parameter values to the sequence class\n", "\n", "The guide uses `SquarePulse`, `SquarePulseScalable`, `ToControlPoint` and `Cpmg` as concrete examples throughout." ] }, { "cell_type": "markdown", "id": "arch-heading", "metadata": {}, "source": [ "## 1. The `SubSequence` Contract" ] }, { "cell_type": "markdown", "id": "arch-body", "metadata": {}, "source": [ "A `SubSequence` is a self-contained, device-agnostic snippet of QUA code. When you subclass it you are responsible for two things:\n", "\n", "**`PARAMETER_CLASS`** — a frozen dataclass that inherits from `ParameterClass` and declares all parameters your sequence needs. The framework reads this at construction time, matches the field names against the configuration, and populates `self.arbok_params` with the corresponding QCoDeS parameters.\n", "\n", "**`qua_sequence`** — the method that contains the actual QUA commands. It is called once per shot (inside the infinite loop arbok wraps around every measurement). Parameters are accessed as `self.arbok_params..qua`, which returns either a Python literal or a QUA variable depending on whether that parameter is being swept.\n", "\n", "Everything else — QCoDeS parameter registration, sweep variable declaration, stream management, the infinite loop — is handled by the framework." ] }, { "cell_type": "markdown", "id": "minimal-heading", "metadata": {}, "source": [ "## 2. A Minimal SubSequence" ] }, { "cell_type": "markdown", "id": "minimal-body", "metadata": {}, "source": [ "### 2.1 The ParameterClass" ] }, { "cell_type": "markdown", "id": "param-class-body", "metadata": {}, "source": [ "Every `SubSequence` subclass must declare a `PARAMETER_CLASS` attribute that points to a frozen dataclass inheriting from `ParameterClass`. Each field in the dataclass is one parameter the sequence needs. The field type determines the QCoDeS unit, the QUA variable type, and any validators.\n", "\n", "The available built-in parameter types are:\n", "\n", "| Type | QUA type | Unit | Use for |\n", "|---|---|---|---|\n", "| `Time` | `int` | `s` (internally cycles) | Wait durations, pulse widths |\n", "| `Voltage` | `fixed` | `V` | Gate voltages, amplitudes |\n", "| `Amplitude` | `fixed` | — | Dimensionless amplitude scalings |\n", "| `Frequency` | `int` | `Hz` | any frequencies |\n", "| `Int` | `int` | — | Loop counters, repetitions, etc |\n", "| `String` | `str` | — | QUA element names |\n", "| `List` | — | — | Lists of element names |\n", "| `ParameterMap[str, T]` | — | — | Per-element voltages or values |" ] }, { "cell_type": "code", "execution_count": null, "id": "param-class-code", "metadata": {}, "outputs": [], "source": [ "from dataclasses import dataclass\n", "from arbok_driver import ParameterClass\n", "from arbok_driver.parameter_types import Amplitude, String, Time\n", "\n", "@dataclass(frozen=True)\n", "class SquarePulseParameters(ParameterClass):\n", " amplitude: Amplitude\n", " element: String\n", " t_ramp: Time\n", " t_square_pulse: Time" ] }, { "cell_type": "markdown", "id": "minimal-subseq-body", "metadata": {}, "source": [ "### 2.2 The SubSequence class" ] }, { "cell_type": "markdown", "id": "minimal-subseq-body2", "metadata": {}, "source": [ "With the `ParameterClass` defined, the `SubSequence` subclass only needs to set `PARAMETER_CLASS`, annotate `arbok_params`, and implement `qua_sequence`. Everything else is inherited." ] }, { "cell_type": "code", "execution_count": null, "id": "minimal-subseq-code", "metadata": {}, "outputs": [], "source": [ "from qm import qua\n", "from arbok_driver import SubSequence\n", "\n", "class SquarePulse(SubSequence):\n", " \"\"\"\n", " Plays a square pulse: ramp up → wait → ramp down on a single gate element.\n", " \"\"\"\n", " PARAMETER_CLASS = SquarePulseParameters\n", " arbok_params: SquarePulseParameters\n", "\n", " def qua_sequence(self):\n", " \"\"\"Macro played within the qua.program() context.\"\"\"\n", " qua.align()\n", " qua.play(\n", " pulse='ramp' * qua.amp(self.arbok_params.amplitude.qua),\n", " element=self.arbok_params.element.qua,\n", " duration=self.arbok_params.t_ramp.qua,\n", " )\n", " qua.wait(\n", " self.arbok_params.t_square_pulse.qua,\n", " self.arbok_params.element.qua,\n", " )\n", " qua.play(\n", " pulse='ramp' * qua.amp(self.arbok_params.amplitude.qua),\n", " element=self.arbok_params.element.qua,\n", " duration=self.arbok_params.t_ramp.qua,\n", " )" ] }, { "cell_type": "markdown", "id": "minimal-conf-heading", "metadata": {}, "source": [ "### 2.3 The configuration dictionary" ] }, { "cell_type": "markdown", "id": "minimal-conf-body", "metadata": {}, "source": [ "The configuration dictionary is the only place where device-specific values live. It is kept separate from the class so the same `SquarePulse` class can be reused on any device by supplying a different config.\n", "\n", "Each entry under `parameters` must contain a `type` key (one of the parameter types listed above) and a `value` key. An optional `label` key sets the axis label used in data saving." ] }, { "cell_type": "code", "execution_count": null, "id": "minimal-conf-code", "metadata": {}, "outputs": [], "source": [ "from arbok_driver.parameter_types import Amplitude, String, Time\n", "\n", "square_conf = {\n", " 'parameters': {\n", " 'amplitude': {'type': Amplitude, 'value': 0.5},\n", " 'element': {'type': String, 'value': 'gate_1'},\n", " 't_ramp': {'type': Time, 'value': 20},\n", " 't_square_pulse': {'type': Time, 'value': 100},\n", " }\n", "}" ] }, { "cell_type": "markdown", "id": "instantiation-heading", "metadata": {}, "source": [ "### 2.4 Instantiation and inspection" ] }, { "cell_type": "markdown", "id": "instantiation-body", "metadata": {}, "source": [ "A `SubSequence` always takes a parent (`Measurement` or another `SubSequence`), a name, and the configuration dict. After construction all parameters are accessible as QCoDeS parameters on the instance." ] }, { "cell_type": "code", "execution_count": null, "id": "instantiation-code", "metadata": {}, "outputs": [], "source": [ "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", "square_pulse = SquarePulse(mock_measurement, 'square_pulse', square_conf)\n", "square_pulse.print_readable_snapshot()" ] }, { "cell_type": "code", "execution_count": null, "id": "qua-program-code", "metadata": {}, "outputs": [], "source": [ "rprint(mock_measurement.get_qua_program_as_str())" ] }, { "cell_type": "markdown", "id": "qua-note", "metadata": {}, "source": [ "The compiled output inlines the values directly. Notice the `infinite_loop_` and `pause` that arbok wraps around every sequence — those are always added by the framework and should not be written inside `qua_sequence`." ] }, { "cell_type": "markdown", "id": "parammap-heading", "metadata": {}, "source": [ "## 3. Scaling to Multiple Gates with `ParameterMap`" ] }, { "cell_type": "markdown", "id": "parammap-body", "metadata": {}, "source": [ "When a sequence must operate on multiple gates simultaneously, use `ParameterMap[str, T]` instead of a single typed parameter. The framework creates one QCoDeS parameter per element listed under the `elements` key in the config, and groups them into a mapping accessible as `self.arbok_params.[element]`.\n", "\n", "The `arbok.ramp` helper uses these maps to issue one `play` call per element automatically, so the QUA code does not need to iterate over elements explicitly.\n", "\n", "Below is `SquarePulseScalable`, which extends `SquarePulse` to operate on any number of gates." ] }, { "cell_type": "code", "execution_count": null, "id": "parammap-class-code", "metadata": {}, "outputs": [], "source": [ "from dataclasses import dataclass\n", "from qm import qua\n", "from arbok_driver import arbok, SubSequence, ParameterClass\n", "from arbok_driver.parameter_types import List, Time, ParameterMap, Voltage\n", "\n", "@dataclass(frozen=True)\n", "class SquarePulseScalableParameters(ParameterClass):\n", " sticky_elements: List # list of gate element names\n", " t_ramp: Time\n", " t_square_pulse: Time\n", " v_home: ParameterMap[str, Voltage] # one Voltage param per element\n", " v_square: ParameterMap[str, Voltage] # one Voltage param per element\n", "\n", "class SquarePulseScalable(SubSequence):\n", " PARAMETER_CLASS = SquarePulseScalableParameters\n", " arbok_params: SquarePulseScalableParameters\n", "\n", " def qua_sequence(self):\n", " qua.align(*self.arbok_params.sticky_elements.qua)\n", " arbok.ramp(\n", " elements=self.arbok_params.sticky_elements.qua,\n", " reference=self.arbok_params.v_home,\n", " target=self.arbok_params.v_square,\n", " duration=self.arbok_params.t_ramp,\n", " operation='unit_ramp',\n", " )\n", " qua.wait(\n", " self.arbok_params.t_square_pulse.qua,\n", " *self.arbok_params.sticky_elements.qua,\n", " )\n", " arbok.ramp(\n", " elements=self.arbok_params.sticky_elements.qua,\n", " reference=self.arbok_params.v_square,\n", " target=self.arbok_params.v_home,\n", " duration=self.arbok_params.t_ramp,\n", " operation='unit_ramp',\n", " )" ] }, { "cell_type": "markdown", "id": "parammap-conf-body", "metadata": {}, "source": [ "The configuration declares the element-wise parameters using an `elements` dict instead of a single `value`:" ] }, { "cell_type": "code", "execution_count": null, "id": "parammap-conf-code", "metadata": {}, "outputs": [], "source": [ "square_scalable_conf = {\n", " 'parameters': {\n", " 'sticky_elements': {\n", " 'type': List,\n", " 'value': ['P1', 'P2', 'J1'],\n", " },\n", " 't_ramp': {'type': Time, 'value': 20},\n", " 't_square_pulse': {'type': Time, 'value': 100},\n", " 'v_home': {\n", " 'type': Voltage,\n", " 'label': 'Home voltage',\n", " 'elements': {\n", " 'P1': 0.0,\n", " 'P2': 0.0,\n", " 'J1': 0.0,\n", " },\n", " },\n", " 'v_square': {\n", " 'type': Voltage,\n", " 'label': 'Square pulse voltage',\n", " 'elements': {\n", " 'P1': 0.1,\n", " 'P2': -0.05,\n", " 'J1': 0.08,\n", " },\n", " },\n", " }\n", "}" ] }, { "cell_type": "markdown", "id": "parammap-conf-note", "metadata": {}, "source": [ "This creates individual QCoDeS parameters `v_home_P1`, `v_home_P2`, `v_home_J1`, etc., while keeping the QUA code completely unchanged. Scaling from 2 to 8 gates requires only updating the config." ] }, { "cell_type": "code", "execution_count": null, "id": "parammap-inst-code", "metadata": {}, "outputs": [], "source": [ "meas_scaled = Measurement(qm_driver, 'meas_scaled')\n", "sq_scalable = SquarePulseScalable(meas_scaled, 'sq_scalable', square_scalable_conf)\n", "sq_scalable.print_readable_snapshot()" ] }, { "cell_type": "markdown", "id": "lifecycle-heading", "metadata": {}, "source": [ "## 4. Lifecycle Hooks" ] }, { "cell_type": "markdown", "id": "lifecycle-body", "metadata": {}, "source": [ "Beyond `qua_sequence`, the framework calls several optional hooks at specific points during compilation. Override only the ones your sequence actually needs. The execution order is:\n", "\n", "```\n", "qua_declare ← once, before the infinite loop (declare QUA variables)\n", "qua_before_sweep ← once per shot, before any sweep loops begin\n", " [ sweep loops ]\n", " qua_before_sequence ← once per sweep point, before the core sequence\n", " qua_sequence ← the core pulse sequence\n", " qua_after_sequence ← once per sweep point, after the core sequence\n", "```\n", "\n", "Always call `super().()` inside any hook you override. The base class propagates the call to all nested sub-sequences.\n", "\n", "| Hook | When to use |\n", "|---|---|\n", "| `qua_declare` | Declare QUA variables your sequence needs across shots (e.g. loop counters, accumulation buffers) |\n", "| `qua_before_sweep` | Set or reset values that belong outside the sweep loop (e.g. re-zero a running average before each outer loop) |\n", "| `qua_before_sequence` | Apply per-shot setup that depends on swept values (e.g. compute a derived QUA variable from current sweep params) |\n", "| `qua_after_sequence` | Save or stream results after each shot |\n", "\n", "The `Cpmg` sequence is a good example. It declares loop counters in `qua_declare`, precomputes a wait-time scaling factor in `qua_before_sequence` (which depends on the swept `repetitions` variable), and only then runs the pulse train in `qua_sequence`." ] }, { "cell_type": "code", "execution_count": null, "id": "lifecycle-code", "metadata": {}, "outputs": [], "source": [ "from dataclasses import dataclass\n", "from qm import qua\n", "from qm.qua.lib import Cast\n", "from arbok_driver import SubSequence, ParameterClass\n", "from arbok_driver.parameter_types import Time, Int, List\n", "\n", "@dataclass(frozen=True)\n", "class EchoParameters(ParameterClass):\n", " elements: List\n", " qubit_element: Int # single element for illustration\n", " repetitions: Int\n", " t_wait: Time\n", "\n", "class Echo(SubSequence):\n", " \"\"\"\n", " Minimal Hahn-echo-style sequence: (wait - pi - wait) x N\n", " Illustrates how to use qua_declare and qua_before_sequence.\n", " \"\"\"\n", " PARAMETER_CLASS = EchoParameters\n", " arbok_params: EchoParameters\n", "\n", " def qua_declare(self):\n", " \"\"\"Declare QUA variables needed across shots.\"\"\"\n", " super().qua_declare() # always call super first\n", " self.n_index = qua.declare(int)\n", " # QUA does not support integer division. We compute 1/(repetitions*2)\n", " # as a fixed-point factor and then multiply the integer time with it\n", " # using Cast.mul_int_by_fixed (same approach as Cpmg).\n", " self.wait_factor = qua.declare(qua.fixed)\n", " self.sub_wait = qua.declare(int)\n", "\n", " def qua_before_sequence(self):\n", " \"\"\"Compute derived variables that depend on swept parameters.\"\"\"\n", " super().qua_before_sequence()\n", " # Store 1 / (repetitions * 2) as a fixed-point number …\n", " qua.assign(\n", " self.wait_factor,\n", " 1 / (self.arbok_params.repetitions.qua * 2),\n", " )\n", " # … then multiply the integer time by that factor\n", " qua.assign(\n", " self.sub_wait,\n", " Cast.mul_int_by_fixed(\n", " self.arbok_params.t_wait.qua, self.wait_factor),\n", " )\n", "\n", " def qua_sequence(self):\n", " qua.align(*self.arbok_params.elements.qua)\n", " with qua.for_(\n", " var=self.n_index,\n", " init=0,\n", " cond=self.n_index < self.arbok_params.repetitions.qua,\n", " update=self.n_index + 1,\n", " ):\n", " qua.wait(self.sub_wait, *self.arbok_params.elements.qua)\n", " qua.play('pi_pulse', self.arbok_params.qubit_element.qua)\n", " qua.wait(self.sub_wait, *self.arbok_params.elements.qua)\n", " qua.align(*self.arbok_params.elements.qua)" ] }, { "cell_type": "markdown", "id": "nesting-heading", "metadata": {}, "source": [ "## 5. Composing Sequences by Nesting" ] }, { "cell_type": "markdown", "id": "nesting-body", "metadata": {}, "source": [ "A `SubSequence` can own other `SubSequence`s as building blocks. When you instantiate a child inside a parent's `__init__`, arbok automatically appends it to `self._sub_sequences`. That list drives arbok's automatic nesting behaviour — it calls `qua_declare`, `qua_sequence`, etc. on every entry in the list during compilation.\\n\",\n", " \"\\n\",\n", " \"When you want to **control the child manually** (call its methods at specific points in your own `qua_sequence`), you must opt out of automatic nesting. The pattern is:\\n\",\n", " \"\\n\",\n", " \"1. Instantiate all children normally. They register themselves into `self._sub_sequences`.\\n\",\n", " \"2. Save a reference to the populated list.\\n\",\n", " \"3. Reset `self._sub_sequences = []` to prevent arbok from auto-nesting them.\\n\",\n", " \"4. Call each child's lifecycle hooks explicitly in your own hooks.\\n\",\n", " \"\\n\",\n", " \"`StateProjection` is the canonical example: it builds several single-qubit gate sub-sequences, saves them in `self.single_qubit_gates`, empties `self._sub_sequences`, and then calls `sub_sequence.qua_declare()` / `qua_before_sequence()` / `qua_gate()` manually at the right points." ] }, { "cell_type": "code", "execution_count": null, "id": "nesting-code", "metadata": {}, "outputs": [], "source": [ "from dataclasses import dataclass\n", "from qm import qua\n", "from arbok_driver import SubSequence, ParameterClass\n", "from arbok_driver.parameter_types import List, Time\n", "from arbok_driver.examples.sequences import ToControlPoint\n", "\n", "@dataclass(frozen=True)\n", "class InitThenPulseParameters(ParameterClass):\n", " gate_elements: List\n", " qubit_elements: List\n", " t_pulse: Time\n", " # ToControlPoint's parameters must also be present in this config\n", "\n", "class InitThenPulse(SubSequence):\n", " \"\"\"\n", " Runs a ToControlPoint sub-sequence, then waits for t_pulse.\n", " Demonstrates the manual-nesting pattern.\n", " \"\"\"\n", " PARAMETER_CLASS = InitThenPulseParameters\n", " arbok_params: InitThenPulseParameters\n", "\n", " def __init__(self, parent, name: str, sequence_config: dict | None = None):\n", " super().__init__(parent, name, sequence_config)\n", "\n", " # 1. Instantiate child — this appends it to self._sub_sequences\n", " self.to_ctrl = ToControlPoint(self, 'to_ctrl', sequence_config)\n", "\n", " # 2. Reset the list so arbok does NOT auto-nest to_ctrl\n", " self._sub_sequences = []\n", "\n", " self.elements = list(self.arbok_params.gate_elements.get())\n", " self.elements += list(self.arbok_params.qubit_elements.get())\n", "\n", " def qua_declare(self):\n", " super().qua_declare()\n", " self.to_ctrl.qua_declare() # propagate manually\n", "\n", " def qua_before_sequence(self):\n", " super().qua_before_sequence()\n", " self.to_ctrl.qua_before_sequence() # propagate manually\n", "\n", " def qua_sequence(self):\n", " # Call child at the right point in the timeline\n", " self.to_ctrl.qua_sequence()\n", " qua.wait(\n", " self.arbok_params.t_pulse.qua,\n", " *self.elements,\n", " )" ] }, { "cell_type": "markdown", "id": "nesting-note", "metadata": {}, "source": [ "The child's parameters are sourced from the same configuration dict as the parent. As long as the parent config includes all fields required by the child's `PARAMETER_CLASS`, arbok resolves them automatically via `map_arbok_params`." ] }, { "cell_type": "markdown", "id": "init-heading", "metadata": {}, "source": [ "## 6. When to Override `__init__`" ] }, { "cell_type": "markdown", "id": "init-body", "metadata": {}, "source": [ "The base `SubSequence.__init__` handles all framework wiring. You only need to override it when your sequence must do work that depends on the already-constructed parameters before compilation starts. Common reasons:\n", "\n", "- Collecting the flat list of QUA elements for `qua.align` / `qua.wait` calls.\n", "- Instantiating child sub-sequences.\n", "- Injecting default parameter values programmatically (see `Cpmg`).\n", "\n", "When you do override `__init__`, always call `super().__init__` with `parent`, `name`, and `sequence_config` before any access to `self.arbok_params`, because the framework populates that attribute inside `super().__init__`." ] }, { "cell_type": "code", "execution_count": null, "id": "init-code", "metadata": {}, "outputs": [], "source": [ "from dataclasses import dataclass\n", "from arbok_driver import SubSequence, ParameterClass\n", "from arbok_driver.parameter_types import List, Time, ParameterMap, Voltage\n", "\n", "@dataclass(frozen=True)\n", "class ToControlPointParameters(ParameterClass):\n", " gate_elements: List\n", " qubit_elements: List\n", " t_ramp_to_control: Time\n", " t_wait_pre_control: Time\n", " v_home: ParameterMap[str, Voltage]\n", " v_control: ParameterMap[str, Voltage]\n", "\n", "class ToControlPoint(SubSequence):\n", " PARAMETER_CLASS = ToControlPointParameters\n", " arbok_params: ToControlPointParameters\n", "\n", " def __init__(self, parent, name: str, sequence_config: dict | None = None):\n", " super().__init__(parent, name, sequence_config) # must come first\n", " # self.arbok_params is now populated, so we can read from it\n", " self.elements = list(self.arbok_params.gate_elements.get())\n", " self.elements += list(self.arbok_params.qubit_elements.get())\n", "\n", " def qua_sequence(self):\n", " from arbok_driver import arbok\n", " qua.align(*self.elements)\n", " arbok.ramp(\n", " elements=self.arbok_params.gate_elements.get(),\n", " reference=self.arbok_params.v_home,\n", " target=self.arbok_params.v_control,\n", " duration=self.arbok_params.t_ramp_to_control,\n", " operation='unit_ramp',\n", " )\n", " qua.align(*self.elements)\n", " qua.wait(self.arbok_params.t_wait_pre_control.qua, *self.elements)\n", " qua.align(*self.elements)" ] }, { "cell_type": "markdown", "id": "checklist-heading", "metadata": {}, "source": [ "## 7. Checklist for a New SubSequence" ] }, { "cell_type": "markdown", "id": "checklist-body", "metadata": {}, "source": [ "Use this checklist when writing a new `SubSequence` subclass.\n", "\n", "**`ParameterClass`**\n", "- Defined as a `@dataclass(frozen=True)` inheriting from `ParameterClass`.\n", "- Every field has a type annotation from `arbok_driver.parameter_types`.\n", "- Use `ParameterMap[str, T]` for parameters that are defined per gate element.\n", "\n", "**Class body**\n", "- `PARAMETER_CLASS` is set to the dataclass defined above.\n", "- `arbok_params` is annotated with the same dataclass type (for IDE autocompletion).\n", "- `qua_sequence` contains only `qua.*` calls and `arbok.*` helper calls.\n", "- Parameters inside `qua_sequence` are always accessed via `.qua`, never via `.get()`.\n", "\n", "**`__init__` (only when needed)**\n", "- `super().__init__(parent, name, sequence_config)` is the first call.\n", "- Post-super code that reads from `arbok_params` uses `.get()` (Python side), not `.qua` (QUA side).\n", "\n", "**Hooks (only when needed)**\n", "- `super().()` is always the first call inside any hook.\n", "- `qua_declare` declares QUA variables with `qua.declare(...)` and stores them as instance attributes.\n", "- `qua_before_sequence` assigns values to QUA variables using `qua.assign(...)`.\n", "\n", "**Configuration**\n", "- Every field name in `PARAMETER_CLASS` appears under `parameters` in the config.\n", "- Single-value parameters use `{'type': ..., 'value': ...}`.\n", "- Element-wise parameters use `{'type': ..., 'label': ..., 'elements': {'gate': value, ...}}`." ] }, { "cell_type": "markdown", "id": "example-heading", "metadata": {}, "source": [ "## 8. Complete Minimal Example" ] }, { "cell_type": "markdown", "id": "example-body", "metadata": {}, "source": [ "The following cells bring everything together as the smallest working `SubSequence`: a voltage ramp to a target point, a wait, and a reset." ] }, { "cell_type": "code", "execution_count": 1, "id": "example-class-code", "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "2026-04-16 05:32:41,538 - qm - INFO - Starting session: 9a1b4cd9-e8a3-4d54-acda-e6072bc6475c\n" ] } ], "source": [ "from dataclasses import dataclass\n", "from qm import qua\n", "from arbok_driver import arbok, SubSequence, ParameterClass\n", "from arbok_driver.parameter_types import List, Time, ParameterMap, Voltage\n", "\n", "@dataclass(frozen=True)\n", "class RampAndWaitParameters(ParameterClass):\n", " gate_elements: List\n", " t_ramp: Time\n", " t_hold: Time\n", " v_home: ParameterMap[str, Voltage]\n", " v_target: ParameterMap[str, Voltage]\n", "\n", "class RampAndWait(SubSequence):\n", " \"\"\"\n", " Ramps all gate elements from v_home to v_target, holds for t_hold, then resets.\n", " \"\"\"\n", " PARAMETER_CLASS = RampAndWaitParameters\n", " arbok_params: RampAndWaitParameters\n", "\n", " def __init__(self, parent, name: str, sequence_config: dict | None = None):\n", " super().__init__(parent, name, sequence_config)\n", " self.elements = list(self.arbok_params.gate_elements.get())\n", "\n", " def qua_sequence(self):\n", " qua.align(*self.elements)\n", " arbok.ramp(\n", " elements=self.arbok_params.gate_elements.get(),\n", " reference=self.arbok_params.v_home,\n", " target=self.arbok_params.v_target,\n", " duration=self.arbok_params.t_ramp,\n", " operation='unit_ramp',\n", " )\n", " qua.align(*self.elements)\n", " qua.wait(self.arbok_params.t_hold.qua, *self.elements)\n", " qua.align(*self.elements)\n", " arbok.reset_sticky_elements(self.arbok_params.gate_elements.get())" ] }, { "cell_type": "code", "execution_count": 2, "id": "example-conf-code", "metadata": {}, "outputs": [], "source": [ "ramp_conf = {\n", " 'parameters': {\n", " 'gate_elements': {'type': List, 'value': ['P1', 'P2', 'J1']},\n", " 't_ramp': {'type': Time, 'value': 100},\n", " 't_hold': {'type': Time, 'value': 5000},\n", " 'v_home': {\n", " 'type': Voltage,\n", " 'label': 'Home voltage',\n", " 'elements': {'P1': 0.0, 'P2': 0.0, 'J1': 0.0},\n", " },\n", " 'v_target': {\n", " 'type': Voltage,\n", " 'label': 'Target voltage',\n", " 'elements': {'P1': 0.15, 'P2': -0.1, 'J1': 0.05},\n", " },\n", " }\n", "}" ] }, { "cell_type": "code", "execution_count": 3, "id": "example-inst-code", "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "qm_driver_mock_measurement_ramp_and_wait:\n", "\tparameter value\n", "--------------------------------------------------------------------------------\n", "gate_elements :\t['P1', 'P2', 'J1'] (N/A)\n", "t_hold :\t5000 (s)\n", "t_ramp :\t100 (s)\n", "v_home_J1 :\t0 (V)\n", "v_home_P1 :\t0 (V)\n", "v_home_P2 :\t0 (V)\n", "v_target_J1 :\t0.05 (V)\n", "v_target_P1 :\t0.15 (V)\n", "v_target_P2 :\t-0.1 (V)\n" ] }, { "data": { "text/html": [ "
\n",
       "# Single QUA script generated at 2026-04-16 05:32:45.212522\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",
       "    with infinite_loop_():\n",
       "        pause()\n",
       "        assign(v1, 0)\n",
       "        align(\"P1\", \"P2\", \"J1\")\n",
       "        align(\"P1\", \"P2\", \"J1\")\n",
       "        play(\"unit_ramp\"*amp(0.8999999999999999), \"P1\", duration=100)\n",
       "        play(\"unit_ramp\"*amp(-0.6000000000000001), \"P2\", duration=100)\n",
       "        play(\"unit_ramp\"*amp(0.05), \"J1\", duration=100)\n",
       "        align(\"P1\", \"P2\", \"J1\")\n",
       "        align(\"P1\", \"P2\", \"J1\")\n",
       "        wait(5000, \"P1\", \"P2\", \"J1\")\n",
       "        align(\"P1\", \"P2\", \"J1\")\n",
       "        ramp_to_zero(\"P1\")\n",
       "        ramp_to_zero(\"P2\")\n",
       "        ramp_to_zero(\"J1\")\n",
       "        align(\"P1\", \"P2\", \"J1\")\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",
       "\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;92m05:32:45\u001b[0m.\u001b[1;36m212522\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", " 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\"J1\"\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\"J1\"\u001b[0m\u001b[1m)\u001b[0m\n", " \u001b[1;35mplay\u001b[0m\u001b[1m(\u001b[0m\u001b[32m\"unit_ramp\"\u001b[0m*\u001b[1;35mamp\u001b[0m\u001b[1m(\u001b[0m\u001b[1;36m0.8999999999999999\u001b[0m\u001b[1m)\u001b[0m, \u001b[32m\"P1\"\u001b[0m, \u001b[33mduration\u001b[0m=\u001b[1;36m100\u001b[0m\u001b[1m)\u001b[0m\n", " \u001b[1;35mplay\u001b[0m\u001b[1m(\u001b[0m\u001b[32m\"unit_ramp\"\u001b[0m*\u001b[1;35mamp\u001b[0m\u001b[1m(\u001b[0m\u001b[1;36m-0.6000000000000001\u001b[0m\u001b[1m)\u001b[0m, \u001b[32m\"P2\"\u001b[0m, \u001b[33mduration\u001b[0m=\u001b[1;36m100\u001b[0m\u001b[1m)\u001b[0m\n", " \u001b[1;35mplay\u001b[0m\u001b[1m(\u001b[0m\u001b[32m\"unit_ramp\"\u001b[0m*\u001b[1;35mamp\u001b[0m\u001b[1m(\u001b[0m\u001b[1;36m0.05\u001b[0m\u001b[1m)\u001b[0m, \u001b[32m\"J1\"\u001b[0m, \u001b[33mduration\u001b[0m=\u001b[1;36m100\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\"J1\"\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\"J1\"\u001b[0m\u001b[1m)\u001b[0m\n", " \u001b[1;35mwait\u001b[0m\u001b[1m(\u001b[0m\u001b[1;36m5000\u001b[0m, \u001b[32m\"P1\"\u001b[0m, \u001b[32m\"P2\"\u001b[0m, \u001b[32m\"J1\"\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\"J1\"\u001b[0m\u001b[1m)\u001b[0m\n", " \u001b[1;35mramp_to_zero\u001b[0m\u001b[1m(\u001b[0m\u001b[32m\"P1\"\u001b[0m\u001b[1m)\u001b[0m\n", " \u001b[1;35mramp_to_zero\u001b[0m\u001b[1m(\u001b[0m\u001b[32m\"P2\"\u001b[0m\u001b[1m)\u001b[0m\n", " \u001b[1;35mramp_to_zero\u001b[0m\u001b[1m(\u001b[0m\u001b[32m\"J1\"\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\"J1\"\u001b[0m\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", "\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": [ "from arbok_driver import ArbokDriver, Device, Measurement\n", "from arbok_driver.examples.configurations.hardware import (\n", " opx1000_config, divider_config)\n", "from rich import print as rprint\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", "ramp_and_wait = RampAndWait(mock_measurement, 'ramp_and_wait', ramp_conf)\n", "ramp_and_wait.print_readable_snapshot()\n", "rprint(mock_measurement.get_qua_program_as_str())" ] }, { "cell_type": "code", "execution_count": null, "id": "f061d137", "metadata": {}, "outputs": [], "source": [] } ], "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 }