{
 "cells": [
  {
   "cell_type": "markdown",
   "id": "a1000001",
   "metadata": {},
   "source": [
    "# SWAP 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 SWAP 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",
    "- **Transmon Rabi calibration**: the steady-state transmon $\\pi$-pulse amplitude `selective_transmon_X180_amplitudes[0]` must be calibrated.\n",
    "- A rough estimate of the SWAP coupling strength (from device design or coarse spectroscopy) is needed to choose appropriate sweep ranges for `swap_durations` and `swap_amplitudes`.\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 **SWAP gate** between the ancilla transmon and the memory cavity of a bosonic qubit using the `swap_calibration` experiment workflow in the LabOne Q Applications Library.\n",
    "\n",
    "### The SWAP gate\n",
    "\n",
    "The SWAP gate coherently exchanges a single excitation between the ancilla transmon and the memory cavity:\n",
    "\n",
    "$$|e,\\, n\\rangle \\;\\leftrightarrow\\; |g,\\, n+1\\rangle$$\n",
    "\n",
    "When combined with a preceding transmon $\\pi$ pulse, the SWAP sequence prepares a $|1\\rangle$ Fock state in the cavity starting from the joint ground state:\n",
    "\n",
    "$$|g,\\, 0\\rangle \\xrightarrow{\\pi} |e,\\, 0\\rangle \\xrightarrow{\\mathrm{SWAP}} |g,\\, 1\\rangle$$\n",
    "\n",
    "The SWAP is implemented as a DC flux pulse on a dedicated SG channel in LF mode, which tunes the transmon frequency into resonance with the memory cavity for a controlled duration $\\tau$.\n",
    "\n",
    "### The pulse sequence\n",
    "\n",
    "The pulse sequence applied to each qubit is:\n",
    "\n",
    "$$\\text{qubit} \\;---\\; [R_x(\\pi)_{n=0}] \\;---\\; [\\mathrm{SWAP}(\\mathrm{amp},\\,\\tau)] \\;---\\; [\\mathrm{measure}]$$\n",
    "\n",
    "The transmon signal oscillates between $|e\\rangle$ and $|g\\rangle$ as a function of the SWAP duration $\\tau$ and drive amplitude. The first minimum of this oscillation — where the excitation is fully transferred to the cavity — gives the calibrated SWAP parameters.\n",
    "\n",
    "### Calibration modes\n",
    "\n",
    "The workflow supports three modes, selected automatically based on which sweep parameters you provide:\n",
    "\n",
    "1. **Length sweep** (`swap_durations` only) — the SWAP pulse duration $\\tau$ is swept in near time at a fixed drive amplitude. The analysis fits a damped cosine\n",
    "\n",
    "$$s(\\tau) = A \\cdot \\cos\\!\\left(2\\pi f \\tau + \\varphi\\right) \\cdot e^{-\\tau / T} + B$$\n",
    "\n",
    "and extracts `t_swap`, the duration at the first minimum (half-period) where the transmon is fully depopulated.\n",
    "\n",
    "2. **Amplitude sweep** (`swap_amplitudes` only) — the SWAP drive amplitude is swept in real time at a fixed pulse duration. The same damped cosine model is fitted and `amp_swap`, the amplitude at the first complete excitation transfer, is extracted.\n",
    "\n",
    "3. **2D chevron sweep** (both `swap_durations` and `swap_amplitudes`) — duration is swept in near time and amplitude in real time, mapping out a chevron pattern. The analysis fits each amplitude row independently, identifies the amplitude with the highest exchange frequency as the optimal operating point, and returns both `amp_swap` and the corresponding `t_swap`."
   ]
  },
  {
   "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 SWAP calibration experiment requires signal lines for the ancilla transmon drive, the SWAP drive, 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 number-selective transmon drives (`drive_transmon_at_n0` … `drive_transmon_at_n{max_photon_number}`)\n",
    "- One **SG channel** in LF mode for the SWAP drive (`swap_drive`)\n",
    "- Optionally, one **SG channel** for the memory cavity drive (`drive_memory`)\n",
    "\n",
    "The SWAP calibration sequence uses only the `drive_transmon_at_n0`, `swap_drive`, `measure`, and `acquire` lines. The `drive_memory` line is included here so that 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`."
   ]
  },
  {
   "metadata": {},
   "cell_type": "code",
   "outputs": [],
   "execution_count": null,
   "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) — not used in SWAP calibration\n",
    "    create_connection(to_signal=\"q0/drive_memory\", ports=\"SGCHANNELS/0/OUTPUT\"),\n",
    "    # SWAP drive — SG channel in LF mode (DC flux pulse)\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",
    "        )"
   ],
   "id": "2078d9d787f0326a"
  },
  {
   "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 SWAP calibration are highlighted in the comments."
   ]
  },
  {
   "cell_type": "code",
   "id": "a1000012",
   "metadata": {},
   "source": [
    "# Qubit parameters — adjust these to match your device.\n",
    "# Parameters marked (*) are directly relevant to the SWAP 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,\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,  # (*) Transmon freq when cavity in |0>\n",
    "    \"transmon_drive_range\": 10,\n",
    "    # ── Selective transmon Rx drives (*) ─────────────────────────────\n",
    "    # The x180 at n=0 is used as the pi pulse before the SWAP.\n",
    "    \"max_photon_number\": MAX_PHOTON_NUMBER,\n",
    "    \"selective_transmon_Rx_length\": 0.08e-6,  # (*) pi-pulse duration\n",
    "    \"selective_transmon_Rx_pulse\": {\"function\": \"gaussian\", \"sigma\": 0.25},\n",
    "    \"selective_transmon_X180_amplitudes\": [0.8]\n",
    "    * (MAX_PHOTON_NUMBER + 1),  # (*) pi amplitudes\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",
    "    # ── SWAP gate (*) ─────────────────────────────────────────────────\n",
    "    # Initial guesses for the SWAP parameters.\n",
    "    # After running this experiment they will be updated with the calibrated values.\n",
    "    \"swap_length\": 500e-9,  # (*) Initial SWAP duration guess (seconds)\n",
    "    \"swap_amplitude\": 0.1,  # (*) Initial SWAP drive amplitude guess\n",
    "    \"swap_pulse\": {\"function\": \"const\"},  # (*) Rectangular DC flux pulse\n",
    "    \"swap_drive_range\": 5,  # (*) Hardware output range of the LF channel (V)\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,\n",
    "    # ── Displacement calibration result ───────────────────────────────\n",
    "    \"displacement_amp_per_unit_beta\": None,\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": "418ee3c6",
   "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 `swap_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 swap_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 SWAP calibration experiment and inspect it using `workflow.show_fields`:"
   ]
  },
  {
   "cell_type": "code",
   "id": "a1000028",
   "metadata": {},
   "source": [
    "options = swap_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 workflow runs automatically after the experiment (`do_analysis=True`). It fits a damped cosine to the transmon oscillation and returns the calibrated `t_swap` and/or `amp_swap` for each qubit.\n",
    "- The figures produced by the analysis are automatically closed (`close_figures=True`)\n",
    "- The qubit parameters will not be updated (`update=False`).\n",
    "\n",
    "The `create_experiment` sub-options expose additional pulse overrides for advanced use:\n",
    "- `swap_pulse`: pulse shape override for the SWAP drive (defaults to a constant pulse, i.e., a rectangular DC flux pulse).\n",
    "- `chunk_count`: number of chunks for splitting the real-time amplitude sweep.\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": [
    "## Mode 1: Length Sweep\n",
    "\n",
    "In the length sweep mode, the SWAP pulse duration is swept in near time at a fixed drive amplitude (taken from `qubit.parameters.swap_amplitude`). This is the fastest way to measure the exchange frequency and find `t_swap` when the approximate drive amplitude is already known.\n",
    "\n",
    "### Define the SWAP duration sweep\n",
    "\n",
    "Choose a range that covers at least one full oscillation period. If the transmon-cavity exchange coupling $g / 2\\pi$ (in Hz) is known from device design, a good starting range is $[0,\\, 1/(g/2\\pi)]$, which covers two full exchange periods. Full transfer $|e,0\\rangle \\to |g,1\\rangle$ happens at `t_swap` $= \\pi / (2g) = 1 / (4 \\cdot g/2\\pi)$."
   ]
  },
  {
   "cell_type": "code",
   "id": "a1000032",
   "metadata": {},
   "source": [
    "qubit = qpu.quantum_elements[0]\n",
    "\n",
    "# SWAP duration sweep — adjust range to cover at least one full oscillation period.\n",
    "swap_durations_q0 = np.linspace(100e-9, 600e-9, 21)\n",
    "\n",
    "print(\n",
    "    f\"Duration sweep: {swap_durations_q0[0] * 1e9:.0f} to {swap_durations_q0[-1] * 1e9:.0f} ns \"\n",
    "    f\"({len(swap_durations_q0)} points)\"\n",
    ")"
   ],
   "outputs": [],
   "execution_count": null
  },
  {
   "cell_type": "code",
   "id": "a1000033",
   "metadata": {},
   "source": [
    "# Run the length sweep on a single qubit\n",
    "exp_workflow_length = swap_calibration.experiment_workflow(\n",
    "    session=session,\n",
    "    qpu=qpu,\n",
    "    qubits=[qubit.uid],\n",
    "    swap_durations=[swap_durations_q0],\n",
    "    options=options,\n",
    ")\n",
    "\n",
    "workflow_results_length = exp_workflow_length.run()"
   ],
   "outputs": [],
   "execution_count": null
  },
  {
   "cell_type": "markdown",
   "id": "a1000034",
   "metadata": {},
   "source": [
    "### Inspect the Tasks That Were Run\n",
    "\n",
    "The SWAP calibration workflow executes four steps in sequence: `create_experiment`, `compile_experiment`, `run_experiment`, and (when `do_analysis=True`) the `analysis_workflow`, which runs the `fit_data`, `extract_qubit_parameters` and plotting tasks. With `update=True`, an `update_qpu` task then stores the new parameter values in the `qpu`."
   ]
  },
  {
   "cell_type": "code",
   "id": "a1000035",
   "metadata": {},
   "source": [
    "for t in workflow_results_length.tasks:\n",
    "    print(t)"
   ],
   "outputs": [],
   "execution_count": null
  },
  {
   "cell_type": "markdown",
   "id": "a1000036",
   "metadata": {},
   "source": [
    "### Inspect the Output Simulation\n",
    "\n",
    "You can inspect the compiled experiment and plot the simulated output to verify that the pulse sequence looks as expected. The SWAP calibration sequence consists of a transmon $\\pi$ pulse on the `drive_transmon_at_n0` line, followed by a rectangular DC flux pulse on the `swap_drive` LF line, and a readout pulse.\n",
    "\n",
    "The total duration of one repetition is approximately:\n",
    "\n",
    "$$T \\approx t_{\\pi} + t_\\mathrm{SWAP} + t_\\mathrm{readout} + t_\\mathrm{reset}$$\n",
    "\n",
    "We plot the first near-time step (at the first SWAP duration in the sweep) to inspect the pulse shapes, and extend the window to the longest SWAP duration to give a sense of the full scale."
   ]
  },
  {
   "cell_type": "code",
   "id": "a1000037",
   "metadata": {},
   "source": [
    "q = qpu.quantum_elements[0]\n",
    "total_time = (\n",
    "    q.parameters.selective_transmon_Rx_length\n",
    "    + swap_durations_q0[-1]  # use the longest SWAP duration for the window\n",
    "    + q.parameters.readout_length\n",
    "    + q.parameters.reset_delay_length\n",
    ")\n",
    "\n",
    "compiled_experiment = workflow_results_length.tasks[\"compile_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 Pulse-Sequence Creation Task\n",
    "\n",
    "You can inspect the source code of the `create_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": [
    "swap_calibration.create_experiment.src"
   ],
   "outputs": [],
   "execution_count": null
  },
  {
   "cell_type": "markdown",
   "id": "a1000040",
   "metadata": {},
   "source": [
    "### Analyse the Results\n",
    "\n",
    "Because `do_analysis=True` by default, the analysis workflow ran automatically after the experiment. For the length sweep it performed the following steps for each qubit:\n",
    "\n",
    "1. Retrieved the transmon signal from the result handle `{qubit_uid}/swap_cal`.\n",
    "2. Fitted the damped cosine model $A \\cdot \\cos(2\\pi f \\tau + \\varphi) \\cdot e^{-\\tau/T} + B$ using an FFT-based frequency guess as the starting point.\n",
    "3. Located the first minimum of the fitted curve at $\\tau > 0$, corresponding to the duration at which the transmon population is fully transferred to the cavity. This is `t_swap`.\n",
    "\n",
    "One plot is generated automatically:\n",
    "- **SWAP length calibration** — the transmon signal versus SWAP duration, with the fitted damped cosine and a vertical dashed line marking `t_swap`."
   ]
  },
  {
   "cell_type": "markdown",
   "id": "a1000041",
   "metadata": {},
   "source": [
    "### Access the Analysis Outputs"
   ]
  },
  {
   "cell_type": "code",
   "id": "a1000042",
   "metadata": {},
   "source": [
    "# Access the fit results of the analysis workflow.\n",
    "analysis_result_length = (\n",
    "    workflow_results_length.tasks[\"analysis_workflow\"].tasks[\"fit_data\"].output\n",
    ")\n",
    "\n",
    "q_uid = qpu.quantum_elements[0].uid\n",
    "q_result_length = analysis_result_length[q_uid]\n",
    "\n",
    "# Keys available in the per-qubit result dict (length mode):\n",
    "print(\"Available result keys:\", list(q_result_length.keys()))"
   ],
   "outputs": [],
   "execution_count": null
  },
  {
   "cell_type": "markdown",
   "id": "a1000043",
   "metadata": {},
   "source": [
    "The per-qubit result dictionary contains:\n",
    "\n",
    "| Key | Description |\n",
    "|-----|-------------|\n",
    "| `\"mode\"` | `\"length\"` — the sweep mode used |\n",
    "| `\"t_swap\"` | Calibrated SWAP duration (seconds) |\n",
    "| `\"frequency\"` | Fitted exchange frequency $f$ (Hz); for a fitted phase $\\varphi \\approx 0$, $t_\\mathrm{swap} \\approx 1 / (2f)$ |\n",
    "| `\"raw_data\"` | 1D array of transmon signal vs. duration |\n",
    "| `\"durations\"` | Duration sweep axis (seconds) |\n",
    "| `\"fit_params\"` | Fitted parameters `[A, f, φ, T, B]` of the damped cosine |\n",
    "| `\"fit_success\"` | `True` if the fit converged |\n",
    "| `\"fit_x\"`, `\"fit_y\"` | Dense fit curve arrays for custom plotting |"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "a1000044",
   "metadata": {},
   "source": [
    "### Extract `t_swap`"
   ]
  },
  {
   "cell_type": "code",
   "id": "a1000045",
   "metadata": {},
   "source": [
    "t_swap = q_result_length[\"t_swap\"]\n",
    "f_exchange = q_result_length[\"frequency\"]\n",
    "fit_ok = q_result_length[\"fit_success\"]\n",
    "\n",
    "print(f\"Qubit {q_uid}:\")\n",
    "print(f\"  t_swap       = {t_swap * 1e9:.2f} ns\")\n",
    "print(f\"  f_exchange   = {f_exchange / 1e6:.3f} MHz\")\n",
    "print(f\"  Fit success  : {fit_ok}\")"
   ],
   "outputs": [],
   "execution_count": null
  },
  {
   "cell_type": "markdown",
   "id": "a1000046",
   "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 a length sweep, the new value is `swap_length`. Once updated, the `swap` quantum operation uses this duration by default when no explicit `length` argument is passed."
   ]
  },
  {
   "cell_type": "code",
   "id": "a1000047",
   "metadata": {},
   "source": [
    "from pprint import pprint\n",
    "\n",
    "qubit_parameters = workflow_results_length.tasks[\"analysis_workflow\"].output\n",
    "pprint(qubit_parameters)\n",
    "\n",
    "qpu.update(qubit_parameters[\"new_parameter_values\"])"
   ],
   "outputs": [],
   "execution_count": null
  },
  {
   "cell_type": "markdown",
   "id": "a1000048",
   "metadata": {},
   "source": [
    "## Mode 2: Amplitude Sweep\n",
    "\n",
    "In the amplitude sweep mode, the SWAP drive amplitude is swept in real time at a fixed pulse duration (taken from `qubit.parameters.swap_length`). This mode is useful for fine-tuning the drive amplitude once a good `t_swap` is already known — for example, after a length sweep has established the exchange frequency.\n",
    "\n",
    "### Define the SWAP amplitude sweep\n",
    "\n",
    "Choose a range that spans the first oscillation in the transmon signal as a function of drive amplitude. The appropriate range depends on the coupling strength and the LF channel output range of your device."
   ]
  },
  {
   "cell_type": "code",
   "id": "a1000049",
   "metadata": {},
   "source": [
    "# SWAP amplitude sweep — adjust range to cover at least one full oscillation.\n",
    "swap_amplitudes_q0 = np.linspace(0.05, 0.2, 21)\n",
    "\n",
    "print(\n",
    "    f\"Amplitude sweep: {swap_amplitudes_q0[0]:.3f} to {swap_amplitudes_q0[-1]:.3f} \"\n",
    "    f\"({len(swap_amplitudes_q0)} points)\"\n",
    ")"
   ],
   "outputs": [],
   "execution_count": null
  },
  {
   "cell_type": "code",
   "id": "a1000050",
   "metadata": {},
   "source": [
    "# Run the amplitude sweep on a single qubit\n",
    "exp_workflow_amp = swap_calibration.experiment_workflow(\n",
    "    session=session,\n",
    "    qpu=qpu,\n",
    "    qubits=[qubit.uid],\n",
    "    swap_amplitudes=[swap_amplitudes_q0],\n",
    "    options=options,\n",
    ")\n",
    "\n",
    "workflow_results_amp = exp_workflow_amp.run()"
   ],
   "outputs": [],
   "execution_count": null
  },
  {
   "cell_type": "markdown",
   "id": "a1000051",
   "metadata": {},
   "source": [
    "### Analyse the Results\n",
    "\n",
    "The analysis fits the same damped cosine model to the transmon signal versus drive amplitude. The first minimum gives `amp_swap`, the drive amplitude at which the excitation is fully transferred to the cavity.\n",
    "\n",
    "One plot is generated automatically:\n",
    "- **SWAP amplitude calibration** — the transmon signal versus drive amplitude, with the fitted damped cosine and a vertical dashed line marking `amp_swap`."
   ]
  },
  {
   "cell_type": "markdown",
   "id": "a1000052",
   "metadata": {},
   "source": [
    "### Access the Analysis Outputs"
   ]
  },
  {
   "cell_type": "code",
   "id": "a1000053",
   "metadata": {},
   "source": [
    "analysis_result_amp = (\n",
    "    workflow_results_amp.tasks[\"analysis_workflow\"].tasks[\"fit_data\"].output\n",
    ")\n",
    "\n",
    "q_result_amp = analysis_result_amp[q_uid]\n",
    "print(\"Available result keys:\", list(q_result_amp.keys()))"
   ],
   "outputs": [],
   "execution_count": null
  },
  {
   "cell_type": "markdown",
   "id": "a1000054",
   "metadata": {},
   "source": [
    "The per-qubit result dictionary for the amplitude mode contains:\n",
    "\n",
    "| Key | Description |\n",
    "|-----|-------------|\n",
    "| `\"mode\"` | `\"amplitude\"` — the sweep mode used |\n",
    "| `\"amp_swap\"` | Calibrated SWAP drive amplitude |\n",
    "| `\"rabi_rate_per_unit_amp\"` | Fitted exchange rate per unit amplitude (oscillation cycles per unit amplitude) |\n",
    "| `\"raw_data\"` | 1D array of transmon signal vs. amplitude |\n",
    "| `\"amplitudes\"` | Amplitude sweep axis |\n",
    "| `\"fit_params\"` | Fitted parameters `[A, f, φ, T, B]` of the damped cosine |\n",
    "| `\"fit_success\"` | `True` if the fit converged |\n",
    "| `\"fit_x\"`, `\"fit_y\"` | Dense fit curve arrays for custom plotting |"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "a1000055",
   "metadata": {},
   "source": [
    "### Extract `amp_swap`"
   ]
  },
  {
   "cell_type": "code",
   "id": "a1000056",
   "metadata": {},
   "source": [
    "amp_swap = q_result_amp[\"amp_swap\"]\n",
    "rabi_rate = q_result_amp[\"rabi_rate_per_unit_amp\"]\n",
    "fit_ok = q_result_amp[\"fit_success\"]\n",
    "\n",
    "print(f\"Qubit {q_uid}:\")\n",
    "print(f\"  amp_swap                 = {amp_swap:.4f}\")\n",
    "print(f\"  rabi_rate_per_unit_amp   = {rabi_rate:.3f} cycles / amplitude unit\")\n",
    "print(f\"  Fit success              : {fit_ok}\")"
   ],
   "outputs": [],
   "execution_count": null
  },
  {
   "cell_type": "markdown",
   "id": "a1000057",
   "metadata": {},
   "source": [
    "### Update the Qubit Parameters\n",
    "\n",
    "For an amplitude sweep, the new value is `swap_amplitude`:"
   ]
  },
  {
   "cell_type": "code",
   "id": "a1000058",
   "metadata": {},
   "source": [
    "from pprint import pprint\n",
    "\n",
    "qubit_parameters = workflow_results_amp.tasks[\"analysis_workflow\"].output\n",
    "pprint(qubit_parameters)\n",
    "\n",
    "qpu.update(qubit_parameters[\"new_parameter_values\"])"
   ],
   "outputs": [],
   "execution_count": null
  },
  {
   "cell_type": "markdown",
   "id": "a1000059",
   "metadata": {},
   "source": [
    "## Mode 3: 2D Chevron Sweep\n",
    "\n",
    "The 2D chevron sweep maps the transmon signal across the full (duration, amplitude) plane, revealing the characteristic chevron pattern of the coherent exchange interaction. This mode is the most comprehensive calibration approach: it simultaneously finds the optimal drive amplitude and the corresponding SWAP duration without requiring prior knowledge of either.\n",
    "\n",
    "The analysis fits each amplitude row of the 2D data with a damped cosine and extracts the exchange frequency. The row with the highest exchange frequency defines the optimal operating point: `amp_swap` is the amplitude of that row and `t_swap` is its fitted half-period.\n",
    "\n",
    "### Define both sweeps\n",
    "\n",
    "The outer (near-time) sweep is over durations and the inner (real-time) sweep is over amplitudes. For a clear chevron map, use at least 21 amplitude points and 21 duration points."
   ]
  },
  {
   "cell_type": "code",
   "id": "a1000060",
   "metadata": {},
   "source": [
    "# 2D chevron sweep — adjust both ranges to cover the expected exchange oscillation.\n",
    "# Few sweep points keep the emulated run short; use >= 21 points per axis on hardware.\n",
    "swap_durations_2d_q0 = np.linspace(100e-9, 600e-9, 5)\n",
    "swap_amplitudes_2d_q0 = np.linspace(0.05, 0.2, 2)\n",
    "\n",
    "print(\n",
    "    f\"Duration sweep : {swap_durations_2d_q0[0] * 1e9:.0f} to \"\n",
    "    f\"{swap_durations_2d_q0[-1] * 1e9:.0f} ns ({len(swap_durations_2d_q0)} points)\"\n",
    ")\n",
    "print(\n",
    "    f\"Amplitude sweep: {swap_amplitudes_2d_q0[0]:.3f} to \"\n",
    "    f\"{swap_amplitudes_2d_q0[-1]:.3f} ({len(swap_amplitudes_2d_q0)} points)\"\n",
    ")"
   ],
   "outputs": [],
   "execution_count": null
  },
  {
   "cell_type": "code",
   "id": "a1000061",
   "metadata": {},
   "source": [
    "# Run the 2D chevron sweep on a single qubit\n",
    "exp_workflow_2d = swap_calibration.experiment_workflow(\n",
    "    session=session,\n",
    "    qpu=qpu,\n",
    "    qubits=[qubit.uid],\n",
    "    swap_durations=[swap_durations_2d_q0],\n",
    "    swap_amplitudes=[swap_amplitudes_2d_q0],\n",
    "    options=options,\n",
    ")\n",
    "\n",
    "workflow_results_2d = exp_workflow_2d.run()"
   ],
   "outputs": [],
   "execution_count": null
  },
  {
   "cell_type": "markdown",
   "id": "a1000062",
   "metadata": {},
   "source": [
    "### Analyse the Results\n",
    "\n",
    "The 2D analysis fits each amplitude row independently and selects the row with the highest exchange frequency as the optimal operating point.\n",
    "\n",
    "One plot is generated automatically:\n",
    "- **SWAP chevron (2D)** — a colour map of the transmon signal as a function of SWAP duration (x axis) and drive amplitude (y axis), with dashed lines marking `amp_swap` and `t_swap`, and a cross at their intersection."
   ]
  },
  {
   "cell_type": "markdown",
   "id": "a1000063",
   "metadata": {},
   "source": [
    "### Access the Analysis Outputs"
   ]
  },
  {
   "cell_type": "code",
   "id": "a1000064",
   "metadata": {},
   "source": [
    "analysis_result_2d = (\n",
    "    workflow_results_2d.tasks[\"analysis_workflow\"].tasks[\"fit_data\"].output\n",
    ")\n",
    "\n",
    "q_result_2d = analysis_result_2d[q_uid]\n",
    "print(\"Available result keys:\", list(q_result_2d.keys()))"
   ],
   "outputs": [],
   "execution_count": null
  },
  {
   "cell_type": "markdown",
   "id": "a1000065",
   "metadata": {},
   "source": [
    "The per-qubit result dictionary for the 2D chevron mode contains:\n",
    "\n",
    "| Key | Description |\n",
    "|-----|-------------|\n",
    "| `\"mode\"` | `\"2d\"` — the sweep mode used |\n",
    "| `\"t_swap\"` | Calibrated SWAP duration at the optimal amplitude (seconds) |\n",
    "| `\"amp_swap\"` | Calibrated SWAP drive amplitude with the highest exchange frequency |\n",
    "| `\"raw_data\"` | 2D array of transmon signal, shape `(n_amplitudes, n_durations)` |\n",
    "| `\"amplitudes\"` | Amplitude sweep axis |\n",
    "| `\"durations\"` | Duration sweep axis (seconds) |\n",
    "| `\"row_fits\"` | List of per-amplitude damped-cosine fit result dicts |\n",
    "| `\"frequencies_per_amp\"` | Array of fitted exchange frequencies for each amplitude row |"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "a1000066",
   "metadata": {},
   "source": [
    "### Extract the Calibration Constants"
   ]
  },
  {
   "cell_type": "code",
   "id": "a1000067",
   "metadata": {},
   "source": [
    "t_swap_2d = q_result_2d[\"t_swap\"]\n",
    "amp_swap_2d = q_result_2d[\"amp_swap\"]\n",
    "# NaN marks amplitude rows whose fit failed.\n",
    "freqs_per_amp = q_result_2d[\"frequencies_per_amp\"]\n",
    "best_freq = np.nanmax(freqs_per_amp) if np.any(np.isfinite(freqs_per_amp)) else np.nan\n",
    "\n",
    "print(f\"Qubit {q_uid} (2D chevron):\")\n",
    "print(f\"  t_swap              = {t_swap_2d * 1e9:.2f} ns\")\n",
    "print(f\"  amp_swap            = {amp_swap_2d:.4f}\")\n",
    "print(f\"  f_exchange (optimum) = {best_freq / 1e6:.3f} MHz\")"
   ],
   "outputs": [],
   "execution_count": null
  },
  {
   "cell_type": "markdown",
   "id": "a1000068",
   "metadata": {},
   "source": [
    "### Update the Qubit Parameters\n",
    "\n",
    "For a 2D sweep, the new values are both `swap_length` and `swap_amplitude`. Once set, the `swap` quantum operation uses these values by default:"
   ]
  },
  {
   "cell_type": "code",
   "id": "a1000069",
   "metadata": {},
   "source": [
    "from pprint import pprint\n",
    "\n",
    "qubit_parameters = workflow_results_2d.tasks[\"analysis_workflow\"].output\n",
    "pprint(qubit_parameters)\n",
    "\n",
    "qpu.update(qubit_parameters[\"new_parameter_values\"])"
   ],
   "outputs": [],
   "execution_count": null
  },
  {
   "cell_type": "markdown",
   "id": "a1000070",
   "metadata": {},
   "source": [
    "## Parallel SWAP Calibration\n",
    "\n",
    "The `swap_calibration` workflow natively supports running multiple qubits in parallel. When you pass a list of qubit UIDs together with corresponding lists of duration and/or 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 sweep range, so qubits with different exchange coupling strengths can be characterised in one shot. The only constraint is that all duration arrays must have the **same number of points**, because the durations of all qubits are swept in lock-step in one shared near-time sweep. The amplitudes are swept in a separate real-time sweep per qubit.\n",
    "\n",
    "Here we demonstrate a parallel 2D chevron sweep on both qubits:"
   ]
  },
  {
   "cell_type": "code",
   "id": "a1000071",
   "metadata": {},
   "source": [
    "# Store the calibrated values in the qpu automatically.\n",
    "options.update(True)\n",
    "\n",
    "q0 = qpu.quantum_elements[0]\n",
    "q1 = qpu.quantum_elements[1]\n",
    "\n",
    "# Each qubit gets its own sweep range — all duration arrays must have equal length,\n",
    "# and all amplitude arrays must have equal length.\n",
    "swap_durations_q0 = np.linspace(100e-9, 600e-9, 21)\n",
    "swap_durations_q1 = np.linspace(100e-9, 600e-9, 21)\n",
    "\n",
    "swap_amplitudes_q0 = np.linspace(0.05, 0.20, 21)\n",
    "swap_amplitudes_q1 = np.linspace(0.05, 0.20, 21)\n",
    "\n",
    "exp_workflow_2q = swap_calibration.experiment_workflow(\n",
    "    session=session,\n",
    "    qpu=qpu,\n",
    "    qubits=[q0.uid, q1.uid],\n",
    "    swap_durations=[swap_durations_q0, swap_durations_q1],\n",
    "    swap_amplitudes=[swap_amplitudes_q0, swap_amplitudes_q1],\n",
    "    options=options,\n",
    ")\n",
    "\n",
    "workflow_results_2q = exp_workflow_2q.run()"
   ],
   "outputs": [],
   "execution_count": null
  },
  {
   "cell_type": "markdown",
   "id": "a1000072",
   "metadata": {},
   "source": [
    "### Check the Updated Qubit Parameters\n",
    "\n",
    "With `update=True`, the workflow stored the new parameter values in the `qpu`. Qubits without a valid fit keep their previous parameters:"
   ]
  },
  {
   "cell_type": "code",
   "id": "a1000073",
   "metadata": {},
   "source": [
    "qubit_parameters_2q = workflow_results_2q.tasks[\"analysis_workflow\"].output\n",
    "pprint(qubit_parameters_2q[\"new_parameter_values\"])\n",
    "\n",
    "for q in [q0, q1]:\n",
    "    print(f\"{q.uid}: swap_length = {q.parameters.swap_length}\")\n",
    "    print(f\"{q.uid}: swap_amplitude = {q.parameters.swap_amplitude}\")"
   ],
   "outputs": [],
   "execution_count": null
  },
  {
   "cell_type": "markdown",
   "id": "a1000074",
   "metadata": {},
   "source": [
    "Great! You have now calibrated the SWAP gate for all qubits. The `swap_length` and `swap_amplitude` parameters are stored in the qubit objects and are used by the `swap` quantum operation whenever a Fock state is prepared via the transmon $\\pi$ + SWAP sequence.\n",
    "\n",
    "The next step in the bosonic qubit tune-up sequence is the [**Wigner tomography demonstrator**](05_wigner_tomography.ipynb), which measures the Wigner function of the memory cavity state."
   ]
  }
 ],
 "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
}
