See also

A Jupyter notebook version of this tutorial can be downloaded here.

Partial Acquisition Download#

In this tutorial, we will demonstrate the Partial Acquisition Download feature. Traditionally, retrieving acquisition data from a sequencer involved downloading a complete set of fields for every bin, including raw I/Q values, average counts, validity bits, and threshold counts.

For many experiments, you may only need a subset of this data. The Partial Acquisition Download feature allows you to specify exactly which fields you want to retrieve, significantly reducing the amount of data transferred and memory usage on the host.

This is achieved using the AcquisitionEncodingConfig class. For a full list of available fields and their configurations, please refer to the API reference.

The AcquisitionEncodingConfig class provides several factory methods for common use cases:

  • default_packing(): Use efficient packing for data transfer.

  • threshold_and_avg_count(): Download only normalized thresholds (threshold/avg_cnt) and average count.

  • threshold_raw_only(): Download only raw thresholds.

  • threshold_normalized_only(): Download only normalized thresholds (threshold/avg_cnt).

  • iq_raw_only(): Download only raw IQ paths.

  • iq_normalized_only(): Download only normalized IQ paths (iq / avg_cng).

  • iq_and_avg_count(): Download only normalized IQ paths (iq / avg_cng) and avg count.

Setup#

First, we import the required packages. Note that AcquisitionEncodingConfig is located in the types submodule.

[1]:
from __future__ import annotations

from qcodes.instrument import find_or_create_instrument

from qblox_instruments import Cluster, ClusterType
from qblox_instruments.types import AcquisitionEncodingConfig

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 set up a simple acquisition on a single sequencer.

[5]:
# Define sequencer target
seq_target = (module.slot_idx, 0)

# Define a simple Q1ASM program that performs several acquisitions
seq_prog = """
        move      100,R0    # Loop 100 times
loop:   wait      4
        acquire   0,0,1024  # Acquire on bin 0, acq index 0
        loop      R0,@loop
        stop
"""

# Create the sequence dictionary
sequence = {
    "waveforms": {},
    "weights": {},
    "acquisitions": {"acq_0": {"num_bins": 1, "index": 0}},
    "program": seq_prog,
}

sequences = {
    (module.slot_idx, 0): sequence,
}

# Upload and start the sequence
cluster.update_sequences(sequences=sequences, erase_existing=True)
[6]:
module.disconnect_outputs()
module.disconnect_inputs()

# Enable marker switches to toggle the RF switch before output port
module.sequencer0.marker_ovr_en(True)
module.sequencer0.marker_ovr_value(3)

# module.sequencer0.connect_sequencer("io0")
module.sequencer0.connect_out0("IQ")
module.sequencer0.connect_acq("in0")
module.sequencer0.mod_en_awg(True)
module.sequencer0.demod_en_acq(True)

module.sequencer0.nco_prop_delay_comp_en(True)

module.out0_in0_lo_freq(3e9)
module.sequencer0.nco_freq(50e6)
[7]:
cluster.arm_sequencers(sequencers=[seq_target])
cluster.start_sequencers(sequencers=[seq_target])

# Wait for completion
cluster.wait_for_sequencers(sequencers=[seq_target], timeout=1)
[7]:
[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=[])]

Full Download#

By default, get_acquisitions() returns all available data fields.

[8]:
# Setting as_numpy=True is recommended for efficient array handling
full_acq = module.sequencer0.get_acquisitions(as_numpy=True)
print(f"Full download fields: {full_acq['acq_0']['acquisition']['bins'].keys()}")
Full download fields: dict_keys(['integration', 'threshold', 'avg_cnt'])

Partial Download Modes#

Now let’s explore how we can download only what we need using AcquisitionEncodingConfig.

Normalized I/Q Data#

If you only care about the final averaged I/Q values, you can use iq_normalized_only(). The division by the average count happens on the hardware, saving you the step in Python.

[9]:
# Download only normalized I/Q data
config_iq = AcquisitionEncodingConfig.iq_normalized_only()
partial_iq = module.sequencer0.get_acquisitions(acq_encoding=config_iq)

print(f"Normalized IQ fields: {partial_iq['acq_0']['acquisition']['bins'].keys()}")
# Note: Fields like 'avg_cnt' or 'threshold' are omitted.
Normalized IQ fields: dict_keys(['integration'])

Threshold Only#

If you are doing single-shot readout and only need the threshold results, use threshold_normalized_only().

[10]:
# Download only normalized threshold data
config_th = AcquisitionEncodingConfig.threshold_normalized_only()
partial_th = module.sequencer0.get_acquisitions(acq_encoding=config_th)

print(f"Threshold fields: {partial_th['acq_0']['acquisition']['bins'].keys()}")
Threshold fields: dict_keys(['threshold'])

Integration with Bulk API#

The Partial Acquisition Download feature is fully integrated with the Bulk API. You can fetch partial data from multiple sequencers in a single efficient call.

[11]:
# Fetch partial data from multiple sequencers (demonstrated with one here)
bulk_partial = cluster.get_all_acquisitions(
    sequencers=[seq_target],
    as_numpy=True,
    acq_encoding=AcquisitionEncodingConfig.iq_and_avg_count(),
)

print(f"Bulk partial fields: {bulk_partial[0]['acq_0']['acquisition']['bins'].keys()}")
Bulk partial fields: dict_keys(['integration', 'avg_cnt'])

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.

[12]:
# 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: NONE, Warning Flags: NONE, Error Flags: NONE, Log: []