See also

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

Binned Mode#

The Qblox Timetagging Module (henceforth referred to as the QTM) allows recording of timetags relative to a user-define time reference, as well as counting of incoming threshold crossing events.

It is highly recommended to go through the Timetag Sequencer page before going through this tutorial.

This tutorial covers the Binned Mode of operation for the QTM as described in the timetag sequencer page. This tutorial will demonstrate how to the binned mode as well as the low-latency counter using a Q1ASM sequencer program.

[1]:
from __future__ import annotations

import matplotlib.pyplot as plt
import numpy as np
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: ERROR, Flags: FEEDBACK_NETWORK_CALIBRATION_FAILED, Slot flags: NONE

Get connected modules#

[4]:
# QTM modules
modules = cluster.get_connected_modules(lambda mod: mod.is_qtm_type)
# This uses the module of the correct type with the lowest slot index
module = list(modules.values())[0]

Binned Mode - Setup#

To demonstrate the binned mode of the QTM, we will send a sequence of digital pulses from one output channel (channel 1) and detect them on another input channel (channel 5).

The pulse sequence consists of uniformly spaced 20 ns-wide TTL pulses, where the delay between each pulse is 300 ns.

To capture these pulses, we will:

  • Setup Binned Mode to acquire the timetags of the events we want to record.

  • Open an acquisition window on the input channel

  • Set the reference time at the start of the first window, so each timetag represents a timestamp relative to this reference

The goal is to observe the detected TTL pulses and verify that their timetags correspond to the expected timing defined by the pulse sequence.

Here we utilize trigger based acquisition to efficiently capture every single pulse arriving at the QTM and store the resultant timedelta (difference of the event timetag from the reference timetag) in a data structure called the bin.

This demonstration will allow us to circumvent the memory limit of the scope mode (limited to 2048 timetags, see the QTM Scope Mode Tutorial), by opening and closing an acquisition window based on a trigger, which is the event detection itself. In this case, the memory limit for the number of saved events is 3.000.000, which corresponds to the total number of bins available for all 8 sequencers (all together) within a QTM.

Even though this approach allows us to store more timetags, it is important to note that the maximum throughput of the timetags is now set by the minimum duration of the acquisition window (300 ns) which is much larger than the intrinsic TDC ‘dead time’ of the QTM (44 ns). This is due to an inherent trade-off between being able to timetag high flux signals and signals with high number of events.

To enable trigger-based closing of the acquisition window, we need to make sure that the minimum duration between two events in the signal is greater than 252 ns, which represents the latency in our trigger network. In case a trigger is sent to the trigger network less than 252 ns after the previous one, it will be missed, and a warning flag will be raised in the sequencer forwarding the trigger.

Configure QTM#

Firstly, we need to configure the QTM’s channels. The QTM provides a one-to-one mapping between the inputs and outputs of its eight sequencers: configuring a channel means configuring the sequencer with the same index.

Note that each QTM channel can operate as either an input or an output. Their indexing in the ``qblox_instruments`` API goes from ``Module.io_channel0`` to ``Module.io_channel7``, whereas the index label on the front panel of the module (this is what is referred to in the text above and throughout the script) goes from 1 to 8.

In this demonstration:

  • We will use channel 1 (front panel label) as an output, to send digital TTL pulses.

  • These pulses will be detected and acquired by channel 5 (also front panel label), configured as an input.

  • Note that a physical connection via a SMP cable is required for that tutorial between channel 1 and channel 5.

[5]:
# Define the pulse duration for our output loop
PULSE_DURATION: int = 20  # ns

# The threshold voltage that that a signal need to pass in order to be classed a detection event.
# NOTE: The minimum set value of the analog threshold for successful detection of counts and timetags is 400 mV (corresponding to a THRESHOLD value of 0.4).
# Input signals at lower voltage than 400mV will not register as counts (or timetags) even if the signal value is higher than the set analog threshold.
THRESHOLD: float = 0.5

ACQ_REPS: int = 3000000  # Number of bins, i.e timetags to acquire

WAIT_TIME: int = 500  # ns
[6]:
# Define some QCoDeS parameters for the binned acquisition.

# Synchronizing the channels.
module.sequencer0.sync_en(True)
module.sequencer4.sync_en(True)

# Setting channel 1 to output.
module.io_channel0.mode("output")

# Setting channel 5 to input as well as the threshold for event detection.
module.io_channel4.mode("input")
module.io_channel4.analog_threshold(THRESHOLD)

# Set up channel 5 to send a trigger on the trigger network with trigger address=1 everytime it detects an event.
module.io_channel4.forward_trigger_en(True)
module.io_channel4.forward_trigger_mode("rising")
module.io_channel4.forward_trigger_address(1)

# Set the reference to which all timetags are measured.
# The time reference is configured through a Q1ASM instruction.
# We also configure the time source to be 'first', meaning the timetag from the first detection will be used.
module.io_channel4.binned_acq_time_ref("sequencer")
module.io_channel4.binned_acq_time_source("first")
module.io_channel4.binned_acq_on_invalid_time_delta("record_0")

Create an acquisition dictionary to store timetags and upload the programs to the appropriate sequencer.

[7]:
# Define acquisition dictionary.
acquisition_modes: dict[str, dict[str, int]] = {
    "timetag_single": {"num_bins": 1, "index": 0},
    "timetag_fully_binned": {"num_bins": ACQ_REPS, "index": 1},
}

Define programs for sending and acquiring pulses.

[8]:
# Program to output digital pulses.
program_pulse = f"""
    move            {WAIT_TIME}, R0
    move            {ACQ_REPS}, R1
    wait_sync       4

    loop:
        set_digital 1,1,0  # level (1=high), mask (always 1 for v1), fine_delay in steps of 1/128 ns
        upd_param   4
        wait        {PULSE_DURATION - 4}
        set_digital 0,1,0
        upd_param   4
        wait        {WAIT_TIME - 4}
        loop        R1, @loop
    stop
"""
[9]:
# Program to acquire timetags.
program_bin = f"""
    move            0, R0
    move            {ACQ_REPS},R1
    move            0, R2

    wait_sync       4

    # Setting the time reference.
    # Note that this will lead to the start of the first acquisition window to be the fixed time reference.
    set_time_ref

    loop:
        # set_time_ref                      # If we set the reference here, the time reference would be reset to the start of each acquisition window in each iteration.
        acquire_timetags 1, R0, 1, R2, 4    # Opening the acquisition window and storing the data in bin 'R0'. Arguments in order are: acquisition index, bin index, window open(1)/close(0), fine_delay, duration
        wait_trigger     1, 4               # Waiting until an event is detected
        acquire_timetags 1, R0, 0, R2, 4    # Closing the acquisition window
        add              R0, 1, R0          # Incrementing the bin for the next iteration
        nop
        loop             R1, @loop          #Repeat loop until R1 is 0
    stop
"""
[10]:
# Upload the pulse program to the sequencer
module.sequencer0.sequence(
    {
        "waveforms": {},
        "weights": {},
        "acquisitions": {},
        "program": program_pulse,
    }
)

# Upload the acquisitions and program to the sequencer
module.sequencer4.sequence(
    {
        "waveforms": {},
        "weights": {},
        "acquisitions": acquisition_modes,
        "program": program_bin,
    }
)
[11]:
# Arm sequencers.
module.arm_sequencer(0)
module.arm_sequencer(4)
[12]:
# Start sequencer.
module.start_sequencer()
[13]:
# Print status of sequencer.
print(module.get_sequencer_status(0))
print(module.get_sequencer_status(4))
Status: OKAY, State: RUNNING, Exit Code: 0, Info Flags: NONE, Warning Flags: NONE, Error Flags: NONE, Log: []
Status: OKAY, State: RUNNING, Exit Code: 0, Info Flags: ACQ_BINNING_DONE, Warning Flags: NONE, Error Flags: NONE, Log: []
[14]:
# Wait for the acquisition to finish with a timeout period of one minute.
print(module.get_acquisition_status(4, 1))
# Extract the acquisition from the sequencer.
if cluster.is_dummy:
    import pickle as pkl

    with open("dependencies/150_timetagging_binned_acq_data.pkl", "rb") as f:
        # dummy data if the dummy cluster is used
        acq = pkl.load(f)
else:
    acq = module.get_acquisitions(4)
True

Let us see what the fetched datastructure looks like.

[15]:
# Please uncomment this cell when executing the notebook to see the output!!
# acq

We see that there is a dictionary corresponding to each acquisition index in the uploaded acquisitions dictionary (named timetag_single, timetag_fully_binned in this case). In the stored bin datastructure (acq["timetag_fully_binned"]["acquisition"]["bins"]) we see:

  • count - corresponding to the number of detected events in the corresponding acquisition window. In our case, we expect the count in every bin to be exactly 1 since we shift to the next bin as soon as we detect an event.

  • timedelta - the timetag of the specified event with respect to the time reference. In this case, we do not miss any timetags, and each timedelta value records the timestamp of the incoming event with respect to the start of the first window.

  • threshold - records what proportion of the windows at least one event crossed the analog threshold .i.e., in what ratio of windows writing to that bin, at least 1 count was detected.

  • avg_cnt - the number of times that the bin was written into (number of windows that wrote into that bin). In our case this should be 1 for every bin. Note that the counts are accumulative, so when writing to the same bin multiple times (for example, for repetitions), the counts will be averaged by using this avg_cnt value.

[16]:
# Extracting both the photon count and an associated time delta for each bin.
# In our case, the time delta is the time difference between the first timetag of each acquisition window and the time reference.

event_counts = acq["timetag_fully_binned"]["acquisition"]["bins"]["count"]
raw_timetags = acq["timetag_fully_binned"]["acquisition"]["bins"]["timedelta"]
# The raw timetags are in units of 1/2048 ns
timetags = [i / (2048) for i in raw_timetags]  # in ns

Below we plot the first 10 acquired timetags to check their timestamps are correct.

[17]:
# Here we plot the acquired timetags for the acquisitions that we carried out.

fig, ax = plt.subplots(1, 1)
ax.plot(timetags[:10], np.ones(len(timetags))[:10], "o", label="Timetags")
ax.set_xlabel(r"Time (ns)")
ax.set_ylabel("Photon detection")
plt.minorticks_on()
plt.grid(which="both")
plt.legend(loc="upper right")
plt.show()
../../../../_images/products_qblox_instruments_tutorials_QTM_150_binned_mode_qtm_32_0.png

Verify that the sum of the event counts in each bin corresponds to the number of pulses sent.

[18]:
# Here we plot the acquired event counts from each bin in the form of a histogram.
# We see indeed that we only ever count 1 photon during each acquisition.

y_hist, x_hist, _ = plt.hist(event_counts)

for i in range(len(x_hist) - 1):
    if y_hist[i] > 0:
        plt.text(x_hist[i], y_hist[i] + 1, str(int(y_hist[i])), fontsize=10)

plt.ylabel("Occurrences")
plt.xlabel("Detected photons")
plt.show()

# Total number of detections in the measurement is found by summing the photon count in all the bins.
print(f"The total event count is {np.sum(event_counts)}")
../../../../_images/products_qblox_instruments_tutorials_QTM_150_binned_mode_qtm_34_0.png
The total event count is 3000000.0

Low-Latency Binned Count Acquisition#

Let us now see how to use the low-latency counter to count incoming events. As we just saw, the number of threshold crossing events is recorded in the binned data structure for each acquisition window. Usually, the source of these counts is the TDC, which can count at a maximum sustained rate of 22.7 MHz. If the experiment requires much faster counting than that, the low-latency counter can be used instead as the source of counts in the bin. Let us see how to use this feature. ### Setup For this example, we will acquire the counts from the QTM channel 4 (IOChannel 3). The source of the signal will be the QCM output 1. We will play a 300 MHz signal from the QCM, which would cross the set threshold every 3.33 ns, much faster than the TDC can count. We acquire the signal for 2000 ns, and therefore expect to count 600 events. Note that the low-latency counter can count at a rate of up to ~ 980 MHz (~ 1 ns dead-time).

[19]:
# Resetting the cluster
cluster.reset()
[20]:
# Finding and connecting to a QCM
qcms = cluster.get_connected_modules(lambda mod: mod.is_qcm_type)
qcm = list(qcms.values())[0]

Let us make some QCoDeS settings for the QCM. We want to synchronize the sequencer, as well as set some static parameters for generating a CW 300 MHz output. For more details about QCM operation please see the Control Sequencer page and the QCM tutorials.

[21]:
qcm.sequencer0.sync_en(True)

# Enabling the NCO
qcm.sequencer0.mod_en_awg(True)
qcm.sequencer0.nco_freq(300e6)

# Channel map to physical outputs
qcm.sequencer0.connect_out0("I")
qcm.sequencer0.connect_out1("Q")

qcm.sequencer0.offset_awg_path0(0)
qcm.sequencer0.offset_awg_path1(0)

# Defining the sequence program.
program_qcm = """

    wait_sync 4
    # Setting the output amplitude
    set_awg_offs 32000, 32000
    upd_param 4
    stop

"""

# Uploading the sequence
qcm.sequencer0.sequence(
    {
        "waveforms": {},
        "weights": {},
        "acquisitions": {},
        "program": program_qcm,
    }
)
[22]:
# Synchronize the two sequencers that will be used.
module.sequencer3.sync_en(True)

# Disable the output mode of the channel we want to acquire with, and set the threshold mode for detection.
module.io_channel3.mode("input")
module.io_channel3.analog_threshold(THRESHOLD)

## ENABLING HE LOW LATENCY COUNTER
module.io_channel3.binned_acq_count_source("low-latency")

# Set the acquisition duration.
ACQUISITION_DURATION: int = 2000
[23]:
# Define acquisition program.
program_acq_low_latency = f"""
    move        0, R0
    move        0, R2
    wait_sync   4

    # Waiting for the signal to arrive from the QCM
    wait 200

    acquire_timetags 0, R0, 1, R2, 4 # Arguments in order are: acquisition index, bin index, window open(1)/close(0), fine_delay, duration
    wait             {ACQUISITION_DURATION - 4}
    acquire_timetags 0, R0, 0, R2, 4

    stop
"""
[24]:
# Upload the acquisitions program to the sequencer.
module.sequencer3.sequence(
    {
        "waveforms": {},
        "weights": {},
        "acquisitions": acquisition_modes,
        "program": program_acq_low_latency,
    }
)
[25]:
# Arm sequencers.
qcm.sequencer0.arm_sequencer()
module.sequencer3.arm_sequencer()
[26]:
# Start sequencers.
module.start_sequencer()
qcm.start_sequencer()
[27]:
# Check sequencer status.
print(module.get_sequencer_status(3))
print(qcm.get_sequencer_status(0))
Status: OKAY, State: STOPPED, Exit Code: 0, Info Flags: 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: []
[28]:
# Wait for the acquisition to finish with a timeout period of one minute.
print(module.get_acquisition_status(3, 1))
# Extract the acquisition from the sequencer.
if cluster.is_dummy:
    import pickle as pkl

    with open("dependencies/150_low_latency_counting_dataset.pkl", "rb") as f:
        # dummy data if the dummy cluster is used
        acq_low_latency = pkl.load(f)
else:
    acq_low_latency = module.get_acquisitions(3)
True
[29]:
photon_count = acq_low_latency["timetag_single"]["acquisition"]["bins"]["count"]
[30]:
# Verify that the sum of the photon counts in each bin corresponds to the number of pulses sent.
# Total number of detections in the measurement is found by summing the photon count in all the bins.

print(f"The total photon count is {np.sum(photon_count)}")
The total photon count is 0.0

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.

[31]:
# Stop both sequencers.
module.stop_sequencer()
qcm.stop_sequencer()

# Print status of both sequencers (should now say it is stopped).
print(qcm.get_sequencer_status(0))
print(module.get_sequencer_status(3))
print()

# Print an overview of the instrument parameters.
# print("Snapshot:")
# module.print_readable_snapshot(update=True)

# Reset the cluster
cluster.reset()
print(cluster.get_system_status())
Status: OKAY, State: STOPPED, Exit Code: 0, Info Flags: FORCED_STOP, Warning Flags: NONE, Error Flags: NONE, Log: []
Status: ERROR, State: STOPPED, Exit Code: 0, Info Flags: ACQ_BINNING_DONE, FORCED_STOP, Warning Flags: NONE, Error Flags: DIO_TIME_DELTA_INVALID, Log: []

Status: ERROR, Flags: FEEDBACK_NETWORK_CALIBRATION_FAILED, Slot flags: NONE