{
 "cells": [
  {
   "cell_type": "markdown",
   "id": "a1000001",
   "metadata": {},
   "source": [
    "# SNAP Gate Calibration"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "a1000002",
   "metadata": {},
   "source": [
    "## Prerequisites\n",
    "\n",
    "This guide assumes you have configured a `DeviceSetup` and created `BosonicQubit` objects with assigned parameters. Please see [our tutorials](https://docs.zhinst.com/labone_q_user_manual/applications_library/tutorials/index.html) if you need to create your setup and qubits for the first time.\n",
    "\n",
    "You can run this notebook on real hardware in the lab. If you don't have the hardware at your disposal, you can also run the notebook \"as is\" using an emulated session (see below).\n",
    "\n",
    "Before running the SNAP gate calibration experiment, the following calibrations must have been performed:\n",
    "\n",
    "- **Transmon spectroscopy**: the steady-state ancilla transmon resonance frequency `transmon_resonance_frequency_at_n0` must be known.\n",
    "- **Dispersive shift**: the dispersive shift `chi` between the memory cavity and the ancilla transmon must be measured, so that each number-selective drive line is tuned to the correct frequency.\n",
    "- **Displacement calibration**: the conversion factor `displacement_amp_per_unit_beta` must be known (see the [Displacement Calibration how-to guide](02_displacement_calibration.ipynb)), to prepare coherent states for photon-number-selective state preparation.\n",
    "- **Selective transmon Rx calibration**: the number-selective transmon π-pulse amplitudes `selective_transmon_X180_amplitudes[n]` must be calibrated for the target photon number (see the [Selective Transmon Rx Calibration how-to guide](03_selective_transmon_rx_calibration.ipynb)).\n",
    "\n",
    "If you are just getting started with the LabOne Q Applications Library, please don't hesitate to reach out to us at info@zhinst.com."
   ]
  },
  {
   "cell_type": "markdown",
   "id": "a1000003",
   "metadata": {},
   "source": [
    "## Background\n",
    "\n",
    "In this how-to guide, you will calibrate the **SNAP (Selective Number-Arbitrary Phase) gate** on a bosonic memory cavity using the `snap_calibration` experiment workflow in the LabOne Q Applications Library.\n",
    "\n",
    "### The SNAP gate\n",
    "\n",
    "The SNAP gate applies an independent phase to each Fock state $|n\\rangle$ of the memory cavity:\n",
    "\n",
    "$$S(\\boldsymbol{\\theta}) = \\sum_n e^{i\\theta_n} |n\\rangle\\langle n|$$\n",
    "\n",
    "It is implemented with two sequential number-selective transmon pulses. Because the dispersive interaction shifts the transmon frequency by $n \\cdot \\chi$ when the cavity contains $n$ photons, a drive at frequency $f_\\mathrm{transmon}(n) = f_\\mathrm{transmon}(0) + n\\chi$ flips the transmon selectively for the $|n\\rangle$ Fock state. Applying two $\\pi$ pulses with a relative drive phase $\\phi$ between them imprints the phase $\\theta_n$ on the cavity without leaving a net transmon excitation: the first $\\pi$ pulse maps $|g,n\\rangle \\to |e,n\\rangle$, and the second $\\pi$ pulse (at drive phase $\\phi$) maps $|e,n\\rangle \\to e^{i\\theta_n(\\phi)}|g,n\\rangle$.\n",
    "\n",
    "The calibration workflow runs in two sequential parts:\n",
    "\n",
    "### Part 1 — Amplitude calibration\n",
    "\n",
    "The workflow calibrates the two-pulse SNAP π amplitude for photon number $n$. The pulse sequence prepares a coherent state $|\\beta\\rangle$ with $|\\beta| = \\sqrt{n}$ in the cavity and sweeps the selective drive amplitude while observing the transmon population:\n",
    "\n",
    "$$\\text{qubit} \\;---\\; [D(|\\beta| = \\sqrt{n})] \\;---\\; [R_x(\\mathrm{amp},\\, n)] \\;---\\; [R_x(\\mathrm{amp},\\, n)] \\;---\\; [\\mathrm{measure}]$$\n",
    "\n",
    "Both pulses rotate the transmon in the same direction. Together they complete a $2\\pi$ rotation at the π amplitude, so the transmon returns to $|g\\rangle$ — giving a minimum in the transmon signal. The analysis fits\n",
    "\n",
    "$$P(e) = \\mathrm{contrast} \\cdot \\sin^2\\!\\left(\\frac{\\pi \\cdot \\mathrm{amp}}{A_\\pi}\\right) + \\mathrm{offset}$$\n",
    "\n",
    "and extracts the SNAP π amplitude $A_\\pi$.\n",
    "\n",
    "### Part 2 — Phase calibration\n",
    "\n",
    "Part 2 maps the SNAP drive phase $\\phi$ to the acquired cavity phase $\\theta_n$. The pulse sequence prepares the cavity in $|\\beta\\rangle$ with $|\\beta| = \\sqrt{n}$, applies the two-pulse SNAP sequence (using the calibrated $A_\\pi$ from Part 1) with a variable drive phase $\\phi$ on the second pulse, and reads out the imprinted cavity phase via Wigner tomography:\n",
    "\n",
    "$$\\text{qubit} \\;---\\; [D(|\\beta| = \\sqrt{n})] \\;---\\; [R_x(A_\\pi,\\, n,\\, \\phi{=}0)] \\;---\\; [R_x(A_\\pi,\\, n,\\, \\phi)] \\;---\\; [W(|\\beta|{=}\\sqrt{n},\\, \\arg\\beta)]$$\n",
    "\n",
    "The Wigner displacement phase $\\arg\\beta$ is swept around the full circle at fixed radius $|\\beta| = \\sqrt{n}$, tracing a Wigner fringe whose angular position $\\theta(\\phi)$ is the imprinted cavity phase. The analysis fits $\\theta(\\phi)$ vs $\\phi$ to a line (expected slope $\\approx \\pm 1$) and records the intercept as the phase calibration constant.\n",
    "\n",
    "### Calibration outputs\n",
    "\n",
    "The workflow produces three calibration values per qubit per photon number $n$:\n",
    "\n",
    "| Parameter | Stored in | Description |\n",
    "|-----------|-----------|-------------|\n",
    "| $A_\\pi$ | `qubit.parameters.snap_drive_amp_per_n[str(n)]` | Two-pulse SNAP π amplitude |\n",
    "| $\\theta_0$ | `qubit.parameters.snap_drive_phase_per_n[str(n)]` | Drive-phase to cavity-phase offset (rad) |\n",
    "| $\\mathrm{sign}(\\mathrm{slope})$ | `qubit.parameters.snap_drive_phase_slope_sign_per_n[str(n)]` | Sign ($\\pm 1$) of the fitted $\\theta(\\phi)$ slope |\n",
    "\n",
    "Once populated, these are used automatically by the `snap` quantum operation."
   ]
  },
  {
   "cell_type": "markdown",
   "id": "a1000004",
   "metadata": {},
   "source": [
    "## Imports\n",
    "\n",
    "You'll start by importing `laboneq.simple`, as well as the `BosonicQubit` and `BosonicQubitOperations` classes from the bosonic qubits contribution."
   ]
  },
  {
   "cell_type": "code",
   "id": "a1000005",
   "metadata": {},
   "source": [
    "import copy\n",
    "\n",
    "import numpy as np\n",
    "from laboneq.simple import *\n",
    "\n",
    "from laboneq_applications.contrib.qpu_types.bosonic_qubits import (\n",
    "    BosonicQubit,\n",
    "    BosonicQubitOperations,\n",
    ")"
   ],
   "outputs": [],
   "execution_count": null
  },
  {
   "cell_type": "markdown",
   "id": "a1000006",
   "metadata": {},
   "source": [
    "## Define your experimental setup\n",
    "\n",
    "Let's define our experimental setup. We will need:\n",
    "\n",
    "- a [DeviceSetup](https://docs.zhinst.com/labone_q_user_manual/core/functionality_and_concepts/00_device_setup/concepts/index.html)\n",
    "- one or more [BosonicQubits](https://docs.zhinst.com/labone_q_user_manual/applications_library/reference/contrib/index.html)\n",
    "- a set of `BosonicQubitOperations`\n",
    "- a [QPU](https://docs.zhinst.com/labone_q_user_manual/core/reference/dsl/quantum.html#laboneq.dsl.quantum.qpu.QPU)\n",
    "\n",
    "Here we will be brief and mainly provide the code to obtain these objects. For full details, see the tutorials on [defining your experimental setup](../../../tutorials/sources/getting_started.ipynb) and [logbooks and data saving](../../../tutorials/sources/logbooks.ipynb).\n",
    "\n",
    "We will use **2 bosonic qubits** in this guide. Change this number to match your setup."
   ]
  },
  {
   "cell_type": "markdown",
   "id": "a1000007",
   "metadata": {},
   "source": [
    "### Instrument serial numbers"
   ]
  },
  {
   "cell_type": "code",
   "id": "a1000008",
   "metadata": {},
   "source": [
    "# Replace with the serial number of your SHFQC instrument\n",
    "SHFQC_SERIAL = \"DEV12073\"\n",
    "\n",
    "DATASERVER_HOST = \"localhost\"\n",
    "DATASERVER_PORT = \"8004\"\n",
    "\n",
    "number_of_qubits = 2"
   ],
   "outputs": [],
   "execution_count": null
  },
  {
   "cell_type": "markdown",
   "id": "a1000009",
   "metadata": {},
   "source": [
    "### DeviceSetup\n",
    "\n",
    "The SNAP calibration experiment requires signal lines for the memory cavity drive, the number-selective transmon drives, and qubit readout. Here we use a single **SHFQC** instrument.\n",
    "\n",
    "Each bosonic qubit uses:\n",
    "- One **QA channel** pair (OUTPUT + INPUT) for readout\n",
    "- One **SG channel** for the memory cavity drive (`drive_memory`) — used both to prepare coherent states via displacement and to perform Wigner tomography displacement in Part 2\n",
    "- One **SG channel** for all number-selective transmon drives (`drive_transmon_at_n0` … `drive_transmon_at_n{max_photon_number}`)\n",
    "- Optionally, one **SG channel** in LF mode for the SWAP drive (`swap_drive`) — not used in this experiment, but included so the same `DeviceSetup` can be reused across all bosonic qubit experiments\n",
    "\n",
    "If you already have a `DeviceSetup`, you can reuse it and skip this cell. Just make sure the signal line names match those expected by `BosonicQubit`."
   ]
  },
  {
   "cell_type": "code",
   "id": "a1000010",
   "metadata": {},
   "source": [
    "# Maximum photon number — determines how many selective transmon signal lines are created.\n",
    "MAX_PHOTON_NUMBER = 5\n",
    "\n",
    "device_setup = DeviceSetup(uid=\"my_bosonic_setup\")\n",
    "device_setup.add_dataserver(\n",
    "    uid=\"my_bosonic_setup\",\n",
    "    host=DATASERVER_HOST,\n",
    "    port=DATASERVER_PORT,\n",
    ")\n",
    "\n",
    "device_setup.add_instruments(\n",
    "    SHFQC(\n",
    "        uid=\"device_shfqc\",\n",
    "        address=SHFQC_SERIAL,\n",
    "        device_options=\"SHFQC/LRT/PLUS/QC6CH/RTR\",\n",
    "    ),\n",
    ")\n",
    "\n",
    "# ── Signal lines for qubit q0 ─────────────────────────────────────────\n",
    "device_setup.add_connections(\n",
    "    \"device_shfqc\",\n",
    "    # Readout (QA channel 0)\n",
    "    create_connection(to_signal=\"q0/measure\", ports=\"QACHANNELS/0/OUTPUT\"),\n",
    "    create_connection(to_signal=\"q0/acquire\", ports=\"QACHANNELS/0/INPUT\"),\n",
    "    # Memory cavity drive (SG channel 0) — displacement and Wigner tomography\n",
    "    create_connection(to_signal=\"q0/drive_memory\", ports=\"SGCHANNELS/0/OUTPUT\"),\n",
    "    # SWAP drive — not used here, included for setup reuse\n",
    "    create_connection(to_signal=\"q0/swap_drive\", ports=\"SGCHANNELS/4/OUTPUT\"),\n",
    ")\n",
    "# Number-selective transmon drive lines for q0 (all share SG channel 2)\n",
    "for n in range(MAX_PHOTON_NUMBER + 1):\n",
    "    device_setup.add_connections(\n",
    "        \"device_shfqc\",\n",
    "        create_connection(\n",
    "            to_signal=f\"q0/drive_transmon_at_n{n}\",\n",
    "            ports=\"SGCHANNELS/2/OUTPUT\",\n",
    "        ),\n",
    "    )\n",
    "\n",
    "# ── Signal lines for qubit q1 (optional) ─────────────────────────────\n",
    "if number_of_qubits == 2:\n",
    "    device_setup.add_connections(\n",
    "        \"device_shfqc\",\n",
    "        create_connection(to_signal=\"q1/measure\", ports=\"QACHANNELS/0/OUTPUT\"),\n",
    "        create_connection(to_signal=\"q1/acquire\", ports=\"QACHANNELS/0/INPUT\"),\n",
    "        create_connection(to_signal=\"q1/drive_memory\", ports=\"SGCHANNELS/1/OUTPUT\"),\n",
    "        create_connection(to_signal=\"q1/swap_drive\", ports=\"SGCHANNELS/5/OUTPUT\"),\n",
    "    )\n",
    "    for n in range(MAX_PHOTON_NUMBER + 1):\n",
    "        device_setup.add_connections(\n",
    "            \"device_shfqc\",\n",
    "            create_connection(\n",
    "                to_signal=f\"q1/drive_transmon_at_n{n}\",\n",
    "                ports=\"SGCHANNELS/3/OUTPUT\",\n",
    "            ),\n",
    "        )"
   ],
   "outputs": [],
   "execution_count": null
  },
  {
   "cell_type": "markdown",
   "id": "a1000011",
   "metadata": {},
   "source": [
    "### BosonicQubits\n",
    "\n",
    "We will create `BosonicQubit` objects from the logical signal groups in our `DeviceSetup` and assign qubit parameters. The qubit UID will match the name of the logical signal group (`q0`, `q1`, …).\n",
    "\n",
    "Adjust the parameter values to reflect the properties of your device. The parameters relevant to SNAP calibration are highlighted in the comments.\n",
    "\n",
    "The `snap_drive_amp_per_n` and `snap_drive_phase_per_n` dictionaries are initially empty — they will be populated by this calibration."
   ]
  },
  {
   "cell_type": "code",
   "id": "a1000012",
   "metadata": {},
   "source": [
    "# Qubit parameters — adjust these to match your device.\n",
    "# Parameters marked (*) are directly relevant to the SNAP calibration experiment.\n",
    "QUBIT_PARAMS = {\n",
    "    # ── Memory cavity ────────────────────────────────────────────────\n",
    "    \"memory_lo_frequency\": 5.8e9,\n",
    "    \"memory_resonance_frequency\": 6.0e9,\n",
    "    \"memory_drive_amplitude\": 0.5,\n",
    "    \"memory_drive_length\": 0.1e-6,  # (*) Displacement pulse duration\n",
    "    \"memory_drive_pulse\": {\"function\": \"gaussian\", \"sigma\": 0.25},\n",
    "    \"memory_spectroscopy_amplitude\": 0.5,\n",
    "    \"memory_spectroscopy_length\": 4e-6,\n",
    "    \"memory_drive_range\": 10,\n",
    "    # ── Ancilla transmon ─────────────────────────────────────────────\n",
    "    \"transmon_lo_frequency\": 4.8e9,\n",
    "    \"transmon_resonance_frequency_at_n0\": 5.0e9,  # (*) From transmon spectroscopy\n",
    "    \"transmon_drive_range\": 10,\n",
    "    # ── Selective transmon Rx drives (*) ─────────────────────────────\n",
    "    # These must be calibrated before running the SNAP calibration.\n",
    "    \"max_photon_number\": MAX_PHOTON_NUMBER,\n",
    "    \"selective_transmon_Rx_length\": 0.08e-6,  # (*) Selective pulse duration\n",
    "    \"selective_transmon_Rx_pulse\": {\"function\": \"gaussian\", \"sigma\": 0.25},\n",
    "    \"selective_transmon_X180_amplitudes\": [0.8]\n",
    "    * (MAX_PHOTON_NUMBER + 1),  # (*) From selective Rx calibration\n",
    "    \"selective_transmon_X90_amplitudes\": [0.4] * (MAX_PHOTON_NUMBER + 1),\n",
    "    # ── SWAP drive ────────────────────────────────────────────────────\n",
    "    # This setup drives the SWAP line from an SG channel, which is put into\n",
    "    # LF mode with its local oscillator at 0 Hz. Leave this out for a SWAP\n",
    "    # line on a baseband channel, such as an HDAWG output.\n",
    "    \"swap_drive_on_sg_channel\": True,\n",
    "    # ── SNAP gate parameters (*) ──────────────────────────────────────\n",
    "    # Initially empty — populated by this calibration workflow.\n",
    "    \"snap_drive_amp_per_n\": {},  # (*) To be calibrated (Part 1)\n",
    "    \"snap_drive_phase_per_n\": {},  # (*) To be calibrated (Part 2)\n",
    "    \"snap_drive_phase_slope_sign_per_n\": {},  # (*) To be calibrated (Part 2)\n",
    "    # ── SWAP gate ─────────────────────────────────────────────────────\n",
    "    \"swap_length\": 500e-9,\n",
    "    \"swap_amplitude\": 0.1,\n",
    "    \"swap_pulse\": {\"function\": \"const\"},\n",
    "    \"swap_drive_range\": 5,\n",
    "    # ── Readout ───────────────────────────────────────────────────────\n",
    "    \"readout_lo_frequency\": 6.8e9,\n",
    "    \"readout_resonator_frequency\": 7.0e9,\n",
    "    \"readout_amplitude\": 0.5,\n",
    "    \"readout_length\": 1e-6,\n",
    "    \"readout_pulse\": {\"function\": \"const\"},\n",
    "    \"readout_integration_length\": 1e-6,\n",
    "    \"readout_integration_delay\": 20e-9,\n",
    "    # ── Reset ─────────────────────────────────────────────────────────\n",
    "    \"reset_delay_length\": 1e-6,\n",
    "    # ── Memory-transmon coupling (*) ──────────────────────────────────\n",
    "    \"chi\": -1e6,  # (*) Dispersive shift in Hz\n",
    "    # ── Displacement calibration (*) ─────────────────────────────────\n",
    "    \"displacement_amp_per_unit_beta\": 0.2,  # (*) Must be calibrated before this experiment\n",
    "}"
   ],
   "outputs": [],
   "execution_count": null
  },
  {
   "cell_type": "code",
   "id": "a1000013",
   "metadata": {},
   "source": [
    "q0 = BosonicQubit.from_logical_signal_group(\n",
    "    uid=\"q0\",\n",
    "    lsg=device_setup.logical_signal_groups[\"q0\"],\n",
    "    parameters=copy.deepcopy(QUBIT_PARAMS),\n",
    ")\n",
    "qubits = [q0]\n",
    "\n",
    "if number_of_qubits == 2:\n",
    "    q1 = BosonicQubit.from_logical_signal_group(\n",
    "        uid=\"q1\",\n",
    "        lsg=device_setup.logical_signal_groups[\"q1\"],\n",
    "        parameters=copy.deepcopy(QUBIT_PARAMS),\n",
    "    )\n",
    "    qubits.append(q1)\n",
    "\n",
    "for q in qubits:\n",
    "    print(\"-------------\")\n",
    "    print(\"Qubit UID:\", q.uid)\n",
    "    print(\"Qubit logical signals:\")\n",
    "    for sig, lsg in q.signals.items():\n",
    "        print(f\"  {sig:<30} ('{lsg}')\")"
   ],
   "outputs": [],
   "execution_count": null
  },
  {
   "cell_type": "markdown",
   "id": "a1000014",
   "metadata": {},
   "source": [
    "### Quantum Operations\n",
    "\n",
    "Create the set of `BosonicQubitOperations`:"
   ]
  },
  {
   "cell_type": "code",
   "id": "a1000015",
   "metadata": {},
   "source": [
    "qops = BosonicQubitOperations()"
   ],
   "outputs": [],
   "execution_count": null
  },
  {
   "cell_type": "markdown",
   "id": "a1000016",
   "metadata": {},
   "source": [
    "### QPU\n",
    "\n",
    "Create the `QPU` object from the qubits and the quantum operations:"
   ]
  },
  {
   "cell_type": "code",
   "id": "a1000017",
   "metadata": {},
   "source": [
    "from laboneq.dsl.quantum import QPU\n",
    "\n",
    "qpu = QPU(qubits, quantum_operations=qops)"
   ],
   "outputs": [],
   "execution_count": null
  },
  {
   "cell_type": "markdown",
   "id": "a1000018",
   "metadata": {},
   "source": [
    "## Connect to Session"
   ]
  },
  {
   "cell_type": "code",
   "id": "a1000019",
   "metadata": {},
   "source": [
    "session = Session(device_setup)\n",
    "session.connect(do_emulation=True)  # set do_emulation=False when at a real setup"
   ],
   "outputs": [],
   "execution_count": null
  },
  {
   "cell_type": "markdown",
   "id": "a1000020",
   "metadata": {},
   "source": [
    "## Create a `FolderStore` for Saving Data\n",
    "\n",
    "The experiment `Workflows` can automatically save the inputs and outputs of all their tasks to the folder path we specify when instantiating the `FolderStore`. Here, we choose the current working directory."
   ]
  },
  {
   "cell_type": "code",
   "id": "a1000021",
   "metadata": {},
   "source": [
    "from pathlib import Path\n",
    "\n",
    "folder_store = workflow.logbook.FolderStore(Path.cwd() / \"data\")"
   ],
   "outputs": [],
   "execution_count": null
  },
  {
   "cell_type": "markdown",
   "id": "f7669f94",
   "metadata": {},
   "source": [
    "We disable saving in this guide. To enable it, simply run `folder_store.activate()`."
   ]
  },
  {
   "cell_type": "code",
   "id": "a1000022",
   "metadata": {},
   "source": [
    "folder_store.deactivate()"
   ],
   "outputs": [],
   "execution_count": null
  },
  {
   "cell_type": "markdown",
   "id": "a1000023",
   "metadata": {},
   "source": [
    "## Optional: Configure the LoggingStore\n",
    "\n",
    "You can also activate/deactivate the `LoggingStore`, which controls the display of `Workflow` logging information in the notebook. See the [tutorial on Recording Experiment Workflow Results](../../../tutorials/sources/logbooks.ipynb) for details.\n",
    "\n",
    "Displaying the `Workflow` logging information is activated by default, but here we deactivate it to shorten the outputs, which are not very meaningful in emulation mode.\n",
    "\n",
    "**We recommend that you do not deactivate the Workflow logging in practice.**"
   ]
  },
  {
   "cell_type": "code",
   "id": "a1000024",
   "metadata": {},
   "source": [
    "from laboneq.workflow.logbook import LoggingStore\n",
    "\n",
    "logging_store = LoggingStore()\n",
    "logging_store.deactivate()"
   ],
   "outputs": [],
   "execution_count": null
  },
  {
   "cell_type": "markdown",
   "id": "a1000025",
   "metadata": {},
   "source": [
    "## Running the Experiment Workflow\n",
    "\n",
    "You'll now instantiate the experiment workflow and run it. For more details on what experiment workflows are and what tasks they execute, see the [Experiment Workflows tutorial](../../../tutorials/sources/experiment_workflows.ipynb).\n",
    "\n",
    "Start by importing the `snap_calibration` experiment workflow from the bosonic qubits contribution, as well as `plot_simulation` for inspecting the pulse sequence."
   ]
  },
  {
   "cell_type": "code",
   "id": "a1000026",
   "metadata": {},
   "source": [
    "from laboneq.contrib.example_helpers.plotting.plot_helpers import plot_simulation\n",
    "\n",
    "from laboneq_applications.contrib.experiments.bosonic_qubits import snap_calibration"
   ],
   "outputs": [],
   "execution_count": null
  },
  {
   "cell_type": "markdown",
   "id": "a1000027",
   "metadata": {},
   "source": [
    "### Configure the experiment options\n",
    "\n",
    "Let's create the options object for the SNAP calibration workflow and inspect it using `workflow.show_fields`:"
   ]
  },
  {
   "cell_type": "code",
   "id": "a1000028",
   "metadata": {},
   "source": [
    "options = snap_calibration.experiment_workflow.options()\n",
    "workflow.show_fields(options)"
   ],
   "outputs": [],
   "execution_count": null
  },
  {
   "cell_type": "markdown",
   "id": "a1000029",
   "metadata": {},
   "source": [
    "Notice that, unless we change it:\n",
    "\n",
    "- The experiment is run in `AcquisitionType.INTEGRATION` and `AveragingMode.CYCLIC`, using 1024 averages (`count`).\n",
    "- The analysis workflows `amplitude_analysis_workflow` and `phase_analysis_workflow` run automatically after each part (`do_analysis=True`).\n",
    "- The figures produced by the analysis are automatically closed (`close_figures=True`)\n",
    "- The qubit parameters will not be updated (`update=False`).\n",
    "- Both Part 1 (amplitude calibration) and Part 2 (phase calibration) are run by default (`do_phase_calibration=True`). Set `do_phase_calibration(False)` to run Part 1 only. Part 2 uses the π amplitudes extracted by the Part 1 analysis, so it runs only when `do_analysis=True`.\n",
    "\n",
    "The experiment sub-options expose additional pulse overrides for advanced use:\n",
    "- `selective_drive_pulse` / `selective_drive_length`: shape and duration overrides for the selective transmon drive pulses.\n",
    "- `memory_drive_pulse` / `memory_drive_length`: shape and duration overrides for the displacement pulse.\n",
    "- `wigner_displacement_pulse` / `wigner_displacement_length`: shape and duration overrides for the Wigner tomography displacement pulse (Part 2 only).\n",
    "- `chunk_count`: number of sweep-point chunks for real-time execution (default: 1).\n",
    "\n",
    "The Part 2 fit options `min_fringe_r2` / `max_slope_dev` set the quality thresholds for the fringe and line fits.\n",
    "\n",
    "Here, let's reduce the number of averages for a quicker run in emulation mode, and disable closing the figures produced by the analysis so we see them in the cell output. Note that the fits attempted by the analysis in emulation mode are not representative, because we do not acquire data from a real experiment."
   ]
  },
  {
   "cell_type": "code",
   "id": "a1000030",
   "metadata": {},
   "source": [
    "options.count(1)\n",
    "options.close_figures(False)"
   ],
   "outputs": [],
   "execution_count": null
  },
  {
   "cell_type": "markdown",
   "id": "a1000031",
   "metadata": {},
   "source": [
    "### Define the photon number and sweep parameters\n",
    "\n",
    "The SNAP calibration is run **one photon number at a time**. Specify the target photon number using the `photon_number_memory` argument.\n",
    "\n",
    "Pass each sweep as a list with one 1-D array per qubit (or a single 1-D array when running on a single qubit). The two phase sweeps also accept a single 1-D array, which is broadcast to all qubits. When multiple qubits are used, all arrays for the same parameter must have the **same number of points**.\n",
    "\n",
    "| Argument | Part | Description |\n",
    "|----------|------|-------------|\n",
    "| `amplitudes_transmon` | 1 | Selective drive amplitude sweep, centred around the selective Rx π amplitude at the target photon number |\n",
    "| `phases_transmon` | 2 | SNAP drive-phase outer sweep; `None` defaults to 13 evenly spaced points over $[0, 2\\pi)$ |\n",
    "| `phases_wigner` | 2 | Wigner displacement-phase inner sweep; `None` defaults to 48 evenly spaced points over $[0, 2\\pi)$ |\n",
    "\n",
    "The recommended practice is to pass all three explicitly so the sweep grids are visible in the notebook:\n"
   ]
  },
  {
   "cell_type": "code",
   "id": "a1000032",
   "metadata": {},
   "source": [
    "# Target photon number for this calibration run.\n",
    "# Repeat with other photon numbers to build up a full SNAP calibration.\n",
    "PHOTON_NUMBER = 1\n",
    "\n",
    "qubit = qpu.quantum_elements[0]\n",
    "\n",
    "# Amplitude sweep (Part 1) — wide enough to contain the selective Rx pi amplitude at the target photon number.\n",
    "amplitudes_q0 = np.linspace(0.1, 0.9, 51)\n",
    "\n",
    "# SNAP drive-phase sweep (Part 2, outer) — 11 points is coarse enough since theta(phi) is linear.\n",
    "phases_transmon_q0 = np.linspace(0, 2 * np.pi, 11, endpoint=False)\n",
    "\n",
    "# Wigner displacement-phase sweep (Part 2, inner) — 51 points for a well-sampled fringe.\n",
    "phases_wigner_q0 = np.linspace(0, 2 * np.pi, 51, endpoint=False)\n",
    "\n",
    "print(f\"Photon number          : {PHOTON_NUMBER}\")\n",
    "print(\n",
    "    f\"Amplitude sweep        : {amplitudes_q0[0]:.3f} to {amplitudes_q0[-1]:.3f} \"\n",
    "    f\"({len(amplitudes_q0)} points)\"\n",
    ")\n",
    "print(\n",
    "    f\"SNAP drive-phase sweep : {phases_transmon_q0[0]:.3f} to {phases_transmon_q0[-1]:.3f} \"\n",
    "    f\"({len(phases_transmon_q0)} points)\"\n",
    ")\n",
    "print(\n",
    "    f\"Wigner phase sweep     : {phases_wigner_q0[0]:.3f} to {phases_wigner_q0[-1]:.3f} \"\n",
    "    f\"({len(phases_wigner_q0)} points)\"\n",
    ")"
   ],
   "outputs": [],
   "execution_count": null
  },
  {
   "cell_type": "code",
   "id": "a1000033",
   "metadata": {},
   "source": [
    "exp_workflow = snap_calibration.experiment_workflow(\n",
    "    session=session,\n",
    "    qpu=qpu,\n",
    "    qubits=[qubit.uid],\n",
    "    amplitudes_transmon=[amplitudes_q0],\n",
    "    phases_transmon=[phases_transmon_q0],\n",
    "    phases_wigner=[phases_wigner_q0],\n",
    "    photon_number_memory=PHOTON_NUMBER,\n",
    "    options=options,\n",
    ")\n",
    "\n",
    "workflow_results = exp_workflow.run()"
   ],
   "outputs": [],
   "execution_count": null
  },
  {
   "cell_type": "markdown",
   "id": "a1000034",
   "metadata": {},
   "source": [
    "### Inspect the Tasks That Were Run\n",
    "\n",
    "The SNAP calibration workflow executes two sets of tasks in sequence:\n",
    "\n",
    "- **Part 1**: `create_amplitude_experiment` → `compile_amplitude_experiment` → `run_amplitude_experiment` → `amplitude_analysis_workflow` (`fit_amplitude_data` → `extract_amplitude_parameters` → `plot_amplitude_calibration`)\n",
    "- **Part 2** (when `do_phase_calibration=True`): `create_phase_experiment` → `compile_phase_experiment` → `run_phase_experiment` → `phase_analysis_workflow` (`fit_phase_data` → `extract_phase_parameters` → `plot_phase_calibration`)\n",
    "\n",
    "With `update=True`, an `update_qpu` task follows each analysis workflow."
   ]
  },
  {
   "cell_type": "code",
   "id": "a1000035",
   "metadata": {},
   "source": [
    "for t in workflow_results.tasks:\n",
    "    print(t)"
   ],
   "outputs": [],
   "execution_count": null
  },
  {
   "cell_type": "markdown",
   "id": "a1000036",
   "metadata": {},
   "source": [
    "### Inspect the Output Simulation (Part 1)\n",
    "\n",
    "You can inspect the compiled Part 1 experiment and plot the simulated output to verify that the pulse sequence looks as expected. The SNAP amplitude calibration sequence consists of a displacement pulse on the `drive_memory` line (for $n > 0$), two sequential selective π pulses on the `drive_transmon_at_n{n}` line, and a readout pulse.\n",
    "\n",
    "The total duration of one real-time repetition is approximately:\n",
    "\n",
    "$$T \\approx t_\\mathrm{displacement} + 2 \\times t_{Rx} + t_\\mathrm{readout} + t_\\mathrm{reset}$$"
   ]
  },
  {
   "cell_type": "code",
   "id": "a1000037",
   "metadata": {},
   "source": [
    "q = qpu.quantum_elements[0]\n",
    "total_time = (\n",
    "    q.parameters.memory_drive_length\n",
    "    + 2 * q.parameters.selective_transmon_Rx_length\n",
    "    + q.parameters.readout_length\n",
    "    + q.parameters.reset_delay_length\n",
    ")\n",
    "\n",
    "compiled_experiment = workflow_results.tasks[\"compile_amplitude_experiment\"].output\n",
    "plot_simulation(\n",
    "    compiled_experiment,\n",
    "    length=total_time * 1.05,\n",
    ")"
   ],
   "outputs": [],
   "execution_count": null
  },
  {
   "cell_type": "markdown",
   "id": "a1000038",
   "metadata": {},
   "source": [
    "### Inspect the Source Code of the Amplitude Pulse-Sequence Creation Task\n",
    "\n",
    "You can inspect the source code of the `create_amplitude_experiment` task to see exactly how the pulse sequence is constructed using quantum operations. To learn more about quantum operations, see the [Quantum Operations tutorial](../../../tutorials/sources/quantum_operations.ipynb)."
   ]
  },
  {
   "cell_type": "code",
   "id": "a1000039",
   "metadata": {},
   "source": [
    "snap_calibration.create_amplitude_experiment.src"
   ],
   "outputs": [],
   "execution_count": null
  },
  {
   "cell_type": "markdown",
   "id": "a1000040",
   "metadata": {},
   "source": [
    "### Inspect the Source Code of the Phase Pulse-Sequence Creation Task\n",
    "\n",
    "Similarly, you can inspect the source code for Part 2:"
   ]
  },
  {
   "cell_type": "code",
   "id": "a1000041",
   "metadata": {},
   "source": [
    "snap_calibration.create_phase_experiment.src"
   ],
   "outputs": [],
   "execution_count": null
  },
  {
   "cell_type": "markdown",
   "id": "a1000042",
   "metadata": {},
   "source": [
    "## Part 1 — Amplitude Calibration\n",
    "\n",
    "### Analyse the Results\n",
    "\n",
    "Because `do_analysis=True` by default, the amplitude analysis workflow ran automatically after Part 1. For the target qubit it performed the following steps:\n",
    "\n",
    "1. Retrieved the transmon signal from the result handle `{qubit_uid}/snap_amp_n{n}`.\n",
    "2. Fitted the two-pulse SNAP Rabi model\n",
    "$$P(e) = \\mathrm{contrast} \\cdot \\sin^2\\!\\left(\\frac{\\pi \\cdot \\mathrm{amp}}{A_\\pi}\\right) + \\mathrm{offset}$$\n",
    "to extract the SNAP π amplitude $A_\\pi$.\n",
    "\n",
    "One plot is generated automatically:\n",
    "- **SNAP amplitude calibration** — the transmon signal versus selective drive amplitude, with the two-pulse Rabi fit and a vertical dashed line marking $A_\\pi$."
   ]
  },
  {
   "cell_type": "markdown",
   "id": "a1000043",
   "metadata": {},
   "source": [
    "### Access the Analysis Outputs"
   ]
  },
  {
   "cell_type": "code",
   "id": "a1000044",
   "metadata": {},
   "source": [
    "# Access the amplitude calibration results.\n",
    "analysis_amp = (\n",
    "    workflow_results.tasks[\"amplitude_analysis_workflow\"]\n",
    "    .tasks[\"fit_amplitude_data\"]\n",
    "    .output\n",
    ")\n",
    "\n",
    "q_uid = qpu.quantum_elements[0].uid\n",
    "q_result_amp = analysis_amp[q_uid]\n",
    "\n",
    "# Keys available in the per-qubit result dict:\n",
    "print(\"Available result keys:\", list(q_result_amp.keys()))"
   ],
   "outputs": [],
   "execution_count": null
  },
  {
   "cell_type": "markdown",
   "id": "a1000045",
   "metadata": {},
   "source": [
    "The per-qubit result dictionary contains:\n",
    "\n",
    "| Key | Description |\n",
    "|-----|-------------|\n",
    "| `\"amp_pi_snap\"` | Dict mapping `str(n)` to the calibrated SNAP π amplitude |\n",
    "| `\"rabi_data\"` | Dict with fit results: `raw_data`, `fit_params`, `fit_success`, `amp_pi`, `fit_x`, `fit_y` |\n",
    "| `\"amplitudes\"` | 1D array of swept selective drive amplitudes |\n",
    "| `\"photon_number\"` | Target photon number $n$ |"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "a1000046",
   "metadata": {},
   "source": [
    "### Extract the Calibrated SNAP π Amplitude"
   ]
  },
  {
   "cell_type": "code",
   "id": "a1000047",
   "metadata": {},
   "source": [
    "amp_pi_snap = q_result_amp[\"amp_pi_snap\"][str(PHOTON_NUMBER)]\n",
    "fit_ok_amp = q_result_amp[\"rabi_data\"][\"fit_success\"]\n",
    "\n",
    "print(f\"Qubit {q_uid}:\")\n",
    "print(f\"  SNAP pi amplitude (n={PHOTON_NUMBER}) : {amp_pi_snap:.4f}\")\n",
    "print(f\"  Fit success             : {fit_ok_amp}\")"
   ],
   "outputs": [],
   "execution_count": null
  },
  {
   "cell_type": "markdown",
   "id": "a1000048",
   "metadata": {},
   "source": [
    "### Update the Qubit Parameters\n",
    "\n",
    "The analysis workflow returns the extracted qubit parameters together with their previous values. Set `options.update(True)` to store the new values in the `qpu` automatically when the workflow runs. Here, we update the `qpu` manually instead. Qubits without a valid fit have no new values and keep their previous parameters.\n",
    "\n",
    "For Part 1, the new value is `snap_drive_amp_per_n`, which maps the photon number (as a string key) to the SNAP π amplitude."
   ]
  },
  {
   "cell_type": "code",
   "id": "a1000049",
   "metadata": {},
   "source": [
    "from pprint import pprint\n",
    "\n",
    "amp_parameters = workflow_results.tasks[\"amplitude_analysis_workflow\"].output\n",
    "pprint(amp_parameters)\n",
    "\n",
    "qpu.update(amp_parameters[\"new_parameter_values\"])"
   ],
   "outputs": [],
   "execution_count": null
  },
  {
   "cell_type": "markdown",
   "id": "a1000050",
   "metadata": {},
   "source": [
    "## Part 2 — Phase Calibration\n",
    "\n",
    "### Analyse the Results\n",
    "\n",
    "<div class=\"admonition note\">\n",
    "<p class=\"admonition-title\"><b>Note</b></p>\n",
    "<div>\n",
    "\n",
    "The Part 2 phase calibration sequence uses a Wigner function measurement to probe the cavity state: after applying a SNAP pulse, the cavity is displaced at amplitude $|\\beta| = \\sqrt{n}$ with varying phase $\\arg(\\beta)$, and the transmon measures the photon-number parity. For a detailed description of the Wigner tomography pulse sequence and how to run it as a standalone experiment, see the [Wigner Tomography](05_wigner_tomography.ipynb) how-to guide.\n",
    "\n",
    "</div>\n",
    "</div>\n",
    "\n",
    "After Part 1, the workflow automatically ran Part 2 using the calibrated SNAP π amplitude. The analysis performed a two-stage fit:\n",
    "\n",
    "**Stage 1** — For each SNAP drive phase $\\phi$, the Wigner fringe\n",
    "$$W(\\arg\\beta) = A \\cdot \\cos(\\arg\\beta - \\theta) + B$$\n",
    "was fitted to extract the fringe angular position $\\theta(\\phi)$. The fit quality $R^2$ is recorded; fringes with $R^2 < $ `min_fringe_r2` (default 0.5) are excluded from Stage 2.\n",
    "\n",
    "**Stage 2** — The extracted fringe angles $\\theta(\\phi)$ were fitted to a line:\n",
    "$$\\theta(\\phi) = \\mathrm{slope} \\cdot \\phi + \\mathrm{intercept}$$\n",
    "The `intercept` is the phase calibration constant. The `slope` is a calibration quality check: a value close to $\\pm 1$ indicates a good calibration; a slope far from $\\pm 1$ suggests a readout or state-preparation problem.\n",
    "\n",
    "Two plots are generated automatically:\n",
    "- **Wigner fringe map** — a 2D colour map of the Wigner signal as a function of SNAP drive phase (y axis) and Wigner displacement phase (x axis), with the extracted fringe angles overlaid as white dots.\n",
    "- **Phase calibration line** — the unwrapped fringe angle $\\theta(\\phi)$ versus drive phase $\\phi$, with the line fit, the slope and intercept annotated."
   ]
  },
  {
   "cell_type": "markdown",
   "id": "a1000051",
   "metadata": {},
   "source": [
    "### Access the Analysis Outputs"
   ]
  },
  {
   "cell_type": "code",
   "id": "a1000052",
   "metadata": {},
   "source": [
    "# Access the phase calibration results.\n",
    "analysis_phase = (\n",
    "    workflow_results.tasks[\"phase_analysis_workflow\"].tasks[\"fit_phase_data\"].output\n",
    ")\n",
    "\n",
    "q_result_phase = analysis_phase[q_uid]\n",
    "\n",
    "# Keys available in the per-qubit result dict:\n",
    "print(\"Available result keys:\", list(q_result_phase.keys()))"
   ],
   "outputs": [],
   "execution_count": null
  },
  {
   "cell_type": "markdown",
   "id": "a1000053",
   "metadata": {},
   "source": [
    "The per-qubit result dictionary contains:\n",
    "\n",
    "| Key | Description |\n",
    "|-----|-------------|\n",
    "| `\"phase_offsets\"` | Dict mapping `str(n)` to the calibration constant (intercept, rad) |\n",
    "| `\"phase_slope_signs\"` | Dict mapping `str(n)` to the sign ($\\pm 1$) of the fitted slope |\n",
    "| `\"calibration\"` | Dict with `slope`, `intercept`, `slope_ok` (bool), `n_good_fringes` (int) |\n",
    "| `\"fringe_angles\"` | 1D array of extracted Wigner fringe angles $\\theta(\\phi)$, one per drive-phase point |\n",
    "| `\"fringe_r2\"` | 1D array of $R^2$ values for the Stage-1 fringe fits |\n",
    "| `\"phases\"` | 1D array of swept SNAP drive phases $\\phi$ (rad) |\n",
    "| `\"phases_wigner\"` | 1D array of swept Wigner displacement phases $\\arg\\beta$ (rad) |\n",
    "| `\"line_fit\"` | Dict with the Stage-2 line-fit details: `slope`, `intercept`, and, if at least two fringes passed, `intercept_raw` and the sorted, unwrapped points `phi_sorted` / `theta_unwrapped` |\n",
    "| `\"data_2d\"` | 2D array of Wigner signal, shape `(len(phases_transmon), len(phases_wigner))` |\n",
    "| `\"photon_number\"` | Target photon number $n$ |"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "a1000054",
   "metadata": {},
   "source": [
    "### Extract the Phase Calibration Constant"
   ]
  },
  {
   "cell_type": "code",
   "id": "a1000055",
   "metadata": {},
   "source": [
    "phase_offset = q_result_phase[\"phase_offsets\"][str(PHOTON_NUMBER)]\n",
    "calibration = q_result_phase[\"calibration\"]\n",
    "slope = calibration[\"slope\"]\n",
    "slope_ok = calibration[\"slope_ok\"]\n",
    "n_good = calibration[\"n_good_fringes\"]\n",
    "n_total = len(q_result_phase[\"phases\"])\n",
    "\n",
    "print(f\"Qubit {q_uid}:\")\n",
    "print(\n",
    "    f\"  Phase offset (n={PHOTON_NUMBER})  : {phase_offset:.4f} rad ({np.degrees(phase_offset):.2f} deg)\"\n",
    ")\n",
    "print(f\"  Slope               : {slope:.4f}\")\n",
    "print(f\"  Slope ok            : {slope_ok}\")\n",
    "print(f\"  Good fringes used   : {n_good} / {n_total}\")"
   ],
   "outputs": [],
   "execution_count": null
  },
  {
   "cell_type": "markdown",
   "id": "a1000056",
   "metadata": {},
   "source": [
    "A `slope_ok = True` result indicates a reliable calibration — the fitted $\\theta(\\phi)$ line has a slope within the expected range of $|\\mathrm{slope}| \\in [1 - \\Delta, 1 + \\Delta]$ where $\\Delta$ is set by the `max_slope_dev` option (default: 0.3).\n",
    "\n",
    "If `slope_ok = False` or `n_good_fringes` is much smaller than the total number of drive-phase points, consider:\n",
    "- Increasing the number of averages for better signal-to-noise on individual Wigner fringes.\n",
    "- Widening the Wigner displacement-phase sweep by increasing the number of points in `phases_wigner`.\n",
    "- Re-running Part 1 with a finer amplitude sweep to verify the SNAP π amplitude."
   ]
  },
  {
   "cell_type": "markdown",
   "id": "a1000057",
   "metadata": {},
   "source": [
    "### Update the Qubit Parameters\n",
    "\n",
    "For Part 2, the new values are `snap_drive_phase_per_n` and `snap_drive_phase_slope_sign_per_n`; they are only extracted when `slope_ok = True`. Together with `snap_drive_amp_per_n`, these fully characterise the SNAP gate at photon number $n$ and are used automatically by the `snap` quantum operation."
   ]
  },
  {
   "cell_type": "code",
   "id": "a1000058",
   "metadata": {},
   "source": [
    "from pprint import pprint\n",
    "\n",
    "phase_parameters = workflow_results.tasks[\"phase_analysis_workflow\"].output\n",
    "pprint(phase_parameters)\n",
    "\n",
    "qpu.update(phase_parameters[\"new_parameter_values\"])\n",
    "\n",
    "qubit = qpu.quantum_elements[0]\n",
    "print(f\"\\nSNAP calibration for n={PHOTON_NUMBER}:\")\n",
    "print(f\"  snap_drive_amp_per_n = {qubit.parameters.snap_drive_amp_per_n}\")\n",
    "print(f\"  snap_drive_phase_per_n = {qubit.parameters.snap_drive_phase_per_n}\")\n",
    "print(\n",
    "    f\"  snap_drive_phase_slope_sign_per_n = \"\n",
    "    f\"{qubit.parameters.snap_drive_phase_slope_sign_per_n}\"\n",
    ")"
   ],
   "outputs": [],
   "execution_count": null
  },
  {
   "cell_type": "markdown",
   "id": "a1000059",
   "metadata": {},
   "source": [
    "## Calibrating Additional Photon Numbers\n",
    "\n",
    "To calibrate the SNAP gate for additional Fock states, repeat the workflow for each target photon number. Here we set `update=True`, so each run stores its calibrated values in the `qpu`. The loop below iterates over `[1, 2]` as an example — adjust the list and amplitude sweep to match your device."
   ]
  },
  {
   "cell_type": "code",
   "id": "a1000060",
   "metadata": {},
   "source": [
    "qubit = qpu.quantum_elements[0]\n",
    "options.update(True)\n",
    "\n",
    "# Phase sweeps are the same for every photon number in this example.\n",
    "phases_transmon = np.linspace(0, 2 * np.pi, 13, endpoint=False)\n",
    "phases_wigner = np.linspace(0, 2 * np.pi, 48, endpoint=False)\n",
    "\n",
    "for n in [1, 2]:\n",
    "    amps = np.linspace(0.1, 0.9, 51)\n",
    "\n",
    "    wf = snap_calibration.experiment_workflow(\n",
    "        session=session,\n",
    "        qpu=qpu,\n",
    "        qubits=[qubit.uid],\n",
    "        amplitudes_transmon=[amps],\n",
    "        phases_transmon=[phases_transmon],\n",
    "        phases_wigner=[phases_wigner],\n",
    "        photon_number_memory=n,\n",
    "        options=options,\n",
    "    )\n",
    "    res = wf.run()\n",
    "\n",
    "    for part in (\"amplitude\", \"phase\"):\n",
    "        new = res.tasks[f\"{part}_analysis_workflow\"].output[\"new_parameter_values\"]\n",
    "        status = \"updated\" if new[qubit.uid] else \"FAILED, not updated\"\n",
    "        print(f\"n={n}: {part} calibration {status}\")\n",
    "\n",
    "print(\"\\nFinal snap_drive_amp_per_n  :\", qubit.parameters.snap_drive_amp_per_n)\n",
    "print(\"Final snap_drive_phase_per_n:\", qubit.parameters.snap_drive_phase_per_n)\n",
    "print(\n",
    "    \"Final snap_drive_phase_slope_sign_per_n:\",\n",
    "    qubit.parameters.snap_drive_phase_slope_sign_per_n,\n",
    ")"
   ],
   "outputs": [],
   "execution_count": null
  },
  {
   "cell_type": "markdown",
   "id": "a1000061",
   "metadata": {},
   "source": [
    "## Parallel SNAP Calibration\n",
    "\n",
    "The `snap_calibration` workflow natively supports running multiple qubits in parallel. When you pass a list of qubit UIDs together with corresponding lists of amplitude arrays, the pulse sequences for all qubits are compiled into a single experiment and executed simultaneously on the hardware.\n",
    "\n",
    "Each qubit gets its own independently chosen amplitude sweep, so qubits with different SNAP π amplitudes can be characterised in one shot. The only constraint is that all amplitude arrays must have the **same number of points**, because the hardware sweeps them in lock-step. Both qubits must also share the same `photon_number_memory`.\n",
    "\n",
    "Here we demonstrate a parallel calibration at photon number `1` on both qubits:"
   ]
  },
  {
   "cell_type": "code",
   "id": "a1000062",
   "metadata": {},
   "source": [
    "q0 = qpu.quantum_elements[0]\n",
    "q1 = qpu.quantum_elements[1]\n",
    "\n",
    "PHOTON_NUMBER = 1\n",
    "\n",
    "# Each qubit gets its own amplitude sweep; the ranges may differ between qubits.\n",
    "amplitudes_q0 = np.linspace(0.1, 0.9, 51)\n",
    "amplitudes_q1 = np.linspace(0.1, 0.9, 51)\n",
    "\n",
    "# Phase sweeps are shared across qubits here, but can be set per-qubit if needed.\n",
    "phases_transmon_q0 = np.linspace(0, 2 * np.pi, 13, endpoint=False)\n",
    "phases_transmon_q1 = np.linspace(0, 2 * np.pi, 13, endpoint=False)\n",
    "\n",
    "phases_wigner_q0 = np.linspace(0, 2 * np.pi, 48, endpoint=False)\n",
    "phases_wigner_q1 = np.linspace(0, 2 * np.pi, 48, endpoint=False)\n",
    "\n",
    "exp_workflow_2q = snap_calibration.experiment_workflow(\n",
    "    session=session,\n",
    "    qpu=qpu,\n",
    "    qubits=[q0.uid, q1.uid],\n",
    "    amplitudes_transmon=[amplitudes_q0, amplitudes_q1],\n",
    "    phases_transmon=[phases_transmon_q0, phases_transmon_q1],\n",
    "    phases_wigner=[phases_wigner_q0, phases_wigner_q1],\n",
    "    photon_number_memory=PHOTON_NUMBER,\n",
    "    options=options,\n",
    ")\n",
    "\n",
    "workflow_results_2q = exp_workflow_2q.run()"
   ],
   "outputs": [],
   "execution_count": null
  },
  {
   "cell_type": "markdown",
   "id": "a1000063",
   "metadata": {},
   "source": [
    "### Check the Updated Qubit Parameters\n",
    "\n",
    "Because `update=True` is still set in `options`, the workflow stored the new parameter values in the `qpu`. Qubits without a valid fit keep their previous parameters:"
   ]
  },
  {
   "cell_type": "code",
   "id": "a1000064",
   "metadata": {},
   "source": [
    "for part in (\"amplitude\", \"phase\"):\n",
    "    new = workflow_results_2q.tasks[f\"{part}_analysis_workflow\"].output\n",
    "    pprint(new[\"new_parameter_values\"])\n",
    "\n",
    "for q in [q0, q1]:\n",
    "    print(f\"{q.uid}: snap_drive_amp_per_n = {q.parameters.snap_drive_amp_per_n}\")\n",
    "    print(f\"{q.uid}: snap_drive_phase_per_n = {q.parameters.snap_drive_phase_per_n}\")"
   ],
   "outputs": [],
   "execution_count": null
  },
  {
   "cell_type": "markdown",
   "id": "a1000065",
   "metadata": {},
   "source": [
    "Great! You have now calibrated the SNAP gate for the target photon number on all qubits. The `snap_drive_amp_per_n`, `snap_drive_phase_per_n` and `snap_drive_phase_slope_sign_per_n` parameters are stored in the qubit objects and will be used by the `snap` quantum operation in subsequent experiments.\n",
    "\n",
    "Repeat this calibration for each additional photon number $n$ you intend to use in SNAP gate sequences. Once all required Fock states are calibrated, the `snap` quantum operation can apply arbitrary independent phases to each cavity Fock state in a single compiled sequence."
   ]
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "Python 3 (ipykernel)",
   "language": "python",
   "name": "python3"
  },
  "language_info": {
   "codemirror_mode": {
    "name": "ipython",
    "version": 3
   },
   "file_extension": ".py",
   "mimetype": "text/x-python",
   "name": "python",
   "nbconvert_exporter": "python",
   "pygments_lexer": "ipython3",
   "version": "3.12.13"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 5
}
