See also
A Jupyter notebook version of this tutorial can be downloaded here.
Sharing Measurement Results with LINQ-based Feedback#
In this tutorial, we demonstrate LINQ-based feedback of data from the demodulation path of a readout sequencer, which allows sequencers to exchange acquisition data with low latency.
We cover two data types:
I/Q Data — The (weighted) integrated result triggered by the
acquireoracquire_weightedQ1ASM instruction.Thresholded Bits (TB) — a single bit indicating whether the I/Q value exceeded a threshold line in the I/Q plane.
More information can be found in the LINQ-based feedback documentation.
Key Commands: | Command | Purpose | |—|—| | fb_acq_tb_id <ID>, <duration> | Share thresholded bits under <ID> after every acquisition | | fb_acq_iq_id <ID>, <duration> | Share I/Q data under <ID> after every acquisition | | fb_pop_data <ID>, <dest> | Pop the next payload matching <ID> into register <dest> | | fb_pull_data <id_dest>, <dest> | Pop the top FIFO entry; store its ID and payload separately |
ID ranges:
0: disable sending1–15: self-cast (same sequencer)16–255: intra-cast / multi-cast (other sequencers on same or different modules)
Hardware Requirements:
A Qblox Cluster with one QRM connected in loopback mode (Output 1 → Input 1).
Setup#
First, we import the required packages and connect to the instrument.
[1]:
import matplotlib.pyplot as plt # noqa: I001 - Ruff will otherwise put below and it won't be included in generation.
import numpy as np # noqa: I001 - Ruff will otherwise put below and it won't be included in generation.
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]:
# QRM baseband modules
modules = cluster.get_connected_modules(lambda mod: mod.is_qrm_type and not mod.is_rf_type)
# This uses the module of the correct type with the lowest slot index
module = list(modules.values())[0]
[5]:
module.disconnect_outputs()
module.disconnect_inputs()
module.sequencer0.nco_prop_delay_comp_en(True)
module.sequencer0.mod_en_awg(True)
module.sequencer0.demod_en_acq(True)
module.sequencer0.connect_sequencer("io0_1")
Calibrating the Threshold#
Cable length, LO and NCO frequency rotate the integrated IQ result away from the real axis, shifting the classifier boundary. Calibration measures and corrects for this offset; skipping it means the data bits in your write-combine payload will be unreliable.
For details on thresholded measurements, see the rotation and thresholding documentation.
[6]:
def calibrate_threshold(tof: int, pulse_length: int, active_sequencers: list) -> None:
"""Calibrate threshold and rotation for each sequencer."""
for sequencer in module.sequencers:
sequencer.sync_en(False)
prog = f"""
set_awg_offs 15000, 0
upd_param {tof - 4}
set_awg_offs 0, 0
acquire 0, 0, {pulse_length}
stop
"""
for sequencer in active_sequencers:
sequencer.sequence(
{
"waveforms": {},
"program": prog,
"acquisitions": {"single": {"index": 0, "num_bins": 1}},
"weights": {},
}
)
sequencer.arm_sequencer()
module.start_sequencer()
for sequencer in active_sequencers:
acq = sequencer.get_acquisitions()["single"]["acquisition"]["bins"]["integration"]
result = acq["path0"][0] + 1j * acq["path1"][0]
rotation = np.mod(-np.angle(result), 2 * np.pi)
threshold = (np.exp(1j * rotation) * result).real / 2
if np.isnan(threshold):
print(
f"{sequencer.name}: calibration returned NaN — "
"check loopback connection and pulse amplitude."
)
else:
sequencer.thresholded_acq_threshold(threshold)
sequencer.thresholded_acq_rotation(rotation * 360 / (2 * np.pi))
print(
f"{sequencer.name}: rotation = {np.degrees(rotation):.2f}°, threshold = {threshold:.4f}"
)
Experiment Parameters#
[7]:
PULSE_LENGTH = 100
TIME_OF_FLIGHT = 149
ID = 16
calibrate_threshold(TIME_OF_FLIGHT, PULSE_LENGTH, [module.sequencer0])
cluster0_module4_sequencer0: rotation = 315.96°, threshold = 10.1035
Sharing Thresholded Bits (TB) with Intra-cast#
Sequencer 0 optionally plays a square pulse, acquires it, thresholds the result, and intra-casts the thresholded bit to Sequencer 1.
When sharing TB, the hardware packs the result into a 2-bit field:
bit 0 (LSB): thresholded bit (1 = above threshold, 0 = below threshold)
bit 1: valid bit (always 1 by default, confirming the acquisition completed)
This gives:
Pulse sent (above threshold) → received payload =
0b00000011(valid=1, data=1)No pulse (below threshold) → received payload =
0b00000010(valid=1, data=0)
The valid bit can be changed with fb_acq_tb_valid <valid>, <duration>. Multiple TB measurements can be packed using the write-combine feature (covered in the write-combine tutorial).
Configure Module#
[8]:
# Reset router paths to default
cluster.clear_router()
# ID 16 falls in the intra-cast range (16–255); set_local_route routes packets tagged with
# that ID to all sequencers within this module (sequencer 1 will receive the LINQ data).
module.set_local_route(ID)
# Set the integration length of all acquire commands to PULSE_LENGTH
module.sequencer0.integration_length_acq(PULSE_LENGTH)
# Enable sync for both sequencers so wait_sync aligns their timelines
module.sequencer0.sync_en(True)
module.sequencer1.sync_en(True)
Experiment Parameters#
[9]:
SEND_PULSE = True # Toggle to test both acquisition outcomes
Define sender Q1ASM Program#
[10]:
offs = 15000 if SEND_PULSE else 0
# Sender: shares TB under ID, enables write-combine at the configured bit position.
prog_tb_sender = f"""
fb_acq_tb_id {ID}, 4 # Share thresholded bits under ID
wait_sync 4
set_awg_offs {offs}, 0
upd_param {TIME_OF_FLIGHT}
set_awg_offs 0,0
acquire 0, 0, {PULSE_LENGTH} # Acquire & threshold
stop
"""
Define receiver Q1ASM Program#
[11]:
prog_tb_receiver = f"""
wait_sync 4 # Synchronize with sequencer 0
wait 600 # Allow time for data to arrive
fb_pop_data {ID}, R0 # Pop TB payload into R0
stop
"""
Upload Sequences#
[12]:
module.sequencer0.sequence(
{
"waveforms": {},
"program": prog_tb_sender,
"acquisitions": {"single": {"index": 0, "num_bins": 1}},
"weights": {},
}
)
module.sequencer1.sequence(
{
"waveforms": {},
"program": prog_tb_receiver,
"acquisitions": {},
"weights": {},
}
)
Run the Experiment#
[13]:
module.arm_sequencer(0)
module.arm_sequencer(1)
module.start_sequencer()
print("Sequencer 0 status:", module.get_sequencer_status(0))
print("Sequencer 1 status:", module.get_sequencer_status(1))
Sequencer 0 status: Status: OKAY, State: STOPPED, Exit Code: 0, Info Flags: ACQ_SCOPE_DONE_PATH_0, ACQ_SCOPE_DONE_PATH_1, ACQ_BINNING_DONE, Warning Flags: NONE, Error Flags: NONE, Log: []
Sequencer 1 status: Status: OKAY, State: STOPPED, Exit Code: 0, Info Flags: NONE, Warning Flags: NONE, Error Flags: NONE, Log: []
Verify Result#
[14]:
tb_result = module.sequencer1.get_register("R0")
expected_tb = "00000011" if SEND_PULSE else "00000010"
print(f"SEND_PULSE = {SEND_PULSE}")
print(f"Received : {tb_result:08b}")
print(f"Expected : {expected_tb}")
print(
"✓ PASS"
if f"{tb_result:08b}" == expected_tb
else "✗ FAIL — check threshold and loopback connection"
)
SEND_PULSE = True
Received : 00000011
Expected : 00000011
✓ PASS
Sharing Raw I/Q Values with Intra-cast#
Instead of a thresholded bit, we can share the raw I/Q values. The hardware sends I and Q as two consecutive 32-bit LINQ payloads tagged with the same ID, always I first and Q second. Two sequential fb_pop_data calls retrieve them in that order:
First
fb_pop_datacall → I valueSecond
fb_pop_datacall → Q value
Experiment Parameters#
[15]:
SEND_PULSE = True # Reset for IQ section; toggle to test both acquisition outcomes
Define Q1ASM Programs#
[16]:
offs = 15000 if SEND_PULSE else 0
# Sender: shares IQ values under ID after each acquisition.
prog_iq_sender = f"""
fb_acq_iq_id {ID}, 4 # Enable I/Q sharing under ID after each acquisition
wait_sync 4
set_awg_offs {offs}, 0
upd_param {TIME_OF_FLIGHT}
set_awg_offs 0,0
acquire 0, 0, {PULSE_LENGTH} # Acquire I and Q values
stop
"""
Define receiver Q1ASM Program#
[17]:
prog_iq_receiver = f"""
wait_sync 4 # Synchronize with sequencer 0
wait 600 # Allow time for LINQ data to arrive
fb_pop_data {ID}, R0 # Pop I value into R0
fb_pop_data {ID}, R1 # Pop Q value into R1
stop
"""
Upload Sequences#
[18]:
module.sequencer0.sequence(
{
"waveforms": {},
"program": prog_iq_sender,
"acquisitions": {"single": {"index": 0, "num_bins": 1}},
"weights": {},
}
)
module.sequencer1.sequence(
{
"waveforms": {},
"program": prog_iq_receiver,
"acquisitions": {},
"weights": {},
}
)
Run the Experiment#
[19]:
module.arm_sequencer(0)
module.arm_sequencer(1)
module.start_sequencer()
print("Sequencer 0 status:", module.get_sequencer_status(0))
print("Sequencer 1 status:", module.get_sequencer_status(1))
Sequencer 0 status: Status: OKAY, State: STOPPED, Exit Code: 0, Info Flags: ACQ_SCOPE_DONE_PATH_0, ACQ_SCOPE_DONE_PATH_1, ACQ_BINNING_DONE, Warning Flags: NONE, Error Flags: NONE, Log: []
Sequencer 1 status: Status: OKAY, State: STOPPED, Exit Code: 0, Info Flags: NONE, Warning Flags: NONE, Error Flags: NONE, Log: []
Verify Result#
[20]:
i_linq = module.sequencer1.get_register("R0")
q_linq = module.sequencer1.get_register("R1")
print(f"SEND_PULSE = {SEND_PULSE}")
print(f"I (R0): {i_linq}")
print(f"Q (R1): {q_linq}")
SEND_PULSE = True
I (R0): 94636904
Q (R1): 5956770
What Do These Values Mean?#
The raw LINQ value is a large integer whose relationship to physical voltage depends on the module and its configuration. To understand this we sweep the AWG offset across its full range (−32768 to +32767) representing (-1V to 1V) for a QRM. and record the LINQ values at each step.
We also compare against the integrated acquisition returned by get_acquisitions. This is the same underlying measurement, but the hardware accumulates and integrates across the integration window.
Both should be perfectly linear in the input voltage — confirming that the LINQ value can be used directly as a proxy for voltage in feedback decisions.
Run Calibration Sweep#
Rather than re-uploading the sequence for each amplitude step, all 32 steps are embedded in a single Q1ASM program. The sender sweeps through 32 DC offsets with set_awg_offs, storing each acquisition in its own bin. The receiver pops each pair of LINQ payloads into dedicated registers — I into R0–R31, Q into R32–R63 — so the entire sweep completes in a single hardware run.
[21]:
amplitudes = np.linspace(-1.0, 1.0, 32)
awg_ints = np.linspace(-32768, 32767, 32).astype(int)
# Sender: configure IQ sharing once, then iterate through all 32 offsets
p_send = f" fb_acq_iq_id {ID}, 4\n wait_sync 4\n"
for i, awg_int in enumerate(awg_ints):
p_send += (
f" set_awg_offs {awg_int}, 0\n"
f" upd_param {TIME_OF_FLIGHT}\n"
f" acquire 0, {i}, {PULSE_LENGTH}\n"
)
p_send += " stop\n"
# Receiver: pop I into R0–R31 and Q into R32–R63 for each sender step
p_recv = " wait_sync 4\n"
for i in range(32):
p_recv += (
f" wait 600\n"
f" fb_pop_data {ID}, R{i}\n"
f" fb_pop_data {ID}, R{i + 32}\n"
)
p_recv += " stop\n"
linq_vals_i, intg_vals = [], []
module.sequencer0.sequence(
{
"waveforms": {},
"program": p_send,
"acquisitions": {"single": {"index": 0, "num_bins": 32}},
"weights": {},
}
)
module.sequencer1.sequence(
{
"waveforms": {},
"program": p_recv,
"acquisitions": {},
"weights": {},
}
)
module.arm_sequencer(0)
module.arm_sequencer(1)
module.start_sequencer()
_intg = module.sequencer0.get_acquisitions()["single"]["acquisition"]["bins"]["integration"]
for i in range(32):
intg_vals.append(_intg["path0"][i])
linq_vals_i.append(module.sequencer1.get_register(f"R{i}"))
Plot Results#
[22]:
def _fit(vals: list | np.ndarray) -> tuple[np.poly1d, np.ndarray]:
"""Return a degree-1 polynomial fit and its coefficients array."""
c = np.polyfit(amplitudes, vals, 1) # always length deg+1=2, no leading-zero stripping
return np.poly1d(c), c
def _plot_series(ax: plt.Axes, vals: list | np.ndarray, color: str, label: str) -> np.poly1d:
"""Scatter-plot vals and overlay its linear fit on ax."""
fit, c = _fit(vals)
ax.scatter(amplitudes, vals, color=color, zorder=5, s=50, label=f"{label} (measured)")
ax.plot(
amplitudes,
fit(amplitudes),
color=color,
linestyle="--",
linewidth=1.5,
label=f"{label} fit: {c[0]:.3g}·A + {c[1]:.3g}",
)
return fit
[23]:
BLUE = "#3A7DC9"
fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(13, 4))
for ax, vals, title, ylabel in [
(ax1, linq_vals_i, "LINQ fb_pop_data — I", "I value (raw LINQ units)"),
(ax2, intg_vals, "Hardware integration path", "I value (integrated units)"),
]:
ax.axhline(0, color="#888", linewidth=0.8, linestyle=":")
ax.axvline(0, color="#888", linewidth=0.8, linestyle=":")
_plot_series(ax, vals, BLUE, "I")
ax.set_xlabel("Waveform Amplitude (normalized)")
ax.set_ylabel(ylabel)
ax.set_title(title)
ax.legend(fontsize=9)
ax.grid(True, alpha=0.25)
fig.suptitle(
"Waveform Amplitude vs. Measured I Value — LINQ vs. Integrated", fontweight="bold", y=1.01
)
plt.tight_layout()
plt.show()
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.
[24]:
# 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: ERROR, State: STOPPED, Exit Code: 0, Info Flags: FORCED_STOP, ACQ_SCOPE_DONE_PATH_0, ACQ_SCOPE_DONE_PATH_1, ACQ_BINNING_DONE, Warning Flags: NONE, Error Flags: SEQUENCE_PROCESSOR_DATA_OVERFLOW, Log: []
Status: OKAY, State: STOPPED, Exit Code: 0, Info Flags: FORCED_STOP, Warning Flags: NONE, Error Flags: NONE, Log: []