See also
A Jupyter notebook version of this tutorial can be downloaded here.
Bulk API sequencing#
In this tutorial, we will demonstrate the Bulk API, a set of methods introduced to batch updates and commands to multiple sequencers across a Cluster. This API is specifically designed to reduce communication overhead and latency by consolidating multiple per-sequencer calls into single, efficient Cluster-wide operations.
This tutorial serves as a migration guide to help you transition from the Iterative (Per-Sequencer) approach to the new Batched (Bulk API) approach.
The Bulk API includes the following methods:
Cluster.update_sequencesCluster.arm_sequencersCluster.start_sequencersCluster.stop_sequencersCluster.get_sequencer_statusesCluster.wait_for_sequencersCluster.get_all_acquisitions(Note that this differs fromCluster.get_acquisitionswhich acts on one slot or sequencer at a time)
Setup#
First, we import the required packages and connect to the instrument.
[1]:
from __future__ import annotations
from qcodes.instrument import find_or_create_instrument
from qblox_instruments import Cluster, ClusterType
Scan For Clusters#
We scan for the available devices connected via ethernet using the Plug & Play functionality of the Qblox Instruments package (see Plug & Play for more info).
!qblox-pnp list
[2]:
cluster_ip = "10.10.200.42"
cluster_name = "cluster0"
Connect to Cluster#
We now make a connection with the Cluster.
[3]:
cluster: Cluster = find_or_create_instrument(
Cluster,
recreate=True,
name=cluster_name,
identifier=cluster_ip,
dummy_cfg=(
{
2: ClusterType.CLUSTER_QCM,
4: ClusterType.CLUSTER_QRM,
6: ClusterType.CLUSTER_QCM_RF,
8: ClusterType.CLUSTER_QRM_RF,
10: ClusterType.CLUSTER_QTM,
12: ClusterType.CLUSTER_QRC,
16: ClusterType.CLUSTER_QSM,
}
if cluster_ip is None
else None
),
)
cluster.reset()
print(cluster.get_system_status())
Status: OKAY, Flags: NONE, Slot flags: NONE
Get connected modules#
[4]:
# QRM-RF modules
modules = cluster.get_connected_modules(lambda mod: mod.is_qrm_type and mod.is_rf_type)
# This uses the module of the correct type with the lowest slot index
module = list(modules.values())[0]
Preparation#
We will prepare a simple sequence to be loaded onto multiple sequencers. For this demonstration, we will use four sequencers of a single module.
[5]:
# Define a simple Q1ASM program.
seq_prog = """
wait_sync 4 # Synchronize sequencers and wait 4 ns.
move 100,R0 # Loop 100 times
loop: wait 4
acquire 0,0,1000
loop R0,@loop
stop # Stop the sequence.
"""
# Create the sequence dictionary
sequence = {
"waveforms": {},
"weights": {},
"acquisitions": {"acq_0": {"num_bins": 1, "index": 0}},
"program": seq_prog,
}
# Map sequencers to their respective sequences for the Bulk API.
# The keys are tuples of (slot_index, sequencer_index).
sequences = {
(module.slot_idx, 0): sequence,
(module.slot_idx, 1): sequence,
(module.slot_idx, 2): sequence,
(module.slot_idx, 3): sequence,
}
# Define sequencers to target: (slot_index, sequencer_index)
seq_targets = list(sequences.keys())
1. Uploading Sequences#
Iterative (Old Way)#
Previously, you had to call the sequence upload method for every single sequencer individually. As the number of sequencers grows, this becomes increasingly tedious and slow due to multiple network round-trips.
[6]:
# Iterative approach (Manual one-by-one calls)
module.sequencer0.sequence(sequence)
module.sequencer1.sequence(sequence)
module.sequencer2.sequence(sequence)
module.sequencer3.sequence(sequence)
Batched (New Way)#
With the Bulk API, you provide a mapping of all targets and upload everything in a single Cluster-wide call.
[7]:
# Batched approach (Single call for all targets)
cluster.update_sequences(sequences=sequences, erase_existing=True)
2. Arming and Starting#
Iterative (Old Way)#
Arming had to be performed per-sequencer, resulting in more boilerplate code.
[8]:
# Iterative approach
module.arm_sequencer(0)
module.arm_sequencer(1)
module.arm_sequencer(2)
module.arm_sequencer(3)
# Start sequencers on the module
module.start_sequencer()
Batched (New Way)#
You can now arm and start a specific list of sequencers across the entire cluster in two calls, regardless of which modules they are on.
[9]:
# Batched approach
cluster.arm_sequencers(sequencers=seq_targets)
cluster.start_sequencers(sequencers=seq_targets)
3. Monitoring and Waiting#
Iterative (Old Way)#
To wait for multiple sequencers to finish, you would have to poll each one individually.
[10]:
# Iterative approach (Manual polling for each sequencer)
module.get_sequencer_status(0, timeout=1)
module.get_sequencer_status(1, timeout=1)
module.get_sequencer_status(2, timeout=1)
module.get_sequencer_status(3, timeout=1)
[10]:
SequencerStatus(status=<SequencerStatuses.OKAY>, state=<SequencerStates.STOPPED>, exit_code=0, info_flags=[<SequencerStatusFlags.ACQ_BINNING_DONE>], warn_flags=[], err_flags=[], log=[])
Batched (New Way)#
The Bulk API handles the polling logic for you across all targeted sequencers efficiently.
[11]:
# Get statuses for all targeted sequencers in one call
statuses = cluster.get_sequencer_statuses(sequencers=seq_targets)
print(f"Current statuses: {statuses}")
# Wait for all targeted sequencers to reach the 'STOPPED' state
cluster.wait_for_sequencers(sequencers=seq_targets, timeout=1)
Current statuses: [SequencerStatus(status=<SequencerStatuses.OKAY>, state=<SequencerStates.STOPPED>, exit_code=0, info_flags=[<SequencerStatusFlags.ACQ_SCOPE_DONE_PATH_0>, <SequencerStatusFlags.ACQ_SCOPE_DONE_PATH_1>, <SequencerStatusFlags.ACQ_BINNING_DONE>], warn_flags=[], err_flags=[], log=[]), SequencerStatus(status=<SequencerStatuses.OKAY>, state=<SequencerStates.STOPPED>, exit_code=0, info_flags=[<SequencerStatusFlags.ACQ_BINNING_DONE>], warn_flags=[], err_flags=[], log=[]), SequencerStatus(status=<SequencerStatuses.OKAY>, state=<SequencerStates.STOPPED>, exit_code=0, info_flags=[<SequencerStatusFlags.ACQ_BINNING_DONE>], warn_flags=[], err_flags=[], log=[]), SequencerStatus(status=<SequencerStatuses.OKAY>, state=<SequencerStates.STOPPED>, exit_code=0, info_flags=[<SequencerStatusFlags.ACQ_BINNING_DONE>], warn_flags=[], err_flags=[], log=[])]
[11]:
[SequencerStatus(status=<SequencerStatuses.OKAY>, state=<SequencerStates.STOPPED>, exit_code=0, info_flags=[<SequencerStatusFlags.ACQ_SCOPE_DONE_PATH_0>, <SequencerStatusFlags.ACQ_SCOPE_DONE_PATH_1>, <SequencerStatusFlags.ACQ_BINNING_DONE>], warn_flags=[], err_flags=[], log=[]),
SequencerStatus(status=<SequencerStatuses.OKAY>, state=<SequencerStates.STOPPED>, exit_code=0, info_flags=[<SequencerStatusFlags.ACQ_BINNING_DONE>], warn_flags=[], err_flags=[], log=[]),
SequencerStatus(status=<SequencerStatuses.OKAY>, state=<SequencerStates.STOPPED>, exit_code=0, info_flags=[<SequencerStatusFlags.ACQ_BINNING_DONE>], warn_flags=[], err_flags=[], log=[]),
SequencerStatus(status=<SequencerStatuses.OKAY>, state=<SequencerStates.STOPPED>, exit_code=0, info_flags=[<SequencerStatusFlags.ACQ_BINNING_DONE>], warn_flags=[], err_flags=[], log=[])]
4. Retrieving Data#
This section applies to readout modules (QRM, QRM-RF, QRC, QTM) that support acquisitions.
Iterative (Old Way)#
Acquisitions had to be retrieved one-by-one from each sequencer and manually aggregated.
[12]:
acq0 = module.sequencer0.get_acquisitions()
acq1 = module.sequencer1.get_acquisitions()
acq2 = module.sequencer2.get_acquisitions()
acq3 = module.sequencer3.get_acquisitions()
all_acquisitions = {
(module.slot_idx, 0): acq0,
(module.slot_idx, 1): acq1,
(module.slot_idx, 2): acq2,
(module.slot_idx, 3): acq3,
}
Batched (New Way)#
You can now fetch all acquisition data from the targeted sequencers in a single batch.
[13]:
all_acquisitions_bulk = cluster.get_all_acquisitions(sequencers=seq_targets)
5. Stopping Sequencers#
Finally, if you need to stop specific sequencers, the Bulk API provides a batched method for that as well.
[14]:
# Stop specific sequencers across the cluster
cluster.stop_sequencers(sequencers=seq_targets)
Summary of Benefits#
By transitioning to the Bulk API, you achieve:
Reduced Latency: Commands are sent directly to multiple modules in parallel, completely eliminating sequential communication delays.
Efficient Execution: You can now upload, arm, start, and retrieve data from multiple sequencers simultaneously in single cluster-wide operations.
Less Boilerplate Code: Your experimental scripts are shorter and cleaner because repetitive per-sequencer calls are consolidated into single commands.
Stop#
Finally, let’s stop the sequencers if they haven’t already and close the instrument connection. One can also display a detailed snapshot containing the instrument parameters before closing the connection by uncommenting the corresponding lines.
[15]:
# Stop all sequencers.
module.stop_sequencer()
# Print status of sequencers 0 and 1 (should now say it is stopped).
print(module.get_sequencer_status(0))
print(module.get_sequencer_status(1))
print()
Status: OKAY, State: STOPPED, Exit Code: 0, Info Flags: FORCED_STOP, ACQ_SCOPE_DONE_PATH_0, ACQ_SCOPE_DONE_PATH_1, ACQ_BINNING_DONE, Warning Flags: NONE, Error Flags: NONE, Log: []
Status: OKAY, State: STOPPED, Exit Code: 0, Info Flags: FORCED_STOP, ACQ_BINNING_DONE, Warning Flags: NONE, Error Flags: NONE, Log: []