Acquisition protocols#

The dataset returned by InstrumentCoordinator.retrieve_acquisition() consists of a number of DataArrays containing data for every acquisition channel. This document specifies the format of these data arrays.

Hide imports and auxiliary definitions
import numpy as np
import xarray as xr
import hvplot.xarray

intermodulation_freq = 1e8  # 100 MHz
voltage_iq = 0.32 + 0.25j
sampling_rate = 1.8e9  # 1.8 GSa/s
readout_duration = 1e-7  # 100 ns
time_grid = xr.DataArray(
    np.arange(0, readout_duration, sampling_rate**-1), dims="trace_index"
)

(Demodulated) Trace Acquisition Protocol#

Readout equipment digitizes a \(V(t)\), where \(V = V_I + i V_Q\) is a complex voltage on inputs of a readout module (up/down conversion of a signal with an IQ mixer is assumed). The signal is demodulated with an intermodulation frequency configured for a readout port.

For example, if we have a readout module (like Qblox QRM or Zurich Instruments UHFQA) that is perfect, and connect its outputs to its inputs directly, raw input on a readout port will look like this:

Hide code cell source
raw_trace = (
    voltage_iq * np.exp(2j * np.pi * intermodulation_freq * time_grid)
).assign_coords({"trace_time": time_grid})
xr.Dataset({"I": raw_trace.real, "Q": raw_trace.imag}).hvplot(
    x="trace_time", xlabel="t [s]", ylabel="V", group_label="Channel"
)

Demodulated trace will unroll this data with respect to the intermodulation frequency, so the resulting I and Q readouts will look like this:

Hide code cell source
demodulated_trace = raw_trace * np.exp(-2j * np.pi * intermodulation_freq * time_grid)

xr.Dataset({"I": demodulated_trace.real, "Q": demodulated_trace.imag}).hvplot(
    x="trace_time", xlabel="t [s]", ylabel="V", group_label="Channel"
)

This acquisition protocol is currently supported only in BinMode.AVERAGE binning mode. The resulting dataset must contain data arrays with two dimensions for each acquisition channel: acquisition index (number of an acquisition in a schedule) and trace index (that corresponds to time from the start of the acquisition). All the dimension names should be suffixed with the acquisition channel to avoid conflicts while merging the datasets. It is recommended to annotate the trace index dimension with a coordinate that describes the time since the start of the acquisition. For example, if two acquisition channels read out once, the resulting dataset should have the following structure:

Hide code cell source
xr.Dataset(
    {
        0: demodulated_trace.expand_dims("acq_index_0", 0).rename(
            {"trace_index": "trace_index_0", "trace_time": "trace_time_0"}
        ),
        1: demodulated_trace.expand_dims("acq_index_1", 0).rename(
            {"trace_index": "trace_index_1", "trace_time": "trace_time_1"}
        ),
    }
)
<xarray.Dataset> Size: 9kB
Dimensions:       (trace_index_0: 180, acq_index_0: 1, trace_index_1: 180,
                   acq_index_1: 1)
Coordinates:
    trace_time_0  (trace_index_0) float64 1kB 0.0 5.556e-10 ... 9.944e-08
    trace_time_1  (trace_index_1) float64 1kB 0.0 5.556e-10 ... 9.944e-08
Dimensions without coordinates: trace_index_0, acq_index_0, trace_index_1,
                                acq_index_1
Data variables:
    0             (acq_index_0, trace_index_0) complex128 3kB (0.32+0.25j) .....
    1             (acq_index_1, trace_index_1) complex128 3kB (0.32+0.25j) .....

Single-sideband Complex Integration#

In this acquisition protocol acquired voltage trace gets demodulated and averaged. For each acquisition, a single complex voltage value is returned (\(V_I + i V_Q\)).

This acquisition protocol supports BinMode.APPEND binning mode for single-shot readout and BinMode.AVERAGE binning mode for returning data averaged for several executions of a schedule. In the first case data arrays for each acquisition channel will have two dimensions: repetition and acquisition index. All the dimension names except repetition should be suffixed with the acquisition channel to avoid conflicts while merging the datasets, the repetition dimension must be named "repetition". For example, two acquisition channels of which acquisition channel 0 read out three times and acquisition channel two read out two times, the resulting dataset should have the following structure in BinMode.APPEND:

Hide code cell source
xr.Dataset(
    {
        0: demodulated_trace.reduce(np.average, "trace_index").expand_dims(
            {"repetition": 5, "acq_index_0": 3}
        ),
        2: demodulated_trace.reduce(np.average, "trace_index").expand_dims(
            {"repetition": 5, "acq_index_2": 2}
        ),
    }
)
<xarray.Dataset> Size: 400B
Dimensions:  (repetition: 5, acq_index_0: 3, acq_index_2: 2)
Dimensions without coordinates: repetition, acq_index_0, acq_index_2
Data variables:
    0        (repetition, acq_index_0) complex128 240B (0.32+0.25j) ... (0.32...
    2        (repetition, acq_index_2) complex128 160B (0.32+0.25j) ... (0.32...

In BinMode.AVERAGE repetition dimension gets reduced and only the acquisition index dimension is left for each channel:

Hide code cell source
xr.Dataset(
    {
        0: demodulated_trace.reduce(np.average, "trace_index").expand_dims(
            {"acq_index_0": 3}
        ),
        2: demodulated_trace.reduce(np.average, "trace_index").expand_dims(
            {"acq_index_2": 2}
        ),
    }
)
<xarray.Dataset> Size: 80B
Dimensions:  (acq_index_0: 3, acq_index_2: 2)
Dimensions without coordinates: acq_index_0, acq_index_2
Data variables:
    0        (acq_index_0) complex128 48B (0.32+0.25j) (0.32+0.25j) (0.32+0.25j)
    2        (acq_index_2) complex128 32B (0.32+0.25j) (0.32+0.25j)

Thresholded Acquisition#

  • Referred to as "ThresholdedAcquisition".

  • Supported by the Qblox backend.

This acquisition protocol is similar to the SSB complex integration, but in this case, the obtained results are compared against a threshold value to obtain 0 or 1. The purpose of this protocol is to discriminate between qubit states.

For example, when acquiring on a single acquisition channel with BinMode.APPEND and repetitions=12, the corresponding dataset could look like:

Hide code cell source
thresholded_data = np.array([0,0,1,0,1,0,0,1,1,0,0,1])
xr.Dataset(
    {0: xr.DataArray(thresholded_data.reshape(1,12), dims = ['acq_index_0', 'repetitions'])}
)
<xarray.Dataset> Size: 96B
Dimensions:  (acq_index_0: 1, repetitions: 12)
Dimensions without coordinates: acq_index_0, repetitions
Data variables:
    0        (acq_index_0, repetitions) int64 96B 0 0 1 0 1 0 0 1 1 0 0 1

In using BinMode.AVERAGE, the corresponding dataset could like:

Hide code cell source
xr.Dataset(
    {0: xr.DataArray(np.mean(thresholded_data, keepdims=1), dims = ['acq_index_0'])}

)
<xarray.Dataset> Size: 8B
Dimensions:  (acq_index_0: 1)
Dimensions without coordinates: acq_index_0
Data variables:
    0        (acq_index_0) float64 8B 0.4167

Numerical Separated Weighted Integration#

  • Referred to as "NumericalSeparatedWeightedIntegration".

  • Supported by the Qblox backend.

Equivalent to SSB complex integration, but instead of a simple average of a demodulated signal, the signal is weighted with two waveforms and then integrated. One waveform for the real part of the signal, and one for the imaginary part. The dataset format is also the same.

Integration weights should normally be calibrated in a separate experiment (see, for example, Magesan et al. [MGCorcolesC15]).

Numerical Weighted Integration#

  • Referred to as "NumericalWeightedIntegration".

  • Supported by the Qblox backend.

Equivalent to Numerical Separated Weighted Integration, but the real part of the output is the sum of the real and imaginary part of the output from the Numerical Separated Weighted Integration protocol. The dataset format is also the same. This is equivalent to multiplying the complex signal with complex waveform weights, and only returning the real part of the result. If the integration weights are calibrated as in Magesan et al. [MGCorcolesC15], i.e. the complex weights are the difference between the two signals we wish to distinguish, then the real part of the complex weighted multiplication contains all the relevant information required to distinguish between the states, and the imaginary part contains only noise.

Integration weights should normally be calibrated in a separate experiment (see, for example, Magesan et al. [MGCorcolesC15]).

Trigger Count#

  • Referred to as "TriggerCount".

  • Supported by the Qblox backend.

Note

Please also see Acquisition details for more information on Qblox module-specific behavior of this operation.

This acquisition protocol measures how many times a predefined voltage threshold has been passed. For the QRM, the threshold is set via ttl_acq_threshold (see also Sequencer options), while for the QTM this threshold setting is a dedicated hardware option called in_threshold_primary, see Digitization thresholds.

First, let’s see an example when the bin mode is BinMode.APPEND. The returned data for the acquisition channel contains the number of triggers counted for each acquisition index. In the following example, suppose a schedule with one trigger count acquisition was executed 5 times (repetitions=5). In order, the number of triggers counted is [6, 3, 8, 1, 3]. The resulting dataset would then look like:

Hide code cell source
trigger_data = np.array([6, 3, 8, 1, 3])
xr.Dataset(
    {0: xr.DataArray(trigger_data.reshape(1, 5), dims = ['acq_index_0', 'repetitions'])}
)
<xarray.Dataset> Size: 40B
Dimensions:  (acq_index_0: 1, repetitions: 5)
Dimensions without coordinates: acq_index_0, repetitions
Data variables:
    0        (acq_index_0, repetitions) int64 40B 6 3 8 1 3

In BinMode.DISTRIBUTION mode, the result is a distribution that maps the trigger count numbers to the number of occurrences of each trigger count number. This provides insights into the overall occurrence of triggers when running the acquisition multiple times.

For example, in the example below, the schedule ran 8 times. From these 8 runs,

  • in 1 run, the trigger was counted 5 times,

  • in 2 runs, the trigger was counted 4 times,

  • in 3 runs, the trigger was counted 2 times,

  • in 2 runs, the trigger was counted once.

Note: 0 counts are removed from the returned data, so there will be no entry for “3 times”.

You can think of the append mode values as the cumulative distribution of the distribution mode values. See an example below.

Hide code cell source
trigger_data = [1, 2, 3, 2]
counts = [5, 4, 2, 1]
xr.Dataset(
    {0: xr.DataArray([trigger_data],
            dims=["repetition", "counts"],
            coords={"repetition": [0], "counts": counts},
        )
    }
)
<xarray.Dataset> Size: 72B
Dimensions:     (repetition: 1, counts: 4)
Coordinates:
  * repetition  (repetition) int64 8B 0
  * counts      (counts) int64 32B 5 4 2 1
Data variables:
    0           (repetition, counts) int64 32B 1 2 3 2

Thresholded Trigger Count#

  • Referred to as "ThresholdedTriggerCount".

  • Supported by the Qblox backend.

Note

Please also see Acquisition details for more information on Qblox module-specific behavior of this operation.

This acquisition protocol returns whether the number of triggers counted surpassed the given threshold. It can also be used together with the ConditionalOperation to execute instructions based on the threshold comparison result.

Note that the analog thresholded for registering a single trigger is set via the hardware configuration. For the QRM, the analog threshold is set via ttl_acq_threshold (see also Sequencer options), while for the QTM this threshold setting is a dedicated hardware option called in_threshold_primary, see Digitization thresholds.

The only available bin mode is BinMode.APPEND. The returned data for the acquisition channel contains the number of triggers counted for each acquisition index. For example, suppose a schedule with one trigger count acquisition was executed 5 times (repetitions=5) with a threshold set to 4 counts. In order, the number of triggers counted is [6, 3, 8, 1, 3]. The resulting dataset would then look like:

Hide code cell source
trigger_data = np.array([1, 0, 1, 0, 0])
xr.Dataset(
    {0: xr.DataArray(trigger_data.reshape(1, 5), dims = ['acq_index_0', 'repetitions'])}
)
<xarray.Dataset> Size: 40B
Dimensions:  (acq_index_0: 1, repetitions: 5)
Dimensions without coordinates: acq_index_0, repetitions
Data variables:
    0        (acq_index_0, repetitions) int64 40B 1 0 1 0 0

Timetag acquisition#

  • Supported by the Qblox backend, only on QTM modules.

Note

Please also see Acquisition details for more information on Qblox module-specific behavior of this operation.

The Timetag acquisition protocol (referred to as "Timetag") measures the point in time at which a voltage threshold was passed with a rising edge (for Qblox QTM modules, this voltage threshold is set with the Digitization thresholds hardware option). The timetag is the difference between a time source and a time reference.

The source of the timetag itself can be one of:

  • The first recorded rising edge,

  • The second recorded rising edge,

  • The last recorded rising edge.

The time reference can be one of:

  • The start of the acquisition window,

  • The end of the acquisition window,

  • The first measured rising edge,

  • A scheduled Timestamp operation.

The protocol always returns one timetag per acquisition bin. If BinMode.APPEND is used, the acquisition bin index is incremented automatically and each timetag measurement is put in a separate bin. For example, let’s look at the schedule below, which is repeated three times.

from quantify_scheduler import Schedule
from quantify_scheduler.enums import BinMode, TimeSource, TimeRef
from quantify_scheduler.operations.pulse_library import Timestamp
from quantify_scheduler.operations.acquisition_library import Timetag

sched = Schedule("Timetag", repetitions=3)

sched.add(Timestamp(port="qe0:optical_readout", clock="qe0.ge0"))
sched.add(
    Timetag(
        duration=10e-6,
        port="qe0:optical_readout",
        clock="qe0.ge0",
        time_source=TimeSource.FIRST,
        time_ref=TimeRef.TIMESTAMP,
        bin_mode=BinMode.APPEND,
    ),
    rel_time=500e-9,
)

An experiment with this schedule will return a dataset that may look like this:

Hide code cell source
data_array = xr.DataArray(
        np.array([5438.2, 756.16, 1059.2]).reshape((3, 1)),
        dims=["repetition", "acq_index_0"],
        coords={"acq_index_0": [0]},
        attrs={"acq_protocol": "Timetag"},
    )
xr.Dataset({0: data_array})
<xarray.Dataset> Size: 32B
Dimensions:      (acq_index_0: 1, repetition: 3)
Coordinates:
  * acq_index_0  (acq_index_0) int64 8B 0
Dimensions without coordinates: repetition
Data variables:
    0            (repetition, acq_index_0) float64 24B 5.438e+03 756.2 1.059e+03

If BinMode.AVERAGE is used, the acquisition data will contain the average of the timetags recorded in each bin. If only bin index 0 was used for the three acquisitions in the above example, the data set may look like this:

Hide code cell source
data_array = xr.DataArray(
        [2417.853333333333],
        dims=["acq_index_0"],
        coords={"acq_index_0": [0]},
        attrs={"acq_protocol": "Timetag"},
    )
xr.Dataset({0: data_array})
<xarray.Dataset> Size: 16B
Dimensions:      (acq_index_0: 1)
Coordinates:
  * acq_index_0  (acq_index_0) int64 8B 0
Data variables:
    0            (acq_index_0) float64 8B 2.418e+03

TimetagTrace acquisition#

  • Supported by the Qblox backend, only on QTM modules.

Note

Please also see Acquisition details for more information on Qblox module-specific behavior of this operation.

The TimetagTrace acquisition protocol (referred to as "TimetagTrace") measures all points in time at which a voltage threshold is passed (with a rising edge), while the acquisition window is active. For Qblox QTM modules, this voltage threshold is set with the Digitization thresholds hardware option. Each timetag value is the difference between the time of the rising edge and a time reference.

The time reference can be one of:

  • The start of the acquisition window,

  • The end of the acquisition window,

  • The first measured rising edge,

  • A scheduled Timestamp operation.

The only usable bin mode at this moment is BinMode.APPEND. If the schedule is repeated multiple times, timetags of each repetition will be appended to the acquisition data. Please note that the returned xarray Dataset is always rectangular. This means that in the case that different amount of pulses are timetagged in each repetition, the sub-arrays are padded with np.NaN to ensure uniformity.

For example, let’s take the following schedule:

from quantify_scheduler import Schedule
from quantify_scheduler.enums import BinMode, TimeSource, TimeRef
from quantify_scheduler.operations.pulse_library import Timestamp
from quantify_scheduler.operations.acquisition_library import TimetagTrace

sched = Schedule("Timetag", repetitions=3)

sched.add(Timestamp(port="qe0:optical_readout", clock="qe0.ge0"))
sched.add(
    TimetagTrace(
        duration=10e-6,
        port="qe0:optical_readout",
        clock="qe0.ge0",
        time_ref=TimeRef.TIMESTAMP,
    ),
    rel_time=500e-9,
)

This schedule could produce data that looks like this:

Hide code cell source
data_array = xr.DataArray(
        np.array([1227.94775391, 605.43261719, 3720.31591797, np.nan, np.nan, 4307.07177734, 6605.31689453, np.nan, np.nan, np.nan, 2063.68652344, 3743.87255859, 3121.44726562, 1534.71484375, 3273.87792969]).reshape((3, 1, 5)),
        dims=["repetition", "acq_index_0", "trace_index_0"],
        coords={"acq_index_0": [0], "trace_index_0": list(range(5))},
        attrs={"acq_protocol": "TimetagTrace"},
    )
xr.Dataset({0: data_array})
<xarray.Dataset> Size: 168B
Dimensions:        (acq_index_0: 1, trace_index_0: 5, repetition: 3)
Coordinates:
  * acq_index_0    (acq_index_0) int64 8B 0
  * trace_index_0  (trace_index_0) int64 40B 0 1 2 3 4
Dimensions without coordinates: repetition
Data variables:
    0              (repetition, acq_index_0, trace_index_0) float64 120B 1.22...