See also

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

Pseudo Random Number Generation for randomized benchmarking#

Oracle for the recovery gate computation#

We want to simulate the outcome of the random number generator on the computer, before running it on the instrument (so that we can compute the recovery gate beforehand). We call this approach “oracle”. We choose to implement the Xorshift32 algorithm to perform the RNG, because it is quite simple to do and only involves basic bitwise operations (logical bit shifts and XOR operations).

Important

The pythonic implementation of the Xorshift32, as well as the subsequent computation of the correct sequence of gates (given the seed and the length) is implemented in the xorshift, generate_random_sequence and randomized_benchmarking_sequence functions of the rb_utils.py module.

  • we generate a sequence of a given length (e.g. 30) where the first 29 gates are random, and the last gate is chosen to ensure the net product product of all the gates, is the identity operator;

  • we double check that the gate generation is correct by computing the net gate and comparing it to the identity ;

This notebook demonstrates an example of randomized benchmarking of single qubit gates. We therefore make use of the single qubit Clifford group, containing 24 elements. That means that the 32-bits random number coming out of the Xorshift32 loop must be remapped onto the [0, 23] integer range. This feature is again achieved with Q1ASM instructions. The principle is shown with the figure below: the first 3 bits \(b_{32}\,b_{31}\,b_{30}\) of the random number \(x_{rand}\) are used to select a group of 3 potential numbers, and the remaining 29 bits are used to select which one of three candidates is kept. This method actually relies on the fact that \(24=2^3 \times 3\).

remapping image

[4]:
# generate a random sequence of 30 gates that realizes in total the desired Clifford operator
rb_clifford_indices = randomized_benchmarking_sequence(
    seed=1,
    number_of_gates=30,
    desired_net_clifford=0,  # 0 is the index of the identity: print(SQC._gates_names_dict) to verify it
)
print(f"Sequence of gates: {rb_clifford_indices}")
print(f"Number of gates: {len(rb_clifford_indices)}")

# We check that the sequence of gates realizes the identity
success = (
    SQC.get_clifford_index_from_PLT(
        np.linalg.multi_dot(
            [SQC(i).pauli_transfer_matrix for i in list(reversed(rb_clifford_indices))]
        )
    )
    == 0
)
print(f"Gate sequence generation succeeded: {success}")
Sequence of gates: [0, 0, 14, 1, 13, 4, 3, 2, 11, 16, 14, 8, 16, 0, 0, 18, 4, 18, 1, 23, 13, 21, 7, 20, 0, 8, 13, 1, 5, 8]
Number of gates: 30
Gate sequence generation succeeded: True

Q1 implementation of the xorshift RNG#

[5]:
SEED = 789456123
NUMBER_OF_GATES = 30
GATE_DURATION = 500  # ns

# Register indices
LOOP_COUNTER_REGISTER_INDEX = 0
RECOVERY_GATE_PLAYED_MARKER = 1
XORSHIFT_OUTPUT_REGISTER_INDEX = 61
WORKING_REGISTER_INDEX = 62
GATE_INDEX_REGISTER = 50

Note

generate_main_rb_script is a function that automatically created the Q1ASM code snippet that needs to be uploaded to the sequencer. It is implemented in the rb_utils.py module.

[6]:
seq_prog: SequenceProgram = generate_main_rb_script(
    SEED,
    NUMBER_OF_GATES,
    LOOP_COUNTER_REGISTER_INDEX,
    RECOVERY_GATE_PLAYED_MARKER,
    XORSHIFT_OUTPUT_REGISTER_INDEX,
    WORKING_REGISTER_INDEX,
    GATE_INDEX_REGISTER,
)

# start acquisition
seq_prog.insert_instruction(
    seq_prog.format_instructions(("acquire", "0,0,200", None)), label="start"
)
[7]:
sequence_of_gates = randomized_benchmarking_sequence(seed=SEED, number_of_gates=NUMBER_OF_GATES)
recovery_gate_index = sequence_of_gates[-1]

Implementation of the gates#

Here the gates are simply DC offsets that explores the full QRM output range.

[8]:
min_offset, max_offset = -32768, 32767

SINGLE_QUBIT_CLIFFORD_GROUP_ORDER = 24

for i in range(SINGLE_QUBIT_CLIFFORD_GROUP_ORDER):
    label = f"clifford_{i}"
    offset = int(
        min_offset + (max_offset - min_offset) * i / 23
    )  # mapping onto the range of the offset
    seq_prog.add_label(label)
    seq_prog.add_awg_instruction(values=(offset, offset), instruction_type="offset", label=label)
    seq_prog.add_update_parameters(duration=GATE_DURATION, label=label)
    seq_prog.add_jump(jump_to="loop", label=label)

Showing the final Q1 script#

[9]:
print("-" * 88)
print(seq_prog.seq_prog)
print("-" * 88)
----------------------------------------------------------------------------------------
init:
        wait_sync       4                   # wait for synchronization
        reset_ph                            # reset absolute phase and time to 0
        upd_param       4                   # update all parameters and wait 4ns
start:
        move            29,R0               # iterations counter
        move            2,R1                # recovery gate played marker
        move            789456123,R61       # initial seed value
        nop
        set_mrk         15                  # turn all markers channels ON
        upd_param       8
        set_mrk         0                   # turn all markers channels OFF
        upd_param       4
        acquire         0,0,200
main:
        jmp             @xorshift           # jump to label @xorshift
loop:
        loop            R0,@main            # if val(R0-1)>0: go to 'main' and continue from there again
        add             R0,1,R0             # to prevent R0 taking neg. values after playing recov. gate
recovery:
        loop            R1,@clifford_6      # R1=1 => play recov. gate -> goto @loop -> R1=0 => skip
stop:
        set_awg_offs    0,0                 # set AWG offset to 0
        upd_param       4                   # update all parameters and wait 4ns
        stop                                # stop the sequencer
xorshift:
        asl             R61,13,R62
        nop
        xor             R61,R62,R61
        nop
        asr             R61,17,R62
        nop
        xor             R61,R62,R61
        nop
        asl             R61,5,R62
        nop
        xor             R61,R62,R61
        nop
        asr             R61,29,R50          # isolating the first 3 bits
        nop
        asl             R50,2,R50           # making room for 00 01 and 10
        nop
        and             R61,536870911,R62   # isolating the last 29 bits
        nop
        jlt             R62,178956970,@shift # 00 case
        nop
        add             R50,1,R50
        nop
        jlt             R62,357913940,@shift # 01 case
        nop
        add             R50,1,R50
        nop
        jmp             @shift              # 10 case
shift:
        add             R50,@gatesLUT,R50   # add @gatesLUT to register R50
        nop
        jmp             R50                 # jump to register R50
gatesLUT:
        jmp             @clifford_0         # jump to label @clifford_0
        jmp             @clifford_1         # jump to label @clifford_1
        jmp             @clifford_2         # jump to label @clifford_2
        nop
        jmp             @clifford_3         # jump to label @clifford_3
        jmp             @clifford_4         # jump to label @clifford_4
        jmp             @clifford_5         # jump to label @clifford_5
        nop
        jmp             @clifford_6         # jump to label @clifford_6
        jmp             @clifford_7         # jump to label @clifford_7
        jmp             @clifford_8         # jump to label @clifford_8
        nop
        jmp             @clifford_9         # jump to label @clifford_9
        jmp             @clifford_10        # jump to label @clifford_10
        jmp             @clifford_11        # jump to label @clifford_11
        nop
        jmp             @clifford_12        # jump to label @clifford_12
        jmp             @clifford_13        # jump to label @clifford_13
        jmp             @clifford_14        # jump to label @clifford_14
        nop
        jmp             @clifford_15        # jump to label @clifford_15
        jmp             @clifford_16        # jump to label @clifford_16
        jmp             @clifford_17        # jump to label @clifford_17
        nop
        jmp             @clifford_18        # jump to label @clifford_18
        jmp             @clifford_19        # jump to label @clifford_19
        jmp             @clifford_20        # jump to label @clifford_20
        nop
        jmp             @clifford_21        # jump to label @clifford_21
        jmp             @clifford_22        # jump to label @clifford_22
        jmp             @clifford_23        # jump to label @clifford_23
        nop
clifford_0:
        set_awg_offs    -32768,-32768
        upd_param       500                 # updating batched parameters
        jmp             @loop               # jump to label @loop
clifford_1:
        set_awg_offs    -29918,-29918
        upd_param       500                 # updating batched parameters
        jmp             @loop               # jump to label @loop
clifford_2:
        set_awg_offs    -27069,-27069
        upd_param       500                 # updating batched parameters
        jmp             @loop               # jump to label @loop
clifford_3:
        set_awg_offs    -24219,-24219
        upd_param       500                 # updating batched parameters
        jmp             @loop               # jump to label @loop
clifford_4:
        set_awg_offs    -21370,-21370
        upd_param       500                 # updating batched parameters
        jmp             @loop               # jump to label @loop
clifford_5:
        set_awg_offs    -18521,-18521
        upd_param       500                 # updating batched parameters
        jmp             @loop               # jump to label @loop
clifford_6:
        set_awg_offs    -15671,-15671
        upd_param       500                 # updating batched parameters
        jmp             @loop               # jump to label @loop
clifford_7:
        set_awg_offs    -12822,-12822
        upd_param       500                 # updating batched parameters
        jmp             @loop               # jump to label @loop
clifford_8:
        set_awg_offs    -9973,-9973
        upd_param       500                 # updating batched parameters
        jmp             @loop               # jump to label @loop
clifford_9:
        set_awg_offs    -7123,-7123
        upd_param       500                 # updating batched parameters
        jmp             @loop               # jump to label @loop
clifford_10:
        set_awg_offs    -4274,-4274
        upd_param       500                 # updating batched parameters
        jmp             @loop               # jump to label @loop
clifford_11:
        set_awg_offs    -1425,-1425
        upd_param       500                 # updating batched parameters
        jmp             @loop               # jump to label @loop
clifford_12:
        set_awg_offs    1424,1424
        upd_param       500                 # updating batched parameters
        jmp             @loop               # jump to label @loop
clifford_13:
        set_awg_offs    4273,4273
        upd_param       500                 # updating batched parameters
        jmp             @loop               # jump to label @loop
clifford_14:
        set_awg_offs    7122,7122
        upd_param       500                 # updating batched parameters
        jmp             @loop               # jump to label @loop
clifford_15:
        set_awg_offs    9972,9972
        upd_param       500                 # updating batched parameters
        jmp             @loop               # jump to label @loop
clifford_16:
        set_awg_offs    12821,12821
        upd_param       500                 # updating batched parameters
        jmp             @loop               # jump to label @loop
clifford_17:
        set_awg_offs    15670,15670
        upd_param       500                 # updating batched parameters
        jmp             @loop               # jump to label @loop
clifford_18:
        set_awg_offs    18520,18520
        upd_param       500                 # updating batched parameters
        jmp             @loop               # jump to label @loop
clifford_19:
        set_awg_offs    21369,21369
        upd_param       500                 # updating batched parameters
        jmp             @loop               # jump to label @loop
clifford_20:
        set_awg_offs    24218,24218
        upd_param       500                 # updating batched parameters
        jmp             @loop               # jump to label @loop
clifford_21:
        set_awg_offs    27068,27068
        upd_param       500                 # updating batched parameters
        jmp             @loop               # jump to label @loop
clifford_22:
        set_awg_offs    29917,29917
        upd_param       500                 # updating batched parameters
        jmp             @loop               # jump to label @loop
clifford_23:
        set_awg_offs    32767,32767
        upd_param       500                 # updating batched parameters
        jmp             @loop               # jump to label @loop
----------------------------------------------------------------------------------------

Upload and run the sequence#

We run the script and acquire the signal on a QRM in loopback configuration (output O¹ connected to input I¹ and same thing for [O², I²]).

[10]:
# Acquisitions
acquisitions = {"single": {"num_bins": 1, "index": 0}}

# Sequence dict generation
sequence = {
    "waveforms": {},  # waveforms_dict,
    "weights": {},
    "acquisitions": acquisitions,
    "program": seq_prog.seq_prog,
}

# Reset cluster and check status
cluster.reset()
print(f"Cluster initial status: {cluster.get_system_status()}")

# Load the sequence
qrm_module.sequencer0.sequence(sequence)

# Synchronize the modules
qrm_module.sequencer0.sync_en(True)

# Connect the sequencer and arm it
qrm_module.disconnect_outputs()
qrm_module.disconnect_inputs()


qrm_module.sequencer0.connect_sequencer("io0_1")

qrm_module.scope_acq_trigger_mode_path0("sequencer")
qrm_module.scope_acq_trigger_mode_path1("sequencer")

qrm_module.arm_sequencer(0)

# Start the sequence
cluster.start_sequencer()

print(f"Cluster final status: {cluster.get_system_status()}\n")
print("Sequencer final status:")
print("-----------------------")
print(qrm_module.sequencer0.get_sequencer_status())
Cluster initial status: Status: OKAY, Flags: NONE, Slot flags: NONE
Cluster final status: Status: OKAY, Flags: NONE, Slot flags: NONE

Sequencer final status:
-----------------------
Status: WARNING, State: STOPPED, Info Flags: ACQ_SCOPE_DONE_PATH_0, ACQ_SCOPE_DONE_PATH_1, ACQ_BINNING_DONE, Warning Flags: OUTPUT_OVERFLOW, Error Flags: NONE, Log: []

Checking out the resulting data#

[11]:
# Wait for the acquisition to finish with a timeout period of one minute.
qrm_module.get_acquisition_status(0)

# Move acquisition data from temporary memory to acquisition list.
qrm_module.store_scope_acquisition(0, "single")

# Get acquisition list from instrument.
single_acq = qrm_module.get_acquisitions(0)

The input ADC of the QRM module needs to be calibrated. In the following we will use calibration constants that we measured manually. More information about how one can calibrate the ADC can be found in this documentation page.

[12]:
# INPUT0 CALIBRATION
I0_CALIBRATED_OFFSET = 0.013182311624099292
I0_CALIBRATED_MAX = 0.5377562325869412
I0_CALIBRATED_MIN = -0.5387479589128147

# INPUT1 CALIBRATION
I1_CALIBRATED_OFFSET = -0.009603063362542746
I1_CALIBRATED_MAX = 0.5303539378129579
I1_CALIBRATED_MIN = -0.5307611379076087

i0_data_rng = np.array(single_acq["single"]["acquisition"]["scope"]["path0"]["data"])
i0_data_rng -= I0_CALIBRATED_OFFSET

i1_data_rng = np.array(single_acq["single"]["acquisition"]["scope"]["path1"]["data"])
i1_data_rng -= I1_CALIBRATED_OFFSET

# remapping the data to the range [0, 23] range
i0_data = 23 * (i0_data_rng - I0_CALIBRATED_MIN) / (I0_CALIBRATED_MAX - I0_CALIBRATED_MIN)
i1_data = 23 * (i1_data_rng - I1_CALIBRATED_MIN) / (I1_CALIBRATED_MAX - I1_CALIBRATED_MIN)

# trimming and rounding
i0_data = i0_data.round().astype(int)
i1_data = i1_data.round().astype(int)

# Plot acquired signal on both inputs.
fig, ax = plt.subplots(1, 1)
ax.plot(i1_data)
ax.set_xlabel("Time (ns)")
ax.set_ylabel("Relative amplitude")
ax.set_yticks(range(24))
ax.grid(which="both")
plt.show()
../../../_images/applications_q1asm_randomized_benchmarking_q1_rng_for_rb_30_0.png
[13]:
# checking that the sequence is correct
print(sequence_of_gates)
[14, 1, 20, 4, 9, 20, 17, 1, 2, 6, 20, 5, 14, 10, 8, 16, 14, 7, 13, 13, 10, 3, 23, 11, 5, 19, 14, 7, 22, 6]
[14]:
# We check that the sequence of gates realizes the desired π/2 pulse
SQC.get_clifford_index_from_PLT(
    np.linalg.multi_dot([SQC(i).pauli_transfer_matrix for i in list(reversed(sequence_of_gates))])
)
[14]:
0