This tutorial demonstrates how to generate Clifford randomized benchmarking (RB) circuits using pygsti
. We will focus on (holistic) Clifford RB for an arbitrary number of qubits. The tutorial uses an in-built Clifford compilation algorithm that works for an arbitrary number of qubits, but does not permit the user to specify their own Clifford compilation. We follow the current standard RB protocol first defined by Magesan et al. in "Scalable and Robust Benchmarking of Quantum Processes". Other RB variants exist, however, and there are often good reasons to use them, particularly for multi-qubit benchmarking. One of these alternatives, "Direct RB", is detailed in its own tutorial (17 Direct Randomized Benchmarking).
Please note that this tutorial does not demonstrate how to analyze Clifford RB data within pyGSTi
. Because the same RB analysis proceedure is applicable to many RB protocols, it appears in a separate tutorial (18 Randomized Benchmarking Data Analysis).
from __future__ import print_function #python 2 & 3 compatibility
import pygsti
from pygsti.extras import rb
To generate Clifford RB circuits, we first specify the two-qubit gate connectivity. The compiled circuits will respect this connectivity, and contain only gates in the "native" gate-set of the device. We do this using a ProcessorSpec
object: see the previous tutorial (15 Multi-Qubit Devices and Quantum Circuits) on how to create these. Here we'll demonstrate creating Clifford RB circuits for a device with:
Below, we generate the ProcessorSpec
for this device:
nQubits = 5
qubit_labels = ['Q0','Q1','Q2','Q3','Q4']
gate_names = ['Gi', 'Gxpi2', 'Gxmpi2', 'Gypi2', 'Gympi2', 'Gcphase']
availability = {'Gcphase':[('Q0','Q1'), ('Q1','Q2'), ('Q2','Q3'),
('Q3','Q4'),('Q4','Q0')]}
pspec = pygsti.obj.ProcessorSpec(nQubits, gate_names, availability=availability,
qubit_labels=qubit_labels)
We can generate a set of Clifford RB circuits using the rb.sample.clifford_rb_experiment()
function.
To sample a Clifford RB experiment, it is necessary to specify:
A "Clifford RB length" ($m$) is, within an additive constant, the length of the uncompiled Clifford circuit (the number of random $n$-qubit Clifford gates in the circuit). There are two possible conventions for the additive constant.
pyGSTi
is that $m$ corresponds to the number of Clifford gates in the uncompiled circuit minus 2. This choice is motivated by the original Clifford RB protocol, which demands that the circuits being used have at least two gates (a circuit consisting only of state prep and measurement is not normally permitted in Clifford RB). Under this convention, $m$ can be any integer $\geq 0$.We use this second convention consistently across all RB circuit generating methods within pyGSTi
. Data analysis, however, is largely indepenent of which convention is used for the circuit generation. Recall that RB data is fit to the function $P_m = A + Bp^m$. Rescaling the length, $m$, by an additive constant will only change the value of $B$ in the optimal fit (scaling it by factors of $p$). Importantly, the optimal fit of $p$, which fixes the error rate, remains unchanged.
In our example, we'll fix the Clifford RB lengths to $m \in \{0, 1, 2, 4, 8, 16\}$ and take the number of circuits at each length to be $k = 10$.
lengths = [0,1,2,4,8,16]
k = 10
The RB samplers in pyGSTi
allow the user to restrict the benchmarking sequences to address a subset of the qubits by specifying a subsetQs
list. This then means that a ProcessorSpec
can be specified for an entire device even if you only wish to benchmark some subset of it. If this list is not specified then the RB circuits returned will cover all qubits, providing a set of experiments capable of holistically benchmarking the entire device.
Obviously, the set of qubits specified must be connected by the two-qubit gates. If this is not the case, it is not possible to implement $n$-qubit Clifford gates over these $n$ qubits.
Let's demonstrate generating circuits to benchmark 3 of the qubits:
subsetQs = ['Q0','Q1','Q2']
Another important optional parameter is randomizeout
. This specifies whether the perfect output of the circuits should be the input state (assumed here to be the all-zero state) or a random computational basis state. The standard Clifford RB procedure is a perfect identity sequence, corresponding to the default of randomizeout
being False
. While there are many good reasons to instead set this to True (such as identifying biased measurement noise or leakage), here we'll stick to the standard procedure and set it to False
.
randomizeout = False
We are now ready to generate the RB experiment:
exp_dict = rb.sample.clifford_rb_experiment(pspec, lengths, k, subsetQs=subsetQs, randomizeout=randomizeout)
- Sampling 10 circuits at CRB length 0 (1 of 6 lengths) - Number of circuits sampled = 1,2,3,4,5,6,7,8,9,10, - Sampling 10 circuits at CRB length 1 (2 of 6 lengths) - Number of circuits sampled = 1,2,3,4,5,6,7,8,9,10, - Sampling 10 circuits at CRB length 2 (3 of 6 lengths) - Number of circuits sampled = 1,2,3,4,5,6,7,8,9,10, - Sampling 10 circuits at CRB length 4 (4 of 6 lengths) - Number of circuits sampled = 1,2,3,4,5,6,7,8,9,10, - Sampling 10 circuits at CRB length 8 (5 of 6 lengths) - Number of circuits sampled = 1,2,3,4,5,6,7,8,9,10, - Sampling 10 circuits at CRB length 16 (6 of 6 lengths) - Number of circuits sampled = 1,2,3,4,5,6,7,8,9,10,
And that's it!
The returned dictionary contains a full specification for the RB circuits to implement on the device defined by pspec
. This dictionary contains 4 keys:
print(exp_dict.keys())
dict_keys(['spec', 'qubitordering', 'circuits', 'idealout'])
Of particular note here is the 'circuits'
key, which indexes another dictionary for which the keys are tuples ($m$,$i$) where $m$ is the RB length and $i = 0, 1, \dots, k$ corresponds to the $i^{\rm th}$ circuit at length $m$.
print(exp_dict['circuits'].keys())
dict_keys([(0, 0), (0, 1), (0, 2), (0, 3), (0, 4), (0, 5), (0, 6), (0, 7), (0, 8), (0, 9), (1, 0), (1, 1), (1, 2), (1, 3), (1, 4), (1, 5), (1, 6), (1, 7), (1, 8), (1, 9), (2, 0), (2, 1), (2, 2), (2, 3), (2, 4), (2, 5), (2, 6), (2, 7), (2, 8), (2, 9), (4, 0), (4, 1), (4, 2), (4, 3), (4, 4), (4, 5), (4, 6), (4, 7), (4, 8), (4, 9), (8, 0), (8, 1), (8, 2), (8, 3), (8, 4), (8, 5), (8, 6), (8, 7), (8, 8), (8, 9), (16, 0), (16, 1), (16, 2), (16, 3), (16, 4), (16, 5), (16, 6), (16, 7), (16, 8), (16, 9)])
Let's look at one of the circuits, which is a pyGSTi Circuit
object. We'll look at the first circuit sampled at Clifford RB length 0, which consists of two $3$-qubit Clifford gates (one uniformly random, the other it's inverse) compiled into the native gates:
print("Here we show first circuit sampled at Clifford RB length $m=0$ (consisting of two uncompiled Clifford gates):")
print("")
gate_string = exp_dict['circuits'][0,0].__str__()
gate_string = gate_string.split('\n')
for bar in range(int(len(gate_string[0])/80)+1):
for ind in range(len(gate_string)):
print(gate_string[ind][80*bar:80*(bar+1)])
print("The circuit size is: ", exp_dict['circuits'][0,0].size())
print("The circuit depth is: ", exp_dict['circuits'][0,0].depth())
print("The circuit multi-qubit-gate count is: ", exp_dict['circuits'][0,0].multiQgate_count())
Here we show first circuit sampled at Clifford RB length $m=0$ (consisting of two uncompiled Clifford gates): Qubit Q0 ---|Gympi2|-| Gi |-| Gi |-| ●Q1 |-|Gympi2|-|Gympi2|-|Gympi2|-|Gymp Qubit Q1 ---|Gypi2 |-|Gxmpi2|-|Gympi2|-| ●Q0 |-|Gympi2|-|Gxpi2 |-|Gympi2|-|Gymp Qubit Q2 ---|Gxmpi2|-|Gxmpi2|-|Gxmpi2|-|Gympi2|-| Gi |-| Gi |-| Gi |-| Gi i2|-|Gi |-| Gi |-| Gi |-| Gi |-| Gi |-|●Q1|-|Gympi2|-|Gympi2|-|Gxmpi2|-| i2|-|●Q2|-|Gympi2|-|Gympi2|-|Gxpi2|-|Gympi2|-|●Q0|-|Gympi2|-|Gxpi2 |-|Gympi2|-|G |-|●Q1|-|Gympi2|-|Gxpi2 |-|Gxpi2|-|Gympi2|-|Gi |-| Gi |-| Gi |-| Gi |-| Gi |-|Gi |-| Gi |-| Gi |-| Gi |-| Gi |-|Gi |-| Gi |-| Gi |-| Gi ympi2|-|●Q2|-|Gympi2|-|Gympi2|-|Gxpi2 |-|Gympi2|-|●Q2|-|Gympi2|-|Gxpi2 |-|Gypi2 Gi |-|●Q1|-|Gympi2|-|Gxmpi2|-|Gympi2|-|Gympi2|-|●Q1|-|Gympi2|-|Gympi2|-|Gxmpi2 |-| Gi |-| Gi |-| Gi |-|Gxmpi2|-|Gxmpi2|-|Gxmpi2|-|Gxmpi2|-|Gxmpi2|-|Gympi2| |-|Gypi2 |-| Gi |-| Gi |-|Gympi2|-| Gi |-| Gi |-| ●Q2 |-|Gympi2|-|Gympi2| |-|Gxmpi2|-|Gypi2|-|Gypi2|-|Gypi2 |-|Gxmpi2|-|Gympi2|-| ●Q1 |-|Gympi2|-|Gxpi2 | -|Gympi2|-| Gi |-| Gi |-| Gi |-|●Q1|-|Gympi2|-|Gympi2|-|Gympi2|-|Gympi2|-| -|Gxmpi2|-|Gxmpi2|-|Gxmpi2|-|Gympi2|-|●Q0|-|Gympi2|-|Gxpi2 |-|Gympi2|-|Gympi2|-| -|Gypi2 |-|Gxmpi2|-|Gympi2|-| Gi |-|Gi |-| Gi |-| Gi |-| Gi |-| Gi |-| Gi |-| Gi |-| Gi |-| Gi |-| Gi |-|●Q1|-|Gympi2|-|Gxmpi2|-|Gxmpi2|-| Gi ●Q2|-|Gympi2|-|Gympi2|-|Gxpi2|-|Gympi2|-|●Q0|-|Gympi2|-|Gxpi2 |-|Gympi2|-|Gympi2 ●Q1|-|Gympi2|-|Gxpi2 |-|Gxpi2|-|Gympi2|-|Gi |-| Gi |-| Gi |-| Gi |-| Gi |-|Gi |-| Gi |-| Gi |-| Gi |-| Gi |-| Gi |-| Gi |--- |-|●Q2|-|Gympi2|-|Gympi2|-|Gxmpi2|-|Gxmpi2|-|Gxmpi2|-|Gxmpi2|--- |-|●Q1|-|Gympi2|-|Gxmpi2|-| Gi |-| Gi |-| Gi |-| Gi |--- The circuit size is: 186 The circuit depth is: 62 The circuit multi-qubit-gate count is: 10
Each of these circuits can be converted to OpenQasm or Quil using the methods shown in the tutorial introducing the Circuit
object. The set of circuits can also be saved to file, using the GateString
methods. To do this, we first turn the circuits
dictionary into a list
.
Note that when analyzing the data from an RB experiment, it is important to be able to assign to each gate sequence a Clifford RB length, $m$. It is not strictly possible to extract this given only the compiled circuit, so we are careful to order the saved sequences so that we can easily deduce their associated the RB lengths. Below, we order the resulting list so that the first k
sequences have length $m=0$, the second k
sequences have length $m=1$, etc.
circuitlist = [exp_dict['circuits'][m,i] for m in lengths for i in range(k)]
We can then use the GateString
export function:
pygsti.io.write_gatestring_list("tutorial_files/CliffordRBCircuits.txt",circuitlist,
"Clifford RB circuits")
exp_dict['spec']
{'lengths': [0, 1, 2, 4, 8, 16], 'circuits_per_length': 10, 'subsetQs': ['Q0', 'Q1', 'Q2'], 'randomizeout': False, 'citerations': 20, 'compilerargs': [], 'descriptor': 'A Clifford RB experiment'}
This stores all the information necessary to sample new circuits in the same way (and to know how the circuits were generated), when combined with the ProcessorSpec
used (which is not stored, as this can be a fairly large object).
However there is one warning here: the compilerargs
specifies what Clifford compiler to use, and when left as an empty list it will use the default option. This will be set to whatever we consider to be the best general-purpose Clifford compiler in pyGSTi
(there are multiple algorithms), and so this may change in future version of pyGSTi
. This is important as the Clifford RB error rate is very strongly dependent on the compilation used (as it is defined as an error rate per Clifford gate). Note that we can avoid compilation-dependence complications by instead implementing "Direct RB" - see the next tutorial.
This RB-specification dictionary highlights one other aspects of the Clifford sampling function which may sometimes be important: citerations
is the number of iterations used in our randomized Clifford compilers. Increasing this will often reduce the size and two-qubit-gate count in each compiled Clifford, at the cost of slowing down the circuit sampling. Sometimes it may be useful to increase this value, particularly if you want to implement Clifford RB on a number of qubits that is at the edge of feasability on your device (due to the native gate error rates) as in this case reducing the "cost" of each Clifford gate will be critical.
Another component of the output dictionary is the ideal outputs of the circuits, which is a dictionary with the same keys as with 'circuits':
exp_dict['idealout'].keys()
dict_keys([(0, 0), (0, 1), (0, 2), (0, 3), (0, 4), (0, 5), (0, 6), (0, 7), (0, 8), (0, 9), (1, 0), (1, 1), (1, 2), (1, 3), (1, 4), (1, 5), (1, 6), (1, 7), (1, 8), (1, 9), (2, 0), (2, 1), (2, 2), (2, 3), (2, 4), (2, 5), (2, 6), (2, 7), (2, 8), (2, 9), (4, 0), (4, 1), (4, 2), (4, 3), (4, 4), (4, 5), (4, 6), (4, 7), (4, 8), (4, 9), (8, 0), (8, 1), (8, 2), (8, 3), (8, 4), (8, 5), (8, 6), (8, 7), (8, 8), (8, 9), (16, 0), (16, 1), (16, 2), (16, 3), (16, 4), (16, 5), (16, 6), (16, 7), (16, 8), (16, 9)])
The values are the error-free outcomes of the circuits. Here, because we have left randomizeout
as False, this is always the bit-string of 3 zeros:
exp_dict['idealout'][0,0]
(0, 0, 0)
But if randomizeout
is True, these will be random bit-strings. It will not be possible to analyze the results of the RB experiments without these bit-strings: because "success" corresponds to the circuit output being the particular bit-string stored here. (These bit-strings can always be re-calculated from the circuits, but doing this with pyGSTi
requires accessing functions that are currently not demonstrated in any tutorial).
Note that the 'idealout'
bitstring assumes the input is ideally $0,0,0,\dots$. So if you use an input state other than $0,0,0,\dots$ it will be necessary to correct for this. E.g., if you start in the computational basis state $1,0,0,\dots$ you would need to add this bit-string modulo 2 to the 'idealout'
bit-string.
The final element in the output dictionary is 'qubitordering':
exp_dict['qubitordering']
('Q0', 'Q1', 'Q2')
This is just a tuple that shows which qubit correpsonds to which bit in the 'idealout'
bit-tuples. Note that this can also be extracted from the circuit objects and the RB 'spec'
, but it's useful information to have easily available.