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

Three qubit repetition code#
Introduction#
Quantum Error Correction (QEC) is a technique where multiple faulty qubits are used to encode a smaller number of logical qubits with better coherence properties. In this application example we will demonstrate how Qblox’s advanced LINQ-based feedback can be used to implement a three-qubit repetition code. This is a type of error correction that protects three data qubits against a single bit-flip error.
The experiment is based on the work by Kelly et al. In this work, they utilized mid-circuit parity measurements of two ancilla qubits to monitor errors in the Z-basis of three data qubits. These errors were then corrected post-measurement.
Our implementation utilizes the same mid-circuit parity measurements; however, the syndromes are encoded and decoded locally on the Field-Programmable Gate Array (FPGA) within each Qblox module. This architecture enables real-time error decoding and hardware-based correction with a latency of less than 700ns. A high-level overview of the experimental configuration is shown in the figure below.

The workflow detailed in this application example follows these core steps:
Performing thresholded measurement of ancilla qubits to construct the error syndrome.
Distributing the syndrome to other sequencers.
Executing on-sequencer decoding of the identified error syndrome.
Triggering a conditional correction pulse on the data qubits based on the decoded syndrome.
Running this example requires two readout sequencers for the ancilla qubits and three control sequencers to manage the correction pulses. This example was written for a QCM (slot 2) and a QRM (slot 4), but any combination of QCM (RF), QRM (RF) or QRC will work. Furthermore, the logic in this example can be easily extended to work with higher numbers of qubits.
[1]:
from __future__ import annotations
import numpy as np
from qcodes.instrument import find_or_create_instrument
from qblox_instruments import Cluster, ClusterType
Connecting to the instrument#
[2]:
cluster_ip = None
cluster: Cluster = find_or_create_instrument(
Cluster,
recreate=True,
name="cluster0",
identifier=cluster_ip,
dummy_cfg=(
{
2: ClusterType.CLUSTER_QCM,
4: ClusterType.CLUSTER_QRM,
}
if cluster_ip is None
else None
),
)
cluster.reset()
# Notation shorthands
qcm = cluster.get_connected_modules(lambda mod: mod.is_qcm_type and not mod.is_rf_type)[2]
qrm = cluster.get_connected_modules(lambda mod: mod.is_qrm_type and not mod.is_rf_type)[4]
readout_sequencer_0 = qrm.sequencer0
readout_sequencer_1 = qrm.sequencer1
control_sequencer_0 = qcm.sequencer0
control_sequencer_1 = qcm.sequencer1
control_sequencer_2 = qcm.sequencer2
Configure readout settings#
To perform the error-correcting cycle, a thresholded ancilla measurement will be performed directly on the FPGA using ThresholdedAcquisition. This measurement style integrates the IQ data taken during measurement and compares the result to a preconfigured threshold value to determine a qubit is in the \(|0 \rangle\) or \(|1 \rangle\) state.
After the measurement is completed and the qubit state has been evaluated, its result is automatically communicated with LINQ-based feedback to any required sequencer present in the Cluster. For additional information about LINQ feedback, we refer to the user guide.
Set up the LINQ communication channel by running the cell below.
[3]:
# Configure LINQ routing settings
LINQ_ID = 50
cluster.set_cmm_route(
id_=[LINQ_ID],
targets=[
control_sequencer_0,
control_sequencer_1,
control_sequencer_2,
],
)
The two cells below configure the readout- and control pulse parameters. Correct readout and control settings will depend on your specific setup and should be calibrated beforehand, for example using the “Single-qubit tuneup notebook”
[4]:
# Common parameters
acq_pulse_duration = 3_000 # ns
acq_pulse_amplitude = 10_000 # NCO units
ctrl_pulse_duration = 500
ctrl_pulse_frequency = 1e5
ctrl_pulse_amplitude = 1.0
# Qubit specific parameters
acq_rotation_q0 = 90 # degrees
acq_threshold_q0 = 100
acq_frequency_q0 = 105e6 # Hz
acq_rotation_q1 = 90 # degrees
acq_threshold_q1 = -100
acq_frequency_q1 = 155e6 # Hz
[5]:
# Connect readout sequencers
qrm.disconnect_outputs()
qrm.disconnect_inputs()
readout_sequencer_0.connect_sequencer("io0_1")
readout_sequencer_1.connect_sequencer("io0_1")
# Sequencer for "qubit" 0
readout_sequencer_0.sync_en(True)
readout_sequencer_0.mod_en_awg(True)
readout_sequencer_0.demod_en_acq(True)
readout_sequencer_0.thresholded_acq_rotation(acq_rotation_q0)
readout_sequencer_0.thresholded_acq_threshold(acq_threshold_q0)
readout_sequencer_0.nco_freq(acq_frequency_q0)
readout_sequencer_0.integration_length_acq(acq_pulse_duration)
# Sequencer for "qubit" 1
readout_sequencer_1.sync_en(True)
readout_sequencer_1.mod_en_awg(True)
readout_sequencer_1.demod_en_acq(True)
readout_sequencer_1.thresholded_acq_rotation(acq_rotation_q1)
readout_sequencer_1.thresholded_acq_threshold(acq_threshold_q1)
readout_sequencer_1.nco_freq(acq_frequency_q1)
readout_sequencer_1.integration_length_acq(acq_pulse_duration)
# Connect control sequencers
qcm.disconnect_outputs()
control_sequencer_0.connect_out0("I")
control_sequencer_1.connect_out1("I")
control_sequencer_2.connect_out2("I")
# Configure control sequencers
control_sequencer_0.sync_en(True)
control_sequencer_0.nco_freq(ctrl_pulse_frequency)
control_sequencer_1.sync_en(True)
control_sequencer_1.nco_freq(ctrl_pulse_frequency)
control_sequencer_2.sync_en(True)
control_sequencer_2.nco_freq(ctrl_pulse_frequency)
Waveforms#
We will play a pi pulse as a correction to the bit-flip error.
[6]:
def pi_pulse(pulse_duration: int, amp: float) -> np.ndarray:
"""
Generate a (Gaussian) pi pulse.
pulse_duration and timestep in nanoseconds.
"""
sigma = pulse_duration / 6
return amp * np.exp(-((np.arange(-pulse_duration / 2, pulse_duration / 2) / sigma) ** 2))
Sending combined thresholded measurement results#
When using thresholded acquisition, the user can choose to write multiple thresholded measurement results into a single string using write-combine. This improves data throughput and allows the encoding of measurement results of up to four qubits per byte of message. To see how write-combine works and how to use it, check out the this section of the user guide.
[7]:
def qrm_sequence(qubit_index: int, n_shots: int) -> str:
"""Generate sequencer program for QRM."""
qrm_sched = f"""
move 0, R0
fb_acq_tb_id {LINQ_ID}, 4 # Specify the communication channel
fb_acq_tb_cfg 1, {2 * qubit_index}, 1, 4 # Enable writecombine
upd_param 4
start:
wait_sync 4 # Sync
reset_ph # Reset NCO phase before measurement
set_awg_offs {acq_pulse_amplitude}, {acq_pulse_amplitude} # Readout tone
upd_param 200
acquire 0, R0, {acq_pulse_duration} # Acquire
set_awg_offs 0, 0 # Stop readout
set_mrk 0 # Turn off marker
upd_param 4
wait 700 # Wait for data to arrive
set_mrk {0b1111} # set all markers HIGH
upd_param 4
add R0, 1, R0 # Increment loop. Comment this line to make it go forever!
nop
jlt R0, {n_shots}, @start # Go back to start
stop
"""
return qrm_sched
Masking and write-combine#
To identify specific ancilla results within the thresholded qubit data from the readout sequencer, we apply a masking process. This technique relies on aligning our mask with the specific syntax of the write-combine string. Each mask is constructed by applying the bit-shift left operator << to the value 1 (binary 00 00 00 01), shifting it by twice the qubit index. The table below demonstrates this logic:
Start value |
Shift |
|
Base 10 |
|---|---|---|---|
|
0 |
|
1 |
|
1 |
|
4 |
|
2 |
|
16 |
When targeting multiple qubits, we combine individual masks using bit-wise addition. Due to the non-overlapping nature of these bit positions, this operation is equivalent to a standard summation:
Name |
Binary mask |
Base 10 |
|---|---|---|
Mask 0 |
|
1 |
Mask 1 |
|
4 |
Mask 2 |
|
16 |
|
|
21 |
This masking strategy allows us to isolate and extract the state of any desired qubit directly from the write-combine string.
[8]:
def mask(qubits: list[int]) -> int:
"""
Create mask.
See the "masking" section above. The sum gives the complete mask over multiple qubits.
"""
m = [1 << 2 * q for q in qubits]
return sum(m)
Decoding#
To apply the bit-flip correction described in the table above, the decoding will be implemented in three steps.
Mask over all of the relevant ancilla qubits in the measurement Implemented by performing a bit-wise
ANDoperation of thewrite-combinestring and the mask. This ensures that we take into account the state of ALL the relevant ancillas.Do a bit-wise comparison of the result, against the trigger condition Implemented by performing a bit-wise
XORoperation of the result against the trigger condition.If the result of the two previous steps is exactly zero the condition is satisfied! Jump to
play.
To convince yourself that this works, consider the following example. The reader is invited to check that this works for the other table entries themselves.
Type |
Value |
Operation |
Meaning |
|---|---|---|---|
R0 |
|
appended string measuring |
|
mask_0 |
|
mask over the last two ancillas |
|
mask_1 |
|
condition to trigger on |
|
R1 |
|
R0 |
Perform the mask over the ancillas |
R3 |
|
R1 |
Check trigger condition |
|
Play if trigger condition is satisfied (R3=0) |
This decoding is implemented in the program of the QCM.
[9]:
def qcm_sequence(ancilla_qubits: list[int], condition: list[int], n_shots: int) -> str:
"""Generate sequencer program for QCM."""
qcm_sched = f"""
move 0, R2
upd_param 4
start:
wait_sync 4 # Sync
wait {acq_pulse_duration} # wait while QRM measuring
wait 700 # Broadcast latency: 500ns
fb_pop_data {LINQ_ID}, R0 # Load string representing syndrome
add R2, 1, R2 # Increment loop. Comment this line to make it go forever!
nop
and R0, {mask(ancilla_qubits)}, R1 # Mask the qubit measurement
nop
xor R1, {mask(condition)}, R3
nop
jlt R3, 1, @play_pulse # If bitflip detected: play pulse
wait {ctrl_pulse_duration} # Ensure equal sequence length
jlt R2, {n_shots}, @start # If no bitflip detected: restart
wait 500
stop
play_pulse:
play 0, 0, {ctrl_pulse_duration} # Play a pi pulse
jlt R2, {n_shots}, @start # Restart
wait 500
stop
"""
return qcm_sched
Uploading the sequences#
[10]:
reps = 10
readout_sequencer_0.sequence(
{
"waveforms": {},
"acquisitions": {"button_0": {"index": 0, "num_bins": reps}},
"weights": {},
"program": qrm_sequence(qubit_index=0, n_shots=reps),
}
)
readout_sequencer_1.sequence(
{
"waveforms": {},
"acquisitions": {"button_1": {"index": 0, "num_bins": reps}},
"weights": {},
"program": qrm_sequence(qubit_index=1, n_shots=reps),
}
)
control_sequencer_0.sequence(
{
"waveforms": {
"pi_pulse": {
"data": pi_pulse(ctrl_pulse_duration, amp=ctrl_pulse_amplitude),
"index": 0,
},
},
"acquisitions": {},
"weights": {},
"program": qcm_sequence(ancilla_qubits=[0, 1], condition=[0], n_shots=reps),
}
)
control_sequencer_1.sequence(
{
"waveforms": {
"pi_pulse": {
"data": pi_pulse(ctrl_pulse_duration, amp=ctrl_pulse_amplitude),
"index": 0,
},
},
"acquisitions": {},
"weights": {},
"program": qcm_sequence(ancilla_qubits=[0, 1], condition=[0, 1], n_shots=reps),
}
)
control_sequencer_2.sequence(
{
"waveforms": {
"pi_pulse": {
"data": pi_pulse(ctrl_pulse_duration, amp=ctrl_pulse_amplitude),
"index": 0,
},
},
"acquisitions": {},
"weights": {},
"program": qcm_sequence(ancilla_qubits=[0, 1], condition=[1], n_shots=reps),
}
)
Running the experiment#
[11]:
readout_sequencer_0.arm_sequencer()
readout_sequencer_1.arm_sequencer()
control_sequencer_0.arm_sequencer()
control_sequencer_1.arm_sequencer()
control_sequencer_2.arm_sequencer()
qrm.start_sequencer()
qcm.start_sequencer()
[12]:
readout_sequencer_0.get_sequencer_status(1)
[12]:
SequencerStatus(status=<SequencerStatuses.OKAY>, state=<SequencerStates.STOPPED>, exit_code=0, info_flags=[<SequencerStatusFlags.ACQ_BINNING_DONE>], warn_flags=[], err_flags=[], log=[])
[13]:
readout_sequencer_0.stop_sequencer()
readout_sequencer_1.stop_sequencer()
control_sequencer_0.stop_sequencer()
control_sequencer_1.stop_sequencer()
control_sequencer_2.stop_sequencer()
[14]:
print(readout_sequencer_0.get_sequencer_status(0))
print(readout_sequencer_1.get_sequencer_status(0))
print(control_sequencer_0.get_sequencer_status(0))
print(control_sequencer_1.get_sequencer_status(0))
print(control_sequencer_2.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: ACQ_BINNING_DONE, 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: []
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: ACQ_BINNING_DONE, Warning Flags: NONE, Error Flags: NONE, Log: []