See also
A Jupyter notebook version of this tutorial can be downloaded here.
LINQ-based feedback with QTM#
Beyond sharing registers between sequencers, the LINQ-based feedback allows the QTM to selfcast, intra-cast and multi-cast hardware-level timing data—such as timetags, time-deltas, and edge counts—directly from its datapath to any sequencer in the cluster.
This tutorial covers the two primary paths for timing data sharing:
TDC (Time-to-Digital Converter) Path: Provides high-resolution (\(1/128\) ns) data.
LLP (Low Latency Path): Provides lower resolution (\(1\) ns) but significantly faster delivery.
The table below summarizes the available features, their respective paths, resolutions, latencies, and the command prefixes used to enable them:
Data Types |
Resolution |
Multi-cast latency |
Intra-cast latency |
Self-cast latency |
Command Prefix |
|---|---|---|---|---|---|
\(1/128\) ns |
\(1.26\) μs |
\(1\) μs |
\(910\) ns |
|
|
\(1/128\) ns |
\(1.26\) μs |
\(1\) μs |
\(910\) ns |
|
|
\(1\) ns |
\(480\) ns |
\(236\) ns |
\(146\) ns |
|
|
\(1\) ns |
\(480\) ns |
\(236\) ns |
\(146\) ns |
|
To share data, the module sequencer must be configured to assign an ID to the desired feature using the appropriate command prefix. Receiving sequencers can then wait for and retrieve this data using the wait and fb_pop_data commands.
This can be done within the same module (intra-cast) or across different modules (multi-cast) within the cluster:
IDs 1–15: reserved for self-casting within the same sequencer.
IDs 16–255: used for intra-cast or multi-cast to other sequencers.
ID 0: reserved to disable sharing for the specified feature.
Sharing is enabled and disabled as follows:
fb_tdc_tags_id <event_id>, <time> # Enable sharing
acquire_timetags 0, 0, 1, 0, {ACQ_DURATION} # Open acquisition window
acquire_timetags 0, 0, 0, 0, 4 # Close acquisition window
fb_tdc_tags_id 0, 4 # Disable sharing
Hardware Requirements#
A Qblox Cluster with one QTM module
Connections#
Channel 1 of the module should be connected to channel 5 of the module
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: 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]
Configure Routing for Intra-Cast#
IDs 16–255 support intra-cast, multi-cast or broadcast (multi-casting to all sequencers in all modules). Data is sent to all sequencers in the module or cluster respectively. Before sequencers can receive on an event ID, routing must be configured to allow them to listen. Intra-cast routes are set with set_local_route.
More info: Routing documentation or Routing tutorial
[5]:
ID = 16
cluster.clear_router()
module.set_local_route(ID)
Hardware Configuration#
[6]:
# Enable sync for all participating sequencers
module.sequencer0.sync_en(True)
module.sequencer4.sync_en(True)
# Configure QTM port 1 for output (looped back to channel 5); io_channel0 = port 1 (0-indexed)
module.io_channel0.mode("output")
# Configure QTM port 5 for input (receiving from channel 1); io_channel4 = port 5 (0-indexed)
module.io_channel4.mode("input")
module.io_channel4.analog_threshold(0.5)
# Configure acquisition settings for channel 5
module.io_channel4.binned_acq_time_ref("first4")
module.io_channel4.binned_acq_time_source("second")
module.io_channel4.binned_acq_on_invalid_time_delta("record_0")
Sharing High-Resolution Time-Delta#
This experiment shares a single computed time-delta (the interval between two pulses) rather than two individual timetags:
The sender (sequencer 0) emits two 4 ns pulses separated by
TIME_DELTAns.The receiver (sequencer 4) acquires the pulses and sends the time-delta via LINQ feedback back to the sender.
The sender pops the time-delta from the feedback queue.
We verify the received value matches
TIME_DELTA.
Experiment Parameters#
[16]:
TIME_DELTA = 100
Define Sequences#
[17]:
prog_sender = f"""
wait_sync 4 # Synchronize with receiver sequencer
set_digital 1,1,0 # First 4 ns pulse
upd_param 4
set_digital 0,1,0
upd_param {TIME_DELTA - 4}
set_digital 1,1,0 # Second 4 ns pulse after TIME_DELTA ns
upd_param 4
set_digital 0,1,0
upd_param 4
wait 1200 # Wait for time-delta from receiver sequencer
fb_pop_data {ID}, R0 # Pop time-delta value
stop
"""
prog_receiver = f"""
fb_tdc_tdelta_id {ID}, 8 # Enable high-res time-delta sharing on ID {ID}
wait_sync 4 # Synchronize with sender sequencer
acquire_timetags 0, 0, 1, 0, 4 # Open acquisition window
wait {TIME_DELTA}
acquire_timetags 0, 0, 0, 0, 4 # Close acquisition window; time-delta is sent
fb_tdc_tdelta_id 0, 8 # Disable time-delta sharing
stop
"""
Upload Sequences#
[18]:
module.sequencer0.sequence(
{
"waveforms": {},
"weights": {},
"acquisitions": {},
"program": prog_sender,
}
)
module.sequencer4.sequence(
{
"waveforms": {},
"weights": {},
"acquisitions": {"timetag_single": {"num_bins": 1, "index": 0}},
"program": prog_receiver,
}
)
Run the Experiment#
[19]:
module.sequencer0.arm_sequencer()
module.sequencer4.arm_sequencer()
module.start_sequencer()
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: []
Verify Result#
The time-delta is expressed in units of \(1/128\) ns. Convert to ns before comparing.
[20]:
time_delta_raw = module.sequencer0.get_register("R0")
time_delta_ns = time_delta_raw / 128
print(f"TIME_DELTA (expected) : {TIME_DELTA} ns")
print(f"Received : {time_delta_ns:.4f} ns")
print(
"✓ PASS"
if abs(time_delta_ns - TIME_DELTA) <= 1
else "✗ FAIL — check threshold and loopback connection"
)
TIME_DELTA (expected) : 100 ns
Received : 99.9375 ns
✓ PASS
Sharing Low-Latency TTL Counts#
This experiment counts the number of rising-edge pulses detected during an acquisition window:
The sender (sequencer 0) emits
TTL_COUNTpulses, each 20 ns wide, with a start-to-start period ofTIME_DELTA + 16ns.The receiver (sequencer 4) acquires the pulses and sends the TTL count via LINQ feedback to the sender.
The sender pops the count from the feedback queue.
We verify the received count matches
TTL_COUNT.
Experiment Parameters#
[21]:
TTL_COUNT = 200
TIME_DELTA = 100
Define Sequences#
[22]:
prog_sender = f"""
move {TTL_COUNT}, R1
wait_sync 4 # Synchronize with receiver sequencer
send_pulse:
set_digital 1,1,0 # 20 ns pulse
upd_param 20
set_digital 0,1,0
upd_param {TIME_DELTA - 4}
loop R1, @send_pulse
wait 2000 # Wait for TTL count from receiver sequencer
fb_pop_data {ID}, R0 # Pop TTL count value
stop
"""
# Acquisition window must cover: (pulse width + gap) * count + loop overhead
ACQ_WAIT = (TTL_COUNT * TIME_DELTA) + (24 * TTL_COUNT)
prog_receiver = f"""
fb_llp_ttls_id {ID}, 8 # Enable TTL count sharing on ID {ID}
wait_sync 4 # Synchronize with sender sequencer
acquire_timetags 0, 0, 1, 0, 4 # Open acquisition window
wait {ACQ_WAIT} # Wait for all pulses (including loop overhead)
acquire_timetags 0, 0, 0, 0, 4 # Close acquisition window; TTL count is sent
fb_llp_ttls_id 0, 8 # Disable TTL count sharing
stop
"""
Upload Sequences#
[23]:
module.sequencer0.sequence(
{
"waveforms": {},
"weights": {},
"acquisitions": {},
"program": prog_sender,
}
)
module.sequencer4.sequence(
{
"waveforms": {},
"weights": {},
"acquisitions": {"timetag_single": {"num_bins": 1, "index": 0}},
"program": prog_receiver,
}
)
Run the Experiment#
[24]:
module.sequencer0.arm_sequencer()
module.sequencer4.arm_sequencer()
module.start_sequencer()
print(module.get_sequencer_status(0))
print(module.get_sequencer_status(4))
Status: OKAY, State: STOPPED, Exit Code: 0, Info Flags: FORCED_STOP, Warning Flags: NONE, Error Flags: NONE, Log: []
Status: OKAY, State: STOPPED, Exit Code: 0, Info Flags: ACQ_BINNING_DONE, FORCED_STOP, Warning Flags: NONE, Error Flags: NONE, Log: []
Verify Result#
[25]:
ttl_count = module.sequencer0.get_register("R0")
print(f"TTL_COUNT (expected) : {TTL_COUNT}")
print(f"Received : {ttl_count}")
print("✓ PASS" if ttl_count == TTL_COUNT else "✗ FAIL — check threshold and loopback connection")
TTL_COUNT (expected) : 200
Received : 200
✓ PASS
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.
[26]:
# 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, Warning Flags: NONE, Error Flags: NONE, Log: []
Status: OKAY, State: STOPPED, Exit Code: 0, Info Flags: NONE, Warning Flags: NONE, Error Flags: NONE, Log: []