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_n0must 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_durationsandswap_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:
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:
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:
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:
- Length sweep (
swap_durationsonly) — the SWAP pulse duration \(\tau\) is swept in near time at a fixed drive amplitude. The analysis fits a damped cosine
and extracts t_swap, the duration at the first minimum (half-period) where the transmon is fully depopulated.
-
Amplitude sweep (
swap_amplitudesonly) — the SWAP drive amplitude is swept in real time at a fixed pulse duration. The same damped cosine model is fitted andamp_swap, the amplitude at the first complete excitation transfer, is extracted. -
2D chevron sweep (both
swap_durationsandswap_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 bothamp_swapand the correspondingt_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:
- a DeviceSetup
- one or more BosonicQubits
- a set of
BosonicQubitOperations - a QPU
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.INTEGRATIONandAveragingMode.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 calibratedt_swapand/oramp_swapfor 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:
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:
- Retrieved the transmon signal from the result handle
{qubit_uid}/swap_cal. - 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.
- 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_swapandt_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.