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:

  1. TDC (Time-to-Digital Converter) Path: Provides high-resolution (\(1/128\) ns) data.

  2. 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

High-Res Timetags

\(1/128\) ns

\(1.26\) μs

\(1\) μs

\(910\) ns

fb_tdc_tags_id

Time-Delta

\(1/128\) ns

\(1.26\) μs

\(1\) μs

\(910\) ns

fb_tdc_tdelta_id

Low-Latency Timetags

\(1\) ns

\(480\) ns

\(236\) ns

\(146\) ns

fb_llp_tags_id

Rising Edge Counts (TTLs)

\(1\) ns

\(480\) ns

\(236\) ns

\(146\) ns

fb_llp_ttls_id

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 Timetags#

Timetag sharing is essential for some quantum communication experiments. This section demonstrates both high-resolution(resolution of 1/128 ns) and low-latency (resolution of 1 ns) timetag sharing.

Note: Timetag results are shared via LINQ-based feedback as soon as they arrive, not when the acquisition window closes (unlike TTL counts and time-deltas).

More information: LINQ-based feedback documentation

High-Resolution Timetags#

This experiment shares two high-resolution timetags between sequencers:

  • The sender (sequencer 0) emits two 20 ns pulses separated by TIME_TAG_DELTA ns (start-to-start).

  • The receiver (sequencer 4) acquires the timetags and forwards them via LINQ feedback back to the sender.

  • The sender pops both timetag values from the feedback queue.

  • We verify the delta between the two timetags matches the expected value.

Experiment Parameters#

[7]:
TIME_TAG_DELTA = 100

Define Sequences#

[8]:
prog_sender = f"""
wait_sync       4                       # Synchronize with receiver sequencer

set_digital 1,1,0                       # First 20 ns pulse
upd_param       20
set_digital 0,1,0
upd_param       {TIME_TAG_DELTA - 20}

set_digital 1,1,0                       # Second 20 ns pulse after TIME_TAG_DELTA ns
upd_param       20
set_digital 0,1,0
upd_param       80

wait            1000                    # Wait for timetags from receiver sequencer
fb_pop_data     {ID}, R0               # Pop first timetag
fb_pop_data     {ID}, R1               # Pop second timetag

stop
"""

prog_receiver = f"""
fb_tdc_tags_id  {ID}, 8                # Enable high-res timetag sharing on ID {ID}
wait_sync       4                       # Synchronize with sender sequencer

acquire_timetags 0, 0, 1, 0, 4         # Open acquisition window
wait            {TIME_TAG_DELTA}
acquire_timetags 0, 0, 0, 0, 4         # Close acquisition window

fb_tdc_tags_id  0, 8                   # Disable timetag sharing
stop
"""

Upload Sequences#

[9]:
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#

[10]:
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#

Absolute timetags are expressed in multiples of \(1/128\) ns, relative to the module’s internal clock. We convert to ns and compute the delta between the two events.

[11]:
first_time_tag_raw = module.sequencer0.get_register("R0")
second_time_tag_raw = module.sequencer0.get_register("R1")

first_time_tag = first_time_tag_raw / 128  # convert to ns
second_time_tag = second_time_tag_raw / 128  # convert to ns
time_tag_delta = second_time_tag - first_time_tag

print(f"TIME_TAG_DELTA (expected) : {TIME_TAG_DELTA} ns")
print(f"First timetag             : {first_time_tag:.4f} ns")
print(f"Second timetag            : {second_time_tag:.4f} ns")
print(f"Measured delta            : {time_tag_delta:.4f} ns")
print(
    "✓ PASS"
    if abs(time_tag_delta - TIME_TAG_DELTA) <= 1
    else "✗ FAIL — check threshold and loopback connection"
)
TIME_TAG_DELTA (expected) : 100 ns
First timetag             : -2048820.7500 ns
Second timetag            : -2048720.7891 ns
Measured delta            : 99.9609 ns
✓ PASS

Low-Latency Timetags#

To share timetags via the low-latency path, replace fb_tdc_tags_id with fb_llp_tags_id. Low-latency timetags take less time to propagate to another sequencer. For simplicity, we keep the sender program identical; only the receiver program needs to change.

Note: Low-latency timetags have 1 ns resolution (vs. 1/128 ns for TDC), so no unit conversion is needed — values are already in ns.

Define Sequences#

[12]:
prog_receiver_llp_tags = f"""
fb_llp_tags_id  {ID}, 8                # Enable low latency timetag sharing on ID {ID}
wait_sync       4                      # Synchronize with sender sequencer

acquire_timetags 0, 0, 1, 0, 4         # Open acquisition window
wait            {TIME_TAG_DELTA}
acquire_timetags 0, 0, 0, 0, 4         # Close acquisition window

fb_llp_tags_id  0, 8                   # Disable timetag sharing
stop
"""

Upload Sequences#

[13]:
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_llp_tags,
    }
)

Run the Experiment#

[14]:
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#

Low-latency timetags are in units of 1 ns — no conversion needed.

[15]:
first_time_tag = module.sequencer0.get_register("R0")
second_time_tag = module.sequencer0.get_register("R1")
time_tag_delta = second_time_tag - first_time_tag

print(f"TIME_TAG_DELTA (expected) : {TIME_TAG_DELTA} ns")
print(f"First timetag             : {first_time_tag} ns")
print(f"Second timetag            : {second_time_tag} ns")
print(f"Measured delta            : {time_tag_delta} ns")
print(
    "✓ PASS"
    if time_tag_delta == TIME_TAG_DELTA
    else "✗ FAIL — check threshold and loopback connection"
)
TIME_TAG_DELTA (expected) : 100 ns
First timetag             : 376072467 ns
Second timetag            : 376072567 ns
Measured delta            : 100 ns
✓ PASS

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_DELTA ns.

  • 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_COUNT pulses, each 20 ns wide, with a start-to-start period of TIME_DELTA + 16 ns.

  • 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: []