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_sequences

  • Cluster.arm_sequencers

  • Cluster.start_sequencers

  • Cluster.stop_sequencers

  • Cluster.get_sequencer_statuses

  • Cluster.wait_for_sequencers

  • Cluster.get_all_acquisitions (Note that this differs from Cluster.get_acquisitions which 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:

  1. Reduced Latency: Commands are sent directly to multiple modules in parallel, completely eliminating sequential communication delays.

  2. Efficient Execution: You can now upload, arm, start, and retrieve data from multiple sequencers simultaneously in single cluster-wide operations.

  3. 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: []