See also
A Jupyter notebook version of this tutorial can be downloaded here.
Scope Mode#
The Qblox Timetagging Module (henceforth referred to as the QTM) allows recording of absolute timetags.
It is highly recommended to go through the Timetag Sequencer page before going through this tutorial.
This tutorial covers the Scope Mode of operation for the QTM as described in the timetag sequencer page. This tutorial will demonstrate how to use each of the three major sub-modes effectively in a Q1ASM sequencer program.
Continuous Scope acquisition
Windowed Scope acquisition
Low-Latency Burst
[1]:
from __future__ import annotations
import pickle as pkl
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]
Setup#
To demonstrate Scope Mode and the QTM’s timetagging capability, 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:
20 ns-wide TTL pulses
spaced with increasing delays, where the delay between each pulse increases linearly by 1 \(\mu s\) relative to the previous one. The first pulse starts 80 ns after the beginning of the acquisition window.
To capture these pulses, we will:
open an acquisition window on the input channel
enable Scope Mode to acquire the timetags
The goal is to observe the detected TTL pulses and verify that their timetags correspond to the expected timing defined by the pulse sequence.
[5]:
# Define the pulse duration for our output loop and the initial wait time.
PULSE_DURATION: int = 20 # ns
INITIAL_WAIT_TIME: int = 80 # ns
INCREASING_WAIT_TIME: int = 1000 # ns
# Set the pulse number to the total number of timetags we wish to acquire.
PULSE_NUMBER: int = 20
# 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
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 this tutorial between channel 1 and channel 5.
[6]:
# Synchronize the two sequencers that will be used.
module.sequencer0.sync_en(True)
module.sequencer4.sync_en(True)
# Set IO Switch of channel 1 to output
module.io_channel0.mode("output")
# Set IO Switch of channel 4 to input, and set the threshold for detection.
module.io_channel4.mode("input")
module.io_channel4.analog_threshold(THRESHOLD)
# Set the sequencer to trigger the scope mode.
module.io_channel4.scope_trigger_mode("sequencer")
Define a program for the output of digital pulses.
[7]:
program_pulse = f"""
move {INITIAL_WAIT_TIME - 4}, R0
move {PULSE_NUMBER}, 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 R0
add R0,{INCREASING_WAIT_TIME}, R0
nop
loop R1, @loop
stop
"""
Low-latency burst mode#
The TDC inside the QTM has an internal buffer of 16 entries and can send detection events to the FPGA at a rate of 44 ns per timetag. This creates a buffer budget of 704 ns, or the total time available for the TDC to buffer timetags.
If the oversampling of the TDC is disabled, any pulse sequences arriving at the QTM faster than a period of 44 ns will be handled correctly. This means the QTM can timetag with a lower latency until the total time buffer is filled up.
Here we will repeat the initial scope acquisition, except we will acquire a signal with a click rate faster than the 44 ns latency limit of the QTM.
Note that the QTM can acquire timetags at this reduced latency, but only in short bursts that are determined by the buffer memory of the TDC module inside.
Setting up the QTM to perform low latency time-tagging#
[21]:
# Reset the cluster before we begin this measurement scheme.
cluster.reset()
print(cluster.get_system_status())
Status: ERROR, Flags: FEEDBACK_NETWORK_CALIBRATION_FAILED, Slot flags: SLOT12_FEEDBACK_NETWORK_CALIBRATION_FAILED
Here we disable the oversampling of the TDC in both quads of the QTM which will enable us to carry out a burst measurement.
[22]:
# The channels in the QTM are grouped into two quads with four channels, that are indexed as
# 0 and 1 respectively.
# The TDC oversampling can be disabled at the quad level using the following command.
module.quad0.timetag_oversampling("disabled")
module.quad1.timetag_oversampling("disabled")
Here we configure the two channels again to acquire signals in windowed scope mode.
[23]:
# Synchronize the sequencers that will be used.
module.sequencer0.sync_en(True)
module.sequencer4.sync_en(True)
[24]:
# Enable the output channel to be controlled by the sequencer
module.io_channel0.mode("output")
Configure the input channel#
[25]:
# Disable the output mode of the channel we want to acquire with, and set the threshold
# mode for detection.
module.io_channel4.mode("input")
module.io_channel4.analog_threshold(THRESHOLD)
module.io_channel4.scope_trigger_mode("sequencer")
module.io_channel4.scope_mode("timetags-windowed")
[26]:
PULSE_DURATION: int = 4 # ns
WAIT_TIME: int = 16 # ns
We will now define a Q1ASM program to produce a series of TTL pulses that arrive with a higher frequency. In this case we have set the wait time to be 16 ns and the pulse duration to be 4 ns. This results in the spacing between successive rising edges to be 20 ns, which is well below the 44 ns latency limit.
We will transmit 26 pulses, which we hope to timetag with 20 ps precision, demonstrating the low latency burst mode.
[27]:
# Define parameter that determines the number of pulses.
NUM_PULSE: int = 26
[28]:
program_pulse_high_rate = f"""
move {WAIT_TIME}, R0
move {NUM_PULSE}, 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 {PULSE_DURATION}
set_digital 0,1,0
upd_param {WAIT_TIME}
loop R1, @loop
stop
"""
We will use the same acquisition program as before, and trigger a windowed scope acquisition.
[29]:
ACQUISITION_DURATION: int = 500 # ns
[30]:
# Define acquisition program.
program_acq = f"""
move 0, R0
move 0, R2
wait_sync 4
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}
acquire_timetags 0, R0, 0, R2, 4
stop
"""
[31]:
# Define acquisition dictionary.
acquisition_modes: dict[str, dict[str, int]] = {"timetag_single": {"num_bins": 1, "index": 0}}
[32]:
# Upload the pulse program to the sequencer.
module.sequencer0.sequence(
{
"waveforms": {},
"weights": {},
"acquisitions": {},
"program": program_pulse_high_rate,
}
)
# Upload the acquisitions program to the sequencer.
module.sequencer4.sequence(
{
"waveforms": {},
"weights": {},
"acquisitions": acquisition_modes,
"program": program_acq,
}
)
[33]:
module.sequencer0.arm_sequencer()
module.sequencer4.arm_sequencer()
[34]:
module.start_sequencer()
[35]:
# Check sequencer status.
print(module.get_sequencer_status(0))
print(module.get_sequencer_status(4))
Status: OKAY, State: STOPPED, Exit Code: 0, Info Flags: NONE, Warning Flags: NONE, Error Flags: NONE, Log: []
Status: OKAY, State: STOPPED, Exit Code: 0, Info Flags: ACQ_BINNING_DONE, Warning Flags: NONE, Error Flags: NONE, Log: []
[36]:
# Wait for the acquisition to finish with a timeout period of one minute.
print(module.get_acquisition_status(4, 1))
# Extract scope data.
if cluster.is_dummy:
with open("dependencies/130_low_latency_timetagging_dataset.pkl", "rb") as f:
# dummy data if the dummy cluster is used
scope_data_low_latency = pkl.load(f)
else:
scope_data_low_latency = module.io_channel4.get_scope_data()
True
Let us check how many timetags we acquired in the window. We expect to see (NUM_PULSE + 2) = 28 timetags, NUM_PULSE coming from each rising edge and 2 from the opening and closing of the acquisition window.
[37]:
print(f"Number of acquired timetags in window: {len(scope_data_low_latency)}")
# Convert the units of timetags to ns.
timetags_low_latency = np.array(
[(i[1] - scope_data_low_latency[0][1]) / (2048) for i in scope_data_low_latency]
) # in ns
Number of acquired timetags in window: 28
The acquired timetags of the high frequency signal are displayed below.
[38]:
timetags_to_plot = timetags_low_latency[0:10]
fig, ax = plt.subplots(1, 1)
for i in range(len(timetags_to_plot)):
if scope_data_low_latency[i][0] == "OPEN":
ax.axvline(timetags_to_plot[i], color="g", label="Window open")
elif scope_data_low_latency[i][0] == "CLOSE":
ax.axvline(timetags_to_plot[i], color="r", label="Window close")
elif scope_data_low_latency[i][0] == "RISE":
ax.plot(timetags_to_plot[i], 1.0, marker="o", color="blue")
ax.set_xlabel(r"Time (ns)")
ax.set_ylabel("Photon detection")
ax.minorticks_on()
# Major grid (default style)
ax.grid(which="major", linestyle="-", axis="x")
# Minor grid (customized style)
ax.grid(which="minor", linestyle=":", axis="x")
plt.legend(loc="upper right")
plt.show()