Skip to content

SWAP Gate Calibration

Prerequisites

This guide assumes you have configured a DeviceSetup and created BosonicQubit objects with assigned parameters. Please see our tutorials if you need to create your setup and qubits for the first time.

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).

Before running the SWAP calibration experiment, the following calibrations must have been performed:

  • Transmon spectroscopy: the steady-state ancilla transmon resonance frequency transmon_resonance_frequency_at_n0 must be known.
  • Transmon Rabi calibration: the steady-state transmon \(\pi\)-pulse amplitude selective_transmon_X180_amplitudes[0] must be calibrated.
  • 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.

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.

Background

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.

The SWAP gate

The SWAP gate coherently exchanges a single excitation between the ancilla transmon and the memory cavity:

\[|e,\, n\rangle \;\leftrightarrow\; |g,\, n+1\rangle\]

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:

\[|g,\, 0\rangle \xrightarrow{\pi} |e,\, 0\rangle \xrightarrow{\mathrm{SWAP}} |g,\, 1\rangle\]

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\).

The pulse sequence

The pulse sequence applied to each qubit is:

\[\text{qubit} \;---\; [R_x(\pi)_{n=0}] \;---\; [\mathrm{SWAP}(\mathrm{amp},\,\tau)] \;---\; [\mathrm{measure}]\]

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.

Calibration modes

The workflow supports three modes, selected automatically based on which sweep parameters you provide:

  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
\[s(\tau) = A \cdot \cos\!\left(2\pi f \tau + \varphi\right) \cdot e^{-\tau / T} + B\]

and extracts t_swap, the duration at the first minimum (half-period) where the transmon is fully depopulated.

  1. 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.

  2. 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.

Imports

You'll start by importing laboneq.simple, as well as the BosonicQubit and BosonicQubitOperations classes from the bosonic qubits contribution.

import copy

import numpy as np
from laboneq.simple import *

from laboneq_applications.contrib.qpu_types.bosonic_qubits import (
    BosonicQubit,
    BosonicQubitOperations,
)

Define your experimental setup

Let's define our experimental setup. We will need:

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 and logbooks and data saving.

We will use 2 bosonic qubits in this guide. Change this number to match your setup.

Instrument serial numbers

# Replace with the serial number of your SHFQC instrument
SHFQC_SERIAL = "DEV12073"

DATASERVER_HOST = "localhost"
DATASERVER_PORT = "8004"

number_of_qubits = 2

DeviceSetup

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.

Each bosonic qubit uses:

  • One QA channel pair (OUTPUT + INPUT) for readout
  • One SG channel for the number-selective transmon drives (drive_transmon_at_n0 … drive_transmon_at_n{max_photon_number})
  • One SG channel in LF mode for the SWAP drive (swap_drive)
  • Optionally, one SG channel for the memory cavity drive (drive_memory)

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.

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.

# Maximum photon number — determines how many selective transmon signal lines are created.
MAX_PHOTON_NUMBER = 5

device_setup = DeviceSetup(uid="my_bosonic_setup")
device_setup.add_dataserver(
    uid="my_bosonic_setup",
    host=DATASERVER_HOST,
    port=DATASERVER_PORT,
)

device_setup.add_instruments(
    SHFQC(
        uid="device_shfqc",
        address=SHFQC_SERIAL,
        device_options="SHFQC/LRT/PLUS/QC6CH/RTR",
    ),
)

# ── Signal lines for qubit q0 ─────────────────────────────────────────
device_setup.add_connections(
    "device_shfqc",
    # Readout (QA channel 0)
    create_connection(to_signal="q0/measure", ports="QACHANNELS/0/OUTPUT"),
    create_connection(to_signal="q0/acquire", ports="QACHANNELS/0/INPUT"),
    # Memory cavity drive (SG channel 0) — not used in SWAP calibration
    create_connection(to_signal="q0/drive_memory", ports="SGCHANNELS/0/OUTPUT"),
    # SWAP drive — SG channel in LF mode (DC flux pulse)
    create_connection(to_signal="q0/swap_drive", ports="SGCHANNELS/4/OUTPUT"),
)
# Number-selective transmon drive lines for q0 (all share SG channel 2)
for n in range(MAX_PHOTON_NUMBER + 1):
    device_setup.add_connections(
        "device_shfqc",
        create_connection(
            to_signal=f"q0/drive_transmon_at_n{n}",
            ports="SGCHANNELS/2/OUTPUT",
        ),
    )

# ── Signal lines for qubit q1 (optional) ─────────────────────────────
if number_of_qubits == 2:
    device_setup.add_connections(
        "device_shfqc",
        create_connection(to_signal="q1/measure", ports="QACHANNELS/0/OUTPUT"),
        create_connection(to_signal="q1/acquire", ports="QACHANNELS/0/INPUT"),
        create_connection(to_signal="q1/drive_memory", ports="SGCHANNELS/1/OUTPUT"),
        create_connection(to_signal="q1/swap_drive", ports="SGCHANNELS/5/OUTPUT"),
    )
    for n in range(MAX_PHOTON_NUMBER + 1):
        device_setup.add_connections(
            "device_shfqc",
            create_connection(
                to_signal=f"q1/drive_transmon_at_n{n}",
                ports="SGCHANNELS/3/OUTPUT",
            ),
        )

BosonicQubits

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, …).

Adjust the parameter values to reflect the properties of your device. The parameters relevant to SWAP calibration are highlighted in the comments.

# Qubit parameters — adjust these to match your device.
# Parameters marked (*) are directly relevant to the SWAP calibration experiment.
QUBIT_PARAMS = {
    # ── Memory cavity ────────────────────────────────────────────────
    "memory_lo_frequency": 5.8e9,
    "memory_resonance_frequency": 6.0e9,
    "memory_drive_amplitude": 0.5,
    "memory_drive_length": 0.1e-6,
    "memory_drive_pulse": {"function": "gaussian", "sigma": 0.25},
    "memory_spectroscopy_amplitude": 0.5,
    "memory_spectroscopy_length": 4e-6,
    "memory_drive_range": 10,
    # ── Ancilla transmon ─────────────────────────────────────────────
    "transmon_lo_frequency": 4.8e9,
    "transmon_resonance_frequency_at_n0": 5.0e9,  # (*) Transmon freq when cavity in |0>
    "transmon_drive_range": 10,
    # ── Selective transmon Rx drives (*) ─────────────────────────────
    # The x180 at n=0 is used as the pi pulse before the SWAP.
    "max_photon_number": MAX_PHOTON_NUMBER,
    "selective_transmon_Rx_length": 0.08e-6,  # (*) pi-pulse duration
    "selective_transmon_Rx_pulse": {"function": "gaussian", "sigma": 0.25},
    "selective_transmon_X180_amplitudes": [0.8]
    * (MAX_PHOTON_NUMBER + 1),  # (*) pi amplitudes
    "selective_transmon_X90_amplitudes": [0.4] * (MAX_PHOTON_NUMBER + 1),
    # ── SWAP drive ────────────────────────────────────────────────────
    # This setup drives the SWAP line from an SG channel, which is put into
    # LF mode with its local oscillator at 0 Hz. Leave this out for a SWAP
    # line on a baseband channel, such as an HDAWG output.
    "swap_drive_on_sg_channel": True,
    # ── SWAP gate (*) ─────────────────────────────────────────────────
    # Initial guesses for the SWAP parameters.
    # After running this experiment they will be updated with the calibrated values.
    "swap_length": 500e-9,  # (*) Initial SWAP duration guess (seconds)
    "swap_amplitude": 0.1,  # (*) Initial SWAP drive amplitude guess
    "swap_pulse": {"function": "const"},  # (*) Rectangular DC flux pulse
    "swap_drive_range": 5,  # (*) Hardware output range of the LF channel (V)
    # ── Readout ───────────────────────────────────────────────────────
    "readout_lo_frequency": 6.8e9,
    "readout_resonator_frequency": 7.0e9,
    "readout_amplitude": 0.5,
    "readout_length": 1e-6,
    "readout_pulse": {"function": "const"},
    "readout_integration_length": 1e-6,
    "readout_integration_delay": 20e-9,
    # ── Reset ─────────────────────────────────────────────────────────
    "reset_delay_length": 1e-6,
    # ── Memory-transmon coupling ──────────────────────────────────────
    "chi": -1e6,
    # ── Displacement calibration result ───────────────────────────────
    "displacement_amp_per_unit_beta": None,
}
q0 = BosonicQubit.from_logical_signal_group(
    uid="q0",
    lsg=device_setup.logical_signal_groups["q0"],
    parameters=copy.deepcopy(QUBIT_PARAMS),
)
qubits = [q0]

if number_of_qubits == 2:
    q1 = BosonicQubit.from_logical_signal_group(
        uid="q1",
        lsg=device_setup.logical_signal_groups["q1"],
        parameters=copy.deepcopy(QUBIT_PARAMS),
    )
    qubits.append(q1)

for q in qubits:
    print("-------------")
    print("Qubit UID:", q.uid)
    print("Qubit logical signals:")
    for sig, lsg in q.signals.items():
        print(f"  {sig:<30} ('{lsg}')")

Quantum Operations

Create the set of BosonicQubitOperations:

qops = BosonicQubitOperations()

QPU

Create the QPU object from the qubits and the quantum operations:

from laboneq.dsl.quantum import QPU

qpu = QPU(qubits, quantum_operations=qops)

Connect to Session

session = Session(device_setup)
session.connect(do_emulation=True)  # set do_emulation=False when at a real setup

Create a FolderStore for Saving Data

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.

from pathlib import Path

folder_store = workflow.logbook.FolderStore(Path.cwd() / "data")

We disable saving in this guide. To enable it, simply run folder_store.activate().

folder_store.deactivate()

Optional: Configure the LoggingStore

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 for details.

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.

We recommend that you do not deactivate the Workflow logging in practice.

from laboneq.workflow.logbook import LoggingStore

logging_store = LoggingStore()
logging_store.deactivate()

Running the Experiment Workflow

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.

Start by importing the swap_calibration experiment workflow from the bosonic qubits contribution, as well as plot_simulation for inspecting the pulse sequence.

from laboneq.contrib.example_helpers.plotting.plot_helpers import plot_simulation

from laboneq_applications.contrib.experiments.bosonic_qubits import swap_calibration

Configure the experiment options

Let's create the options object for the SWAP calibration experiment and inspect it using workflow.show_fields:

options = swap_calibration.experiment_workflow.options()
workflow.show_fields(options)

Notice that, unless we change it:

  • The experiment is run in AcquisitionType.INTEGRATION and AveragingMode.CYCLIC, using 1024 averages (count).
  • 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.
  • The figures produced by the analysis are automatically closed (close_figures=True)
  • The qubit parameters will not be updated (update=False).

The create_experiment sub-options expose additional pulse overrides for advanced use:

  • swap_pulse: pulse shape override for the SWAP drive (defaults to a constant pulse, i.e., a rectangular DC flux pulse).
  • chunk_count: number of chunks for splitting the real-time amplitude sweep.

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.

options.count(1)
options.close_figures(False)

Mode 1: Length Sweep

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.

Define the SWAP duration sweep

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)\).

qubit = qpu.quantum_elements[0]

# SWAP duration sweep — adjust range to cover at least one full oscillation period.
swap_durations_q0 = np.linspace(100e-9, 600e-9, 21)

print(
    f"Duration sweep: {swap_durations_q0[0] * 1e9:.0f} to {swap_durations_q0[-1] * 1e9:.0f} ns "
    f"({len(swap_durations_q0)} points)"
)
# Run the length sweep on a single qubit
exp_workflow_length = swap_calibration.experiment_workflow(
    session=session,
    qpu=qpu,
    qubits=[qubit.uid],
    swap_durations=[swap_durations_q0],
    options=options,
)

workflow_results_length = exp_workflow_length.run()

Inspect the Tasks That Were Run

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.

for t in workflow_results_length.tasks:
    print(t)

Inspect the Output Simulation

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.

The total duration of one repetition is approximately:

\[T \approx t_{\pi} + t_\mathrm{SWAP} + t_\mathrm{readout} + t_\mathrm{reset}\]

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.

q = qpu.quantum_elements[0]
total_time = (
    q.parameters.selective_transmon_Rx_length
    + swap_durations_q0[-1]  # use the longest SWAP duration for the window
    + q.parameters.readout_length
    + q.parameters.reset_delay_length
)

compiled_experiment = workflow_results_length.tasks["compile_experiment"].output
plot_simulation(
    compiled_experiment,
    length=total_time * 1.05,
)

Inspect the Source Code of the Pulse-Sequence Creation Task

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.

swap_calibration.create_experiment.src

Analyse the Results

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:

  1. Retrieved the transmon signal from the result handle {qubit_uid}/swap_cal.
  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.
  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.

One plot is generated automatically:

  • SWAP length calibration — the transmon signal versus SWAP duration, with the fitted damped cosine and a vertical dashed line marking t_swap.

Access the Analysis Outputs

# Access the fit results of the analysis workflow.
analysis_result_length = (
    workflow_results_length.tasks["analysis_workflow"].tasks["fit_data"].output
)

q_uid = qpu.quantum_elements[0].uid
q_result_length = analysis_result_length[q_uid]

# Keys available in the per-qubit result dict (length mode):
print("Available result keys:", list(q_result_length.keys()))

The per-qubit result dictionary contains:

Key Description
"mode" "length" — the sweep mode used
"t_swap" Calibrated SWAP duration (seconds)
"frequency" Fitted exchange frequency \(f\) (Hz); for a fitted phase \(\varphi \approx 0\), \(t_\mathrm{swap} \approx 1 / (2f)\)
"raw_data" 1D array of transmon signal vs. duration
"durations" Duration sweep axis (seconds)
"fit_params" Fitted parameters [A, f, φ, T, B] of the damped cosine
"fit_success" True if the fit converged
"fit_x", "fit_y" Dense fit curve arrays for custom plotting

Extract t_swap

t_swap = q_result_length["t_swap"]
f_exchange = q_result_length["frequency"]
fit_ok = q_result_length["fit_success"]

print(f"Qubit {q_uid}:")
print(f"  t_swap       = {t_swap * 1e9:.2f} ns")
print(f"  f_exchange   = {f_exchange / 1e6:.3f} MHz")
print(f"  Fit success  : {fit_ok}")

Update the Qubit Parameters

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.

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.

from pprint import pprint

qubit_parameters = workflow_results_length.tasks["analysis_workflow"].output
pprint(qubit_parameters)

qpu.update(qubit_parameters["new_parameter_values"])

Mode 2: Amplitude Sweep

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.

Define the SWAP amplitude sweep

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.

# SWAP amplitude sweep — adjust range to cover at least one full oscillation.
swap_amplitudes_q0 = np.linspace(0.05, 0.2, 21)

print(
    f"Amplitude sweep: {swap_amplitudes_q0[0]:.3f} to {swap_amplitudes_q0[-1]:.3f} "
    f"({len(swap_amplitudes_q0)} points)"
)
# Run the amplitude sweep on a single qubit
exp_workflow_amp = swap_calibration.experiment_workflow(
    session=session,
    qpu=qpu,
    qubits=[qubit.uid],
    swap_amplitudes=[swap_amplitudes_q0],
    options=options,
)

workflow_results_amp = exp_workflow_amp.run()

Analyse the Results

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.

One plot is generated automatically:

  • SWAP amplitude calibration — the transmon signal versus drive amplitude, with the fitted damped cosine and a vertical dashed line marking amp_swap.

Access the Analysis Outputs

analysis_result_amp = (
    workflow_results_amp.tasks["analysis_workflow"].tasks["fit_data"].output
)

q_result_amp = analysis_result_amp[q_uid]
print("Available result keys:", list(q_result_amp.keys()))

The per-qubit result dictionary for the amplitude mode contains:

Key Description
"mode" "amplitude" — the sweep mode used
"amp_swap" Calibrated SWAP drive amplitude
"rabi_rate_per_unit_amp" Fitted exchange rate per unit amplitude (oscillation cycles per unit amplitude)
"raw_data" 1D array of transmon signal vs. amplitude
"amplitudes" Amplitude sweep axis
"fit_params" Fitted parameters [A, f, φ, T, B] of the damped cosine
"fit_success" True if the fit converged
"fit_x", "fit_y" Dense fit curve arrays for custom plotting

Extract amp_swap

amp_swap = q_result_amp["amp_swap"]
rabi_rate = q_result_amp["rabi_rate_per_unit_amp"]
fit_ok = q_result_amp["fit_success"]

print(f"Qubit {q_uid}:")
print(f"  amp_swap                 = {amp_swap:.4f}")
print(f"  rabi_rate_per_unit_amp   = {rabi_rate:.3f} cycles / amplitude unit")
print(f"  Fit success              : {fit_ok}")

Update the Qubit Parameters

For an amplitude sweep, the new value is swap_amplitude:

from pprint import pprint

qubit_parameters = workflow_results_amp.tasks["analysis_workflow"].output
pprint(qubit_parameters)

qpu.update(qubit_parameters["new_parameter_values"])

Mode 3: 2D Chevron Sweep

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.

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.

Define both sweeps

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.

# 2D chevron sweep — adjust both ranges to cover the expected exchange oscillation.
# Few sweep points keep the emulated run short; use >= 21 points per axis on hardware.
swap_durations_2d_q0 = np.linspace(100e-9, 600e-9, 5)
swap_amplitudes_2d_q0 = np.linspace(0.05, 0.2, 2)

print(
    f"Duration sweep : {swap_durations_2d_q0[0] * 1e9:.0f} to "
    f"{swap_durations_2d_q0[-1] * 1e9:.0f} ns ({len(swap_durations_2d_q0)} points)"
)
print(
    f"Amplitude sweep: {swap_amplitudes_2d_q0[0]:.3f} to "
    f"{swap_amplitudes_2d_q0[-1]:.3f} ({len(swap_amplitudes_2d_q0)} points)"
)
# Run the 2D chevron sweep on a single qubit
exp_workflow_2d = swap_calibration.experiment_workflow(
    session=session,
    qpu=qpu,
    qubits=[qubit.uid],
    swap_durations=[swap_durations_2d_q0],
    swap_amplitudes=[swap_amplitudes_2d_q0],
    options=options,
)

workflow_results_2d = exp_workflow_2d.run()

Analyse the Results

The 2D analysis fits each amplitude row independently and selects the row with the highest exchange frequency as the optimal operating point.

One plot is generated automatically:

  • 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.

Access the Analysis Outputs

analysis_result_2d = (
    workflow_results_2d.tasks["analysis_workflow"].tasks["fit_data"].output
)

q_result_2d = analysis_result_2d[q_uid]
print("Available result keys:", list(q_result_2d.keys()))

The per-qubit result dictionary for the 2D chevron mode contains:

Key Description
"mode" "2d" — the sweep mode used
"t_swap" Calibrated SWAP duration at the optimal amplitude (seconds)
"amp_swap" Calibrated SWAP drive amplitude with the highest exchange frequency
"raw_data" 2D array of transmon signal, shape (n_amplitudes, n_durations)
"amplitudes" Amplitude sweep axis
"durations" Duration sweep axis (seconds)
"row_fits" List of per-amplitude damped-cosine fit result dicts
"frequencies_per_amp" Array of fitted exchange frequencies for each amplitude row

Extract the Calibration Constants

t_swap_2d = q_result_2d["t_swap"]
amp_swap_2d = q_result_2d["amp_swap"]
# NaN marks amplitude rows whose fit failed.
freqs_per_amp = q_result_2d["frequencies_per_amp"]
best_freq = np.nanmax(freqs_per_amp) if np.any(np.isfinite(freqs_per_amp)) else np.nan

print(f"Qubit {q_uid} (2D chevron):")
print(f"  t_swap              = {t_swap_2d * 1e9:.2f} ns")
print(f"  amp_swap            = {amp_swap_2d:.4f}")
print(f"  f_exchange (optimum) = {best_freq / 1e6:.3f} MHz")

Update the Qubit Parameters

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:

from pprint import pprint

qubit_parameters = workflow_results_2d.tasks["analysis_workflow"].output
pprint(qubit_parameters)

qpu.update(qubit_parameters["new_parameter_values"])

Parallel SWAP Calibration

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.

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.

Here we demonstrate a parallel 2D chevron sweep on both qubits:

# Store the calibrated values in the qpu automatically.
options.update(True)

q0 = qpu.quantum_elements[0]
q1 = qpu.quantum_elements[1]

# Each qubit gets its own sweep range — all duration arrays must have equal length,
# and all amplitude arrays must have equal length.
swap_durations_q0 = np.linspace(100e-9, 600e-9, 21)
swap_durations_q1 = np.linspace(100e-9, 600e-9, 21)

swap_amplitudes_q0 = np.linspace(0.05, 0.20, 21)
swap_amplitudes_q1 = np.linspace(0.05, 0.20, 21)

exp_workflow_2q = swap_calibration.experiment_workflow(
    session=session,
    qpu=qpu,
    qubits=[q0.uid, q1.uid],
    swap_durations=[swap_durations_q0, swap_durations_q1],
    swap_amplitudes=[swap_amplitudes_q0, swap_amplitudes_q1],
    options=options,
)

workflow_results_2q = exp_workflow_2q.run()

Check the Updated Qubit Parameters

With update=True, the workflow stored the new parameter values in the qpu. Qubits without a valid fit keep their previous parameters:

qubit_parameters_2q = workflow_results_2q.tasks["analysis_workflow"].output
pprint(qubit_parameters_2q["new_parameter_values"])

for q in [q0, q1]:
    print(f"{q.uid}: swap_length = {q.parameters.swap_length}")
    print(f"{q.uid}: swap_amplitude = {q.parameters.swap_amplitude}")

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.

The next step in the bosonic qubit tune-up sequence is the Wigner tomography demonstrator, which measures the Wigner function of the memory cavity state.