{ "cells": [ { "cell_type": "markdown", "id": "2c6a82eb-f45f-47cc-b012-f149d71e66ec", "metadata": {}, "source": [ "# Tutorial 1) Parameterizing sequences with arbok" ] }, { "cell_type": "markdown", "id": "8c51c408-fcfc-46d8-8f33-497e282013a7", "metadata": {}, "source": [ "## 0. Introduction" ] }, { "cell_type": "markdown", "id": "c6cdf8b7-e05d-4011-a2a1-57d91454dae2", "metadata": {}, "source": [ "This tutorial gives a first insight into the architecture and philosophy behind arbok.\n", "\n", "Arbok is a top-level python control framework based on [QCoDeS](https://microsoft.github.io/Qcodes/index.html) compiling into FPGA instructions ([QM-QUA SDK](https://pypi.org/project/qm-qua/1.1.7/)) for [quantum machines hardware](https://www.quantum-machines.co/). The core idea behind arbok is to write **qubit control sequences in a device and measurement setup agnostic manner** that are configured/ scaled to larger systems by providing the respective **configurations that characterize that given system**.\n", "\n", "**QCoDeS** is a full stack data acquisition framework that handles instrument communication, parameterization, data storage and visualization. Arbok leverages this existing infrastructure and " ] }, { "cell_type": "markdown", "id": "6e995c72-55e3-475d-a4b3-a2d1ff0ad8ad", "metadata": {}, "source": [ "

\n", " \n", "

" ] }, { "cell_type": "markdown", "id": "1bcbc6bc-5154-4d68-a77b-562aa629b04c", "metadata": {}, "source": [ "The **following tutorial** proivides:\n", "1. An overview of the basic architecture of arbok\n", "2. Demonstration of building a custom square pulse from scratch\n", "3. Defining parameter sweeps\n", "4. Using arbok QUA helpers to write sequences\n", "\n", "The **other tutorials** in this documentation cover the topics:\n", "- Tutorial 0) Measurement notebook example\n", "- Tutorial 2) Readout sequences and abstract readouts\n", "- Tutorial 3) Input streaming of parameters" ] }, { "cell_type": "markdown", "id": "d46b9e25-dbbd-4a68-8c8c-92fcccc8a7ea", "metadata": {}, "source": [ "## 1. The basic architecture" ] }, { "cell_type": "markdown", "id": "440cc09d-52b3-4dc6-af13-9f1181b57a1f", "metadata": {}, "source": [ "To get started, four types of classes are needed:\n", "\n", "`Device`:\\\n", "The Device holds the quantum machines configuration that is quantifying elements, waveforms, mixers, etc. Also contains information about voltage dividers between the quantum machine outputs and the given device. Each of the following classes requires an instance of a sample to be instanciated.\n", "\n", "`ArbokDriver`:\\\n", "This instance can be understood as the actual qcodes instrument and manages the hardware connection, and all modular (Sub)Sequences.\n", "\n", "`SubSequence`:\\\n", "The SubSequence has and modular snippet of qua code which is written in a device agnostic way and can be re used on any system. The SubSequence is then parameterized by a configuration in the form a python dictionairy that make the executed code device specific.\n", "\n", "`Measurement`:\\\n", "The Measurement does not contain qua code itself but holds one or more SubSequences that are compiled together. A Measurement is meant to asseble its sub-modules to a full experiment. \n", "\n", "\n", "How the mentioned classes relate to each other is sketched in the schematic below:" ] }, { "cell_type": "markdown", "id": "a53310ab-86c3-4d91-a363-283f1e101594", "metadata": {}, "source": [ "" ] }, { "cell_type": "markdown", "id": "2d4bf7e2-5fa9-40ce-9d87-308fa2e8668d", "metadata": {}, "source": [ "## 2. Parameterizing a simple square pulse" ] }, { "cell_type": "markdown", "id": "ca1c8f52-e218-4f21-b808-01f1c770723e", "metadata": {}, "source": [ "Firstly we import `ArbokDriver`, `Device` and `Sequence` classes that were discussed before. As a `SubSequence`, we import one of the given examples. We are starting with a simple square pulse sequence.\n", "\n", "We also import a dummy configuration which has been taken from the quantum machines github repository." ] }, { "cell_type": "code", "execution_count": 1, "id": "cc463882-eefb-40cf-acdc-0ceb782cb16f", "metadata": {}, "outputs": [], "source": [ "%load_ext rich\n", "\n", "from IPython.display import display, HTML" ] }, { "cell_type": "code", "execution_count": 2, "id": "a3ba59b9-1fad-47f1-ad8e-59d8e4eb5383", "metadata": {}, "outputs": [], "source": [ "# import logging\n", "# logging.basicConfig(\n", "# filename = 'logs/tute1.txt',\n", "# filemode = 'w',\n", "# encoding = 'utf-8',\n", "# level = logging.DEBUG\n", "# )\n" ] }, { "cell_type": "code", "execution_count": 3, "id": "86bdb3f5-56f9-4192-87be-b1a76e694e33", "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "2026-03-19 06:08:15,563 - qm - INFO - Starting session: bbc36766-a1c8-49b4-903f-25527e94d771\n" ] } ], "source": [ "from arbok_driver import ArbokDriver, Device, Measurement\n", "from arbok_driver.examples.sub_sequences import SquarePulse\n", "from arbok_driver.examples.configurations import opx1000_config" ] }, { "cell_type": "code", "execution_count": 4, "id": "cdf0ee41-ca91-4509-b818-2d27f76ef1c0", "metadata": { "execution": { "iopub.execute_input": "2025-09-03T05:16:35.850751Z", "iopub.status.busy": "2025-09-03T05:16:35.849749Z", "iopub.status.idle": "2025-09-03T05:16:35.854307Z", "shell.execute_reply": "2025-09-03T05:16:35.854307Z", "shell.execute_reply.started": "2025-09-03T05:16:35.850751Z" } }, "outputs": [ { "data": { "text/html": [ "
\n"
      ],
      "text/plain": []
     },
     "metadata": {},
     "output_type": "display_data"
    },
    {
     "data": {
      "text/html": [
       ""
      ],
      "text/plain": [
       "\u001b[1m<\u001b[0m\u001b[1;95mIPython.core.display.HTML\u001b[0m\u001b[39m object\u001b[0m\u001b[1m>\u001b[0m"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    }
   ],
   "source": [
    "display(HTML(''))"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "dbc524d2-e135-4278-8f24-3a6b8e469e97",
   "metadata": {},
   "source": [
    "### 2.1 Configuring the `Device`"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "5d6276a2-681a-4cac-8625-834a5127d89b",
   "metadata": {},
   "source": [
    "In the first step, a  `Device` object will be configured and is used in every following\n",
    "sequence.\n",
    "The sample holds the configuration of the quantum machine that you probably already have from your experiments."
   ]
  },
  {
   "cell_type": "markdown",
   "id": "eedf69bf-6060-4fa0-96b3-93d4b5a70bb5",
   "metadata": {},
   "source": [
    "Besides this configuration a further **'divider_config'** is required.\n",
    "This dictionary represents voltage divider that are in between the quantum machine and your sample. All voltage values in arbok are meant to be what is applied to the sample, not the output of the machine. This is implemented by the scale attribute of qcodes parameters."
   ]
  },
  {
   "cell_type": "markdown",
   "id": "614e1f4b-0614-4d6d-a929-a5d1acb5eadd",
   "metadata": {},
   "source": [
    "A simple example is given below. A dict entry is required for every element in the quantum machines config that has an output port configured. For every element we multiply the factor `opx_scale` which compensates the output range of the OPX (-0.5V ->  0.5V)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 5,
   "id": "5b4b942f-d06f-46d2-b934-d6ad27fc5de3",
   "metadata": {},
   "outputs": [],
   "source": [
    "opx_scale = 2\n",
    "divider_config = {\n",
    "    'gate_1': {\n",
    "        'division': 1*opx_scale,\n",
    "    },\n",
    "    'gate_2': {\n",
    "        'division': 1*opx_scale,\n",
    "    },\n",
    "    'readout_element': {\n",
    "        'division': 1*opx_scale\n",
    "    }\n",
    "}"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "e7bba380-e0a1-4915-bab3-66dad8eee271",
   "metadata": {},
   "source": [
    "Both of those configs are now used to instantiate the given device."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 6,
   "id": "529e92fa-f071-4445-b2ce-bdc53a5ebd6e",
   "metadata": {},
   "outputs": [],
   "source": [
    "mock_device = Device(\n",
    "    name = 'mock_device',\n",
    "    opx_config = opx1000_config,\n",
    "    divider_config = divider_config)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "4d7bd316-8b83-4249-b07e-5095a3e7c858",
   "metadata": {},
   "source": [
    "### 2.2 Building the `Arbok_driver` and a `Measurement`"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "9c5b0358-9bfe-4ad5-ada9-454ec82e06d3",
   "metadata": {},
   "source": [
    "The sample we created previously is the only requirement to build a basic arbok_driver."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 7,
   "id": "50c6f31b-c6a3-423c-a405-697db3a35b68",
   "metadata": {},
   "outputs": [],
   "source": [
    "qm_driver = ArbokDriver('qm_driver', mock_device)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "3bdf87b2-428f-46de-bc06-091cc2fb8559",
   "metadata": {},
   "source": [
    "`Measurement`s can now be registered into this driver. Measurements are meant to act as a container for all sub-sequences that are required to run a single type of measurement.\n",
    "Currently single sequences per driver are supported."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "348abf7f-de23-4e70-9eaa-54b5b5ebd510",
   "metadata": {},
   "outputs": [],
   "source": [
    "mock_measurement = Measurement(qm_driver, 'mock_measurement')"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "49fde882-0a8b-4bb3-8337-ac195d9a11e3",
   "metadata": {},
   "source": [
    "## 2.1 Configuring a simple square pulse sequence"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "0af2ead7-7f88-4a1f-9609-322a8a113b19",
   "metadata": {},
   "source": [
    "So far, predefined classes were created and instantiated. The following `SquarePulse` class will demonstrate inheritance, let us have a look at the source code.\n",
    "\n",
    "The `SquarePulse` inherits from `SubSequence` and the only thing added/overwritten is the `qua_sequence` method.\n",
    "The qua commands to execute are written within that method and the given arguments are filled with other attribute calls (like self.amplitude()).\n",
    "Those attributes are in fact qcodes parameters and can be tracked and varied throughout an experiment."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 9,
   "id": "e3e61644-2967-481d-888f-232a4588cd1f",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "\u001b[31mInit signature:\u001b[39m\n",
      "SquarePulse(\n",
      "    parent,\n",
      "    name: \u001b[33m'str'\u001b[39m,\n",
      "    sequence_config: \u001b[33m'dict | None'\u001b[39m = \u001b[38;5;28;01mNone\u001b[39;00m,\n",
      "    check_step_requirements: \u001b[33m'bool'\u001b[39m = \u001b[38;5;28;01mFalse\u001b[39;00m,\n",
      "    **kwargs,\n",
      ")\n",
      "\u001b[31mSource:\u001b[39m        \n",
      "\u001b[38;5;28;01mclass\u001b[39;00m SquarePulse(SubSequence):\n",
      "    \u001b[33m\"\"\"\u001b[39m\n",
      "\u001b[33m    Class containing parameters and sequence for a simple square pulse\u001b[39m\n",
      "\u001b[33m    \"\"\"\u001b[39m\n",
      "    PARAMETER_CLASS = SquarePulseParameters\n",
      "    arbok_params: SquarePulseParameters\n",
      "    \u001b[38;5;28;01mdef\u001b[39;00m qua_sequence(self):\n",
      "        \u001b[33m\"\"\"Macro that will be played within the qua.program() context\"\"\"\u001b[39m\n",
      "        qua.align()\n",
      "        qua.play(\n",
      "            pulse = \u001b[33m'ramp'\u001b[39m*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",
      "        qua.play(\n",
      "            pulse = \u001b[33m'ramp'\u001b[39m*qua.amp(self.arbok_params.amplitude.qua),\n",
      "            element = self.arbok_params.element.qua,\n",
      "            duration = self.arbok_params.t_ramp.qua\n",
      "            )\n",
      "\u001b[31mFile:\u001b[39m           ~/git-repos/arbok_driver/arbok_driver/examples/sub_sequences/square_pulse.py\n",
      "\u001b[31mType:\u001b[39m           ABCMeta\n",
      "\u001b[31mSubclasses:\u001b[39m     "
     ]
    }
   ],
   "source": [
    "from arbok_driver.examples.sub_sequences.square_pulse import SquarePulse\n",
    "SquarePulse??"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "cf0a595f-e04f-4f34-b672-1390732ee3ac",
   "metadata": {},
   "source": [
    "The `SquarePulse` `SubSequence` requires parameters (e.g. defined in a `sequence_config` file) that adds a `qcodes.parameter` for each entry like the one given here (note that we import `Voltage` and `Time` to specify our parameter units and other variables automatically):"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 10,
   "id": "593ad9ef-ebfa-4c06-8a61-b0128660141b",
   "metadata": {},
   "outputs": [],
   "source": [
    "from arbok_driver.parameter_types import Voltage, Time, String\n",
    "\n",
    "square_conf = {\n",
    "    'parameters': {\n",
    "        'amplitude': {\n",
    "            'value': 0.5,\n",
    "            'type': Voltage,\n",
    "        },\n",
    "        't_square_pulse': {\n",
    "            'value': 100,\n",
    "            'type': Time\n",
    "        },\n",
    "\n",
    "        'element': {\n",
    "            'value': 'gate_1',\n",
    "            'unit': 'gate label',\n",
    "            'type':  String\n",
    "        },\n",
    "        't_ramp': {\n",
    "            'value': 20,\n",
    "            'type': Time\n",
    "        },\n",
    "    }\n",
    "}"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "6e4d89e8-903f-4389-825c-2c6021d827c1",
   "metadata": {},
   "source": [
    "`square_conf` fully configures parameters with an initial value and\n",
    "unit. Optionally you can add the **variable type within qua, an axis label for data saving and validators** like this:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 11,
   "id": "55e9255b-e0ea-4743-ba37-d1cc2efb06dd",
   "metadata": {},
   "outputs": [],
   "source": [
    "_ = {\n",
    "    't_square_pulse': {\n",
    "        'value': 100,\n",
    "        'type': Time,\n",
    "        'label': 'Square pulse width'\n",
    "    },\n",
    "}"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "29658e9c-d7d0-4314-bc7b-5e39caf1c084",
   "metadata": {},
   "source": [
    "After creating the square pulse `SubSequence` with the respective config we can take a look at its snapshot."
   ]
  },
  {
   "cell_type": "markdown",
   "id": "25dd7b10-6850-41bb-995f-32014bb213f4",
   "metadata": {},
   "source": [
    "Again, as all (sub-) sequences so far it requires a sample object for instantiation.\n",
    "This makes sure that all added sub-sequences are configured for the same device.\n",
    "Finally we add the `SquarePulse` of type `SubSequence` to our `Sequence` which is registered to the `ArbokDriver`."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 12,
   "id": "222d3480-bd68-4a3e-a598-52d717e4016b",
   "metadata": {},
   "outputs": [],
   "source": [
    "square_pulse = SquarePulse(mock_measurement, 'square_pulse', square_conf)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 13,
   "id": "e54c99f6-7dfb-4f12-ae80-47f4a193fa65",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "qm_driver_dummy_squence_square_pulse:\n",
      "\tparameter     value\n",
      "--------------------------------------------------------------------------------\n",
      "amplitude      :\t0.5 (V)\n",
      "element        :\tgate_1 (gate label)\n",
      "t_ramp         :\t20 (s)\n",
      "t_square_pulse :\t100 (s)\n"
     ]
    }
   ],
   "source": [
    "qm_driver.dummy_squence.square_pulse.print_readable_snapshot()"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "b918163e-8a43-458d-81ac-1462427e8b24",
   "metadata": {},
   "source": [
    "Another way to see all available parameters on a (sub-) sequence is by checking the parameters attribute:"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "9d847519-8278-442e-b2be-a455c22c24fe",
   "metadata": {},
   "source": [
    "square_pulse.parameters"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "28b86b5a-80af-4135-a51b-6c92e406c6ba",
   "metadata": {},
   "source": [
    "Parameters can be easily modified and read out. Let us half the duration of the square pulse."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 14,
   "id": "0993b917-10ac-4895-a3c8-eccfc9d632d7",
   "metadata": {},
   "outputs": [],
   "source": [
    "square_pulse.t_square_pulse.set(50)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 15,
   "id": "f76e5685-60a5-41e2-a1c2-7364c420108b",
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/html": [
       "
\n"
      ],
      "text/plain": []
     },
     "metadata": {},
     "output_type": "display_data"
    },
    {
     "data": {
      "text/plain": [
       "\u001b[1;36m50.0\u001b[0m"
      ]
     },
     "execution_count": 15,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "square_pulse.t_square_pulse.get()"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "d82c8487-849b-4a7b-92e7-b01795a88ace",
   "metadata": {},
   "source": [
    "### 2.3 Compiling the qua program"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "6fca270a-8f62-4093-886f-a9b0ff1aa198",
   "metadata": {},
   "source": [
    "All (sub-) sequences are prepared and the QUA program can be compiled. The method `get_qua_program` of the `ArbokDriver` recursively goes through the sequences and subsequences and returns the qua source code. The output can now be printed to a file with the method `print_qua_program_to_file`. Let us have a look at the result:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 16,
   "id": "640b23e3-6819-4be5-b7f8-9030cfccbffe",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "\u001b[31mType:\u001b[39m        module\n",
      "\u001b[31mString form:\u001b[39m \n",
      "\u001b[31mFile:\u001b[39m        ~/git-repos/arbok_driver/docs/qua_programs/tut1_square_pulses.py\n",
      "\u001b[31mSource:\u001b[39m     \n",
      "\u001b[38;5;66;03m# Single QUA script generated at 2026-03-19 06:08:17.314374\u001b[39;00m\n",
      "\u001b[38;5;66;03m# QUA library version: 1.2.5\u001b[39;00m\n",
      "\n",
      "\n",
      "\u001b[38;5;28;01mfrom\u001b[39;00m qm \u001b[38;5;28;01mimport\u001b[39;00m CompilerOptionArguments\n",
      "\u001b[38;5;28;01mfrom\u001b[39;00m qm.qua \u001b[38;5;28;01mimport\u001b[39;00m *\n",
      "\n",
      "\u001b[38;5;28;01mwith\u001b[39;00m program() \u001b[38;5;28;01mas\u001b[39;00m prog:\n",
      "    v1 = declare(int, value=\u001b[32m0\u001b[39m)\n",
      "    \u001b[38;5;28;01mwith\u001b[39;00m infinite_loop_():\n",
      "        pause()\n",
      "        assign(v1, \u001b[32m0\u001b[39m)\n",
      "        align()\n",
      "        play(\u001b[33m\"ramp\"\u001b[39m*amp(\u001b[32m0.5\u001b[39m), \u001b[33m\"gate_1\"\u001b[39m, duration=\u001b[32m20\u001b[39m)\n",
      "        wait(\u001b[32m50\u001b[39m, \u001b[33m\"gate_1\"\u001b[39m)\n",
      "        play(\u001b[33m\"ramp\"\u001b[39m*amp(\u001b[32m0.5\u001b[39m), \u001b[33m\"gate_1\"\u001b[39m, duration=\u001b[32m20\u001b[39m)\n",
      "        align()\n",
      "        assign(v1, (v1+\u001b[32m1\u001b[39m))\n",
      "        r0 = declare_stream()\n",
      "        save(v1, r0)\n",
      "        align()\n",
      "    \u001b[38;5;28;01mwith\u001b[39;00m stream_processing():\n",
      "        r0.buffer(\u001b[32m1\u001b[39m).save(\u001b[33m\"qm_driver_dummy_squence_shots\"\u001b[39m)\n",
      "\n",
      "config = \u001b[38;5;28;01mNone\u001b[39;00m\n",
      "\n",
      "loaded_config = \u001b[38;5;28;01mNone\u001b[39;00m"
     ]
    }
   ],
   "source": [
    "qua_program = mock_measurement.get_qua_program()\n",
    "\n",
    "qm_driver.print_qua_program_to_file(\n",
    "    'qua_programs/tut1_square_pulses.py', qua_program)\n",
    "from qua_programs import tut1_square_pulses\n",
    "tut1_square_pulses??"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "5261813a-40aa-4c62-9220-26eebfdf3380",
   "metadata": {},
   "source": [
    "The implicit parameters of the `SquarePulse` class became explicit upon QUA compilation with the values we have set."
   ]
  },
  {
   "cell_type": "markdown",
   "id": "aaba41e2-8a86-4e52-8c74-5ad82142ac6e",
   "metadata": {},
   "source": [
    "Two lines defining an `infinite_loop` and `pause` of the given QUA program were not written in the square pulse class and are created by the driver in every case. The given logic is required to perform multiple shots of a sequence for example with different parameters or just to average a result. The `pause` statement keeps the client PC in sync with the execution of the quantum machine for live data plotting, input streaming, etc."
   ]
  },
  {
   "cell_type": "markdown",
   "id": "0baffbfb-34f8-4ba8-a7a6-d2adaabfe9df",
   "metadata": {},
   "source": [
    "## 3. Parameter sweeps"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "6240f227-c8c2-42c9-9260-102bf7100f0a",
   "metadata": {},
   "source": [
    "So far we have learnt how to create a qcodes Instrument which parameterises a QUA sequence. Often experiments are characterising a physical system by sweeping parameters. In the next step we want to get familiar with defining parameter sweeps. Parameters can be swept concurrently along the same axis and the sweeps can be nested arbitrarily deep."
   ]
  },
  {
   "cell_type": "markdown",
   "id": "19879dd3-cdd1-4d50-9f7a-866d0562e8db",
   "metadata": {},
   "source": [
    "First we look at the most simple example of a 1D sweep (a sweep of only one parameter). Remember that the sequence acts as the container for all subseqeunces and is meant to represent one experiment. Therefore we define the sweep on it."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 17,
   "id": "e68994ff-117f-4886-935e-d8da2388a883",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Declared 1-dimensional parameter sweep of size 5 [5]\n"
     ]
    }
   ],
   "source": [
    "import numpy as np\n",
    "mock_measurement.set_sweeps(\n",
    "    {\n",
    "        square_pulse.amplitude: np.linspace(0.1, 1, 5)\n",
    "    }\n",
    ")"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "9c3f5a10-3604-43f7-93e3-0786b7389e0c",
   "metadata": {},
   "source": [
    "We print the compiled program to a file to inspect. One finds that the `SquarePulse` instructions are indented by an additional for loop which represents the defined sweep. In comparison to the example above, the parameter we sweep is not defined explicitly anymore but given by a QUA variable everywhere it is called.\n",
    "\n",
    "Arbok tries to parameterize input arrays into **start, step and stop** to save FPGA memory. A user warning is raised every time this is done. The threshold for parameterisation for this is currently that the variance of the input array item steps is 10 times smaller than the step size itself. Arbok will always raise a warning when input arrays are parameterised."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 18,
   "id": "6975bff4-d60d-4bb5-9d3e-4287f531a9d0",
   "metadata": {},
   "outputs": [
    {
     "name": "stderr",
     "output_type": "stream",
     "text": [
      "/home/nixos/git-repos/arbok_driver/arbok_driver/sweep.py:381: RuntimeWarning: invalid value encountered in add\n",
      "  sss['start'] + Cast.mul_fixed_by_int(\n"
     ]
    },
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "\u001b[31mType:\u001b[39m        module\n",
      "\u001b[31mString form:\u001b[39m \n",
      "\u001b[31mFile:\u001b[39m        ~/git-repos/arbok_driver/docs/qua_programs/tut1_square_pulses.py\n",
      "\u001b[31mSource:\u001b[39m     \n",
      "\u001b[38;5;66;03m# Single QUA script generated at 2026-03-19 06:08:17.314374\u001b[39;00m\n",
      "\u001b[38;5;66;03m# QUA library version: 1.2.5\u001b[39;00m\n",
      "\n",
      "\n",
      "\u001b[38;5;28;01mfrom\u001b[39;00m qm \u001b[38;5;28;01mimport\u001b[39;00m CompilerOptionArguments\n",
      "\u001b[38;5;28;01mfrom\u001b[39;00m qm.qua \u001b[38;5;28;01mimport\u001b[39;00m *\n",
      "\n",
      "\u001b[38;5;28;01mwith\u001b[39;00m program() \u001b[38;5;28;01mas\u001b[39;00m prog:\n",
      "    v1 = declare(int, value=\u001b[32m0\u001b[39m)\n",
      "    \u001b[38;5;28;01mwith\u001b[39;00m infinite_loop_():\n",
      "        pause()\n",
      "        assign(v1, \u001b[32m0\u001b[39m)\n",
      "        align()\n",
      "        play(\u001b[33m\"ramp\"\u001b[39m*amp(\u001b[32m0.5\u001b[39m), \u001b[33m\"gate_1\"\u001b[39m, duration=\u001b[32m20\u001b[39m)\n",
      "        wait(\u001b[32m50\u001b[39m, \u001b[33m\"gate_1\"\u001b[39m)\n",
      "        play(\u001b[33m\"ramp\"\u001b[39m*amp(\u001b[32m0.5\u001b[39m), \u001b[33m\"gate_1\"\u001b[39m, duration=\u001b[32m20\u001b[39m)\n",
      "        align()\n",
      "        assign(v1, (v1+\u001b[32m1\u001b[39m))\n",
      "        r0 = declare_stream()\n",
      "        save(v1, r0)\n",
      "        align()\n",
      "    \u001b[38;5;28;01mwith\u001b[39;00m stream_processing():\n",
      "        r0.buffer(\u001b[32m1\u001b[39m).save(\u001b[33m\"qm_driver_dummy_squence_shots\"\u001b[39m)\n",
      "\n",
      "config = \u001b[38;5;28;01mNone\u001b[39;00m\n",
      "\n",
      "loaded_config = \u001b[38;5;28;01mNone\u001b[39;00m"
     ]
    }
   ],
   "source": [
    "qua_program = mock_measurement.get_qua_program()\n",
    "\n",
    "qm_driver.print_qua_program_to_file(\n",
    "    'qua_programs/tut1_parameter_sweeps.py', qua_program)\n",
    "from qua_programs import tut1_parameter_sweeps\n",
    "tut1_square_pulses??"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "4802a23d-0c50-4241-9d6c-abf41636c719",
   "metadata": {},
   "source": [
    "Now let us see how a two dimensional sweep would work where we sweep two parameters along a certain axis. When sweeping two params together it is important to pass input arrays that have the same length."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 19,
   "id": "cfef505b-ac21-4bdb-b9ec-040210ad0968",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Declared 2-dimensional parameter sweep of size 20 [5, 4]\n"
     ]
    }
   ],
   "source": [
    "mock_measurement.set_sweeps(\n",
    "    {\n",
    "        square_pulse.amplitude: np.linspace(0.1, 1, 5)\n",
    "    },\n",
    "    {\n",
    "        square_pulse.t_square_pulse: np.arange(10, 50, 10, dtype = int),\n",
    "        square_pulse.t_ramp: np.arange(10, 50, 10, dtype = int)\n",
    "    }\n",
    ")"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "58bb65f2-3830-47bd-aaf7-286a50f569a4",
   "metadata": {},
   "source": [
    "We see that there is yet another for loop nested into the other. For multi param sweeps, arrays are always defined explicitly."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 20,
   "id": "ee614530-7b8f-4a43-9c6b-462b8f6fa5f4",
   "metadata": {},
   "outputs": [
    {
     "name": "stderr",
     "output_type": "stream",
     "text": [
      "/home/nixos/git-repos/arbok_driver/arbok_driver/sweep.py:381: RuntimeWarning: invalid value encountered in add\n",
      "  sss['start'] + Cast.mul_fixed_by_int(\n"
     ]
    },
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "\u001b[31mType:\u001b[39m        module\n",
      "\u001b[31mString form:\u001b[39m \n",
      "\u001b[31mFile:\u001b[39m        ~/git-repos/arbok_driver/docs/qua_programs/tut1_square_pulses.py\n",
      "\u001b[31mSource:\u001b[39m     \n",
      "\u001b[38;5;66;03m# Single QUA script generated at 2026-03-19 06:08:17.314374\u001b[39;00m\n",
      "\u001b[38;5;66;03m# QUA library version: 1.2.5\u001b[39;00m\n",
      "\n",
      "\n",
      "\u001b[38;5;28;01mfrom\u001b[39;00m qm \u001b[38;5;28;01mimport\u001b[39;00m CompilerOptionArguments\n",
      "\u001b[38;5;28;01mfrom\u001b[39;00m qm.qua \u001b[38;5;28;01mimport\u001b[39;00m *\n",
      "\n",
      "\u001b[38;5;28;01mwith\u001b[39;00m program() \u001b[38;5;28;01mas\u001b[39;00m prog:\n",
      "    v1 = declare(int, value=\u001b[32m0\u001b[39m)\n",
      "    \u001b[38;5;28;01mwith\u001b[39;00m infinite_loop_():\n",
      "        pause()\n",
      "        assign(v1, \u001b[32m0\u001b[39m)\n",
      "        align()\n",
      "        play(\u001b[33m\"ramp\"\u001b[39m*amp(\u001b[32m0.5\u001b[39m), \u001b[33m\"gate_1\"\u001b[39m, duration=\u001b[32m20\u001b[39m)\n",
      "        wait(\u001b[32m50\u001b[39m, \u001b[33m\"gate_1\"\u001b[39m)\n",
      "        play(\u001b[33m\"ramp\"\u001b[39m*amp(\u001b[32m0.5\u001b[39m), \u001b[33m\"gate_1\"\u001b[39m, duration=\u001b[32m20\u001b[39m)\n",
      "        align()\n",
      "        assign(v1, (v1+\u001b[32m1\u001b[39m))\n",
      "        r0 = declare_stream()\n",
      "        save(v1, r0)\n",
      "        align()\n",
      "    \u001b[38;5;28;01mwith\u001b[39;00m stream_processing():\n",
      "        r0.buffer(\u001b[32m1\u001b[39m).save(\u001b[33m\"qm_driver_dummy_squence_shots\"\u001b[39m)\n",
      "\n",
      "config = \u001b[38;5;28;01mNone\u001b[39;00m\n",
      "\n",
      "loaded_config = \u001b[38;5;28;01mNone\u001b[39;00m"
     ]
    }
   ],
   "source": [
    "qua_program = mock_measurement.get_qua_program()\n",
    "\n",
    "qm_driver.print_qua_program_to_file(\n",
    "    'qua_programs/tut1_multi_param_sweeps.py', qua_program)\n",
    "from qua_programs import tut1_multi_param_sweeps\n",
    "tut1_square_pulses??"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 21,
   "id": "f150a9d6-0e5f-4464-ac5f-810923d4a561",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "qm_driver_dummy_squence_square_pulse:\n",
      "\tparameter     value\n",
      "--------------------------------------------------------------------------------\n",
      "amplitude      :\t[0.1   0.325 0.55  0.775 1.   ] (V)\n",
      "element        :\tgate_1 (gate label)\n",
      "t_ramp         :\t[4.0e-08 8.0e-08 1.2e-07 1.6e-07] (s)\n",
      "t_square_pulse :\t[4.0e-08 8.0e-08 1.2e-07 1.6e-07] (s)\n"
     ]
    }
   ],
   "source": [
    "square_pulse.print_readable_snapshot()"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "09387774-b26b-4a7d-925b-e6aaecdb10b6",
   "metadata": {},
   "source": [
    "## 4. QUA helpers for system scale up"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "39056ae8-4afe-4c80-9997-f1aff402a852",
   "metadata": {},
   "source": [
    "### 4.1 Creating parameters for multiple elements at the same time"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "f009b99f-815a-4ee9-8096-d136b7085fe9",
   "metadata": {},
   "source": [
    "If multiple output gates have to be configured at the same time, for example when one wants to move in a high dimensional voltage space, parameters can be defined with a gate prefix as shown below. Looking at 'vHome' and 'vSquare' you will see that no 'value' key is given here but 'elements'. For this given configuration four different parameters will be defined for both 'vHome' and 'vSquare' as seen in the snapshot below."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 22,
   "id": "a7451451-91f5-42c2-957b-6b1f9c8973ef",
   "metadata": {},
   "outputs": [],
   "source": [
    "from arbok_driver.examples.sub_sequences import SquarePulseScalable\n",
    "from arbok_driver.parameter_types import List"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 23,
   "id": "de6a2bf4-25e1-4c45-bdf7-c52f4afbc99c",
   "metadata": {},
   "outputs": [],
   "source": [
    "square_conf_scaled = {\n",
    "    'sequence': SquarePulseScalable,\n",
    "    'parameters': {\n",
    "        'sticky_elements': {\n",
    "            'value': ['gate_1', 'gate_2', 'gate_3', 'gate_4'],\n",
    "            'unit': 'gate label',\n",
    "            'type': List\n",
    "        },\n",
    "        'v_home': {\n",
    "            'type': Voltage,\n",
    "            \"label\": 'Default voltage point during the sequence',\n",
    "            'elements': {\n",
    "                'gate_1': 0,\n",
    "                'gate_2': 0,\n",
    "                'gate_3': 0,\n",
    "                'gate_4': 0,\n",
    "            }\n",
    "        },\n",
    "        'v_square': {\n",
    "            'type': Voltage,\n",
    "            \"label\": 'Voltage amplitude of square pulse',\n",
    "            'elements': {\n",
    "                'gate_1': 0.1,\n",
    "                'gate_2': -0.05,\n",
    "                'gate_3': 0.08,\n",
    "                'gate_4': 0.25,\n",
    "            }\n",
    "        },\n",
    "        't_square_pulse': {\n",
    "            'value': 100,\n",
    "            'type': Time\n",
    "        },\n",
    "        't_ramp': {\n",
    "            'value': 20,\n",
    "            'type': Time\n",
    "        },\n",
    "    }\n",
    "}"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 24,
   "id": "2fa19b8b-11a1-4800-9447-2a4ccf5b71b7",
   "metadata": {},
   "outputs": [],
   "source": [
    "qm_driver2 = ArbokDriver('qm_driver2', mock_device)\n",
    "mock_measurement2 = Measurement(qm_driver2, 'mock_measurement2')\n",
    "square_pulse_scaled = SquarePulseScalable(\n",
    "    mock_measurement2,\n",
    "    'square_pulse_scaled',\n",
    "    square_conf_scaled\n",
    "    )"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "35b4973e-5daf-422b-9a4d-2be79504603b",
   "metadata": {},
   "source": [
    "After instantiating `SquarePulseScalable` with the given configuration we see that for example 'vSquare' created one paramer for each given gate. Resulting parameters are 'v_square_gate_1', 'v_square_gate_2', 'v_square_gate_3', 'v_square_gate_4'."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 26,
   "id": "820302e4-f348-4ff7-9454-290291c303cb",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "qm_driver2_mock_measurement2_square_pulse_scaled:\n",
      "\tparameter      value\n",
      "--------------------------------------------------------------------------------\n",
      "sticky_elements :\t['gate_1', 'gate_2', 'gate_3', 'gate_4'] (gate label)\n",
      "t_ramp          :\t20 (s)\n",
      "t_square_pulse  :\t100 (s)\n",
      "v_home_gate_1   :\t0 (V)\n",
      "v_home_gate_2   :\t0 (V)\n",
      "v_home_gate_3   :\t0 (V)\n",
      "v_home_gate_4   :\t0 (V)\n",
      "v_square_gate_1 :\t0.1 (V)\n",
      "v_square_gate_2 :\t-0.05 (V)\n",
      "v_square_gate_3 :\t0.08 (V)\n",
      "v_square_gate_4 :\t0.25 (V)\n"
     ]
    }
   ],
   "source": [
    "square_pulse_scaled.print_readable_snapshot()"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "e790c586-9019-40af-a7d4-5ac978d73459",
   "metadata": {},
   "source": [
    "### 4.2 Playing pulses on multiple gates at the same time"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "3ae1a377-745a-40c6-97c7-0643cb397655",
   "metadata": {},
   "source": [
    "The square pulse we imported now is slightly different to the one we have seen before. Play statements are not called explicitly but with the qua_helper arbok_go that takes the gate prefix and a list of gates. Play commands are then executed for all of those combinations that are available. Explicit align statements with the given gates can be executed optionally as well. See the resulting program for reference below."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 29,
   "id": "8c4a8e12-f166-4e13-ab71-951700e7f8f5",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "\u001b[31mType:\u001b[39m        module\n",
      "\u001b[31mString form:\u001b[39m \n",
      "\u001b[31mFile:\u001b[39m        ~/git-repos/arbok_driver/docs/qua_programs/tut1_scaled_sweep.py\n",
      "\u001b[31mSource:\u001b[39m     \n",
      "\u001b[38;5;66;03m# Single QUA script generated at 2026-03-19 06:09:41.378656\u001b[39;00m\n",
      "\u001b[38;5;66;03m# QUA library version: 1.2.5\u001b[39;00m\n",
      "\n",
      "\n",
      "\u001b[38;5;28;01mfrom\u001b[39;00m qm \u001b[38;5;28;01mimport\u001b[39;00m CompilerOptionArguments\n",
      "\u001b[38;5;28;01mfrom\u001b[39;00m qm.qua \u001b[38;5;28;01mimport\u001b[39;00m *\n",
      "\n",
      "\u001b[38;5;28;01mwith\u001b[39;00m program() \u001b[38;5;28;01mas\u001b[39;00m prog:\n",
      "    v1 = declare(int, value=\u001b[32m0\u001b[39m)\n",
      "    \u001b[38;5;28;01mwith\u001b[39;00m infinite_loop_():\n",
      "        pause()\n",
      "        assign(v1, \u001b[32m0\u001b[39m)\n",
      "        align(\u001b[33m\"gate_1\"\u001b[39m, \u001b[33m\"gate_2\"\u001b[39m, \u001b[33m\"gate_3\"\u001b[39m, \u001b[33m\"gate_4\"\u001b[39m)\n",
      "        align(\u001b[33m\"gate_1\"\u001b[39m, \u001b[33m\"gate_2\"\u001b[39m, \u001b[33m\"gate_3\"\u001b[39m, \u001b[33m\"gate_4\"\u001b[39m)\n",
      "        play(\u001b[33m\"unit_ramp\"\u001b[39m*amp(\u001b[32m0.2\u001b[39m), \u001b[33m\"gate_1\"\u001b[39m, duration=\u001b[32m20\u001b[39m)\n",
      "        play(\u001b[33m\"unit_ramp\"\u001b[39m*amp(-\u001b[32m0.1\u001b[39m), \u001b[33m\"gate_2\"\u001b[39m, duration=\u001b[32m20\u001b[39m)\n",
      "        play(\u001b[33m\"unit_ramp\"\u001b[39m*amp(\u001b[32m0.08\u001b[39m), \u001b[33m\"gate_3\"\u001b[39m, duration=\u001b[32m20\u001b[39m)\n",
      "        play(\u001b[33m\"unit_ramp\"\u001b[39m*amp(\u001b[32m0.25\u001b[39m), \u001b[33m\"gate_4\"\u001b[39m, duration=\u001b[32m20\u001b[39m)\n",
      "        align(\u001b[33m\"gate_1\"\u001b[39m, \u001b[33m\"gate_2\"\u001b[39m, \u001b[33m\"gate_3\"\u001b[39m, \u001b[33m\"gate_4\"\u001b[39m)\n",
      "        wait(\u001b[32m100\u001b[39m, \u001b[33m\"gate_1\"\u001b[39m, \u001b[33m\"gate_2\"\u001b[39m, \u001b[33m\"gate_3\"\u001b[39m, \u001b[33m\"gate_4\"\u001b[39m)\n",
      "        align(\u001b[33m\"gate_1\"\u001b[39m, \u001b[33m\"gate_2\"\u001b[39m, \u001b[33m\"gate_3\"\u001b[39m, \u001b[33m\"gate_4\"\u001b[39m)\n",
      "        play(\u001b[33m\"unit_ramp\"\u001b[39m*amp(-\u001b[32m0.2\u001b[39m), \u001b[33m\"gate_1\"\u001b[39m, duration=\u001b[32m20\u001b[39m)\n",
      "        play(\u001b[33m\"unit_ramp\"\u001b[39m*amp(\u001b[32m0.1\u001b[39m), \u001b[33m\"gate_2\"\u001b[39m, duration=\u001b[32m20\u001b[39m)\n",
      "        play(\u001b[33m\"unit_ramp\"\u001b[39m*amp(-\u001b[32m0.08\u001b[39m), \u001b[33m\"gate_3\"\u001b[39m, duration=\u001b[32m20\u001b[39m)\n",
      "        play(\u001b[33m\"unit_ramp\"\u001b[39m*amp(-\u001b[32m0.25\u001b[39m), \u001b[33m\"gate_4\"\u001b[39m, duration=\u001b[32m20\u001b[39m)\n",
      "        align(\u001b[33m\"gate_1\"\u001b[39m, \u001b[33m\"gate_2\"\u001b[39m, \u001b[33m\"gate_3\"\u001b[39m, \u001b[33m\"gate_4\"\u001b[39m)\n",
      "        align()\n",
      "        assign(v1, (v1+\u001b[32m1\u001b[39m))\n",
      "        r0 = declare_stream()\n",
      "        save(v1, r0)\n",
      "        align()\n",
      "    \u001b[38;5;28;01mwith\u001b[39;00m stream_processing():\n",
      "        r0.buffer(\u001b[32m1\u001b[39m).save(\u001b[33m\"qm_driver2_mock_measurement2_shots\"\u001b[39m)\n",
      "\n",
      "config = \u001b[38;5;28;01mNone\u001b[39;00m\n",
      "\n",
      "loaded_config = \u001b[38;5;28;01mNone\u001b[39;00m"
     ]
    }
   ],
   "source": [
    "qua_program = mock_measurement2.get_qua_program()\n",
    "\n",
    "qm_driver2.print_qua_program_to_file(\n",
    "    'qua_programs/tut1_scaled_sweep.py', qua_program)\n",
    "from qua_programs import tut1_scaled_sweep\n",
    "tut1_scaled_sweep??\n"
   ]
  }
 ],
 "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
}