See also

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

image0

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.

QEC Schematic

The workflow detailed in this application example follows these core steps:

  1. Performing thresholded measurement of ancilla qubits to construct the error syndrome.

  2. Distributing the syndrome to other sequencers.

  3. Executing on-sequencer decoding of the identified error syndrome.

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

1 << (2 * shift)

Base 10

00 00 00 01

0

00 00 00 01

1

00 00 00 01

1

00 00 01 00

4

00 00 00 01

2

00 01 00 00

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

00 00 00 01

1

Mask 1

00 00 01 00

4

Mask 2

00 01 00 00

16

SUM

00 01 01 01

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.

  1. Mask over all of the relevant ancilla qubits in the measurement Implemented by performing a bit-wise AND operation of the write-combine string and the mask. This ensures that we take into account the state of ALL the relevant ancillas.

  2. Do a bit-wise comparison of the result, against the trigger condition Implemented by performing a bit-wise XOR operation of the result against the trigger condition.

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

11 10 10 11

appended string measuring 1 0 0 1

mask_0

00 00 01 01

mask over the last two ancillas X X _ _

mask_1

00 00 00 01

condition to trigger on X X 0 1

R1

00 00 00 01

R0 AND mask_0

Perform the mask over the ancillas

R3

00 00 00 00

R1 XOR mask_1

Check trigger condition

jlt R3, 1, @play

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