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\).
[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()

[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