This example illustrates how to generate readout calibration (RCAL) circuits and use them to correct readout errors in other circuits.

The make_rcal() method produces a set of circuits that prepare both the ground state (through application of identity gates) and excited states (through the application of bitflip or $X$ gates in the case of qubits, or their multi-dimensional equivalent in the case of qudits). The relative frequency with which the ground and excited states are measured are used to estimate state-dependent readout errors.

In this example we use the built-in Simulator to execute the RCAL circuits, but the same procedure can be followed for circuits that are run on hardware.

The code below generates RCAL circuits for two qubits (labelled 0 and 1), populates their results using a simulator with readout error, and displays the resulting state probabilities in the form of confusion matrices.

[2]:

import trueq as tq

# generate RCAL circuits to measure the readout errors on qubits [0, 1]
circuits = tq.make_rcal([0, 1])

# display the circuits
circuits[0]

[2]:

True-Q formatting will not be loaded without trusting this notebook or rerunning the affected cells. Notebooks can be marked as trusted by clicking "File -> Trust Notebook".
 Circuit Key: compiled_pauli: XX protocol: RCAL Marker 0 Compilation tools may only recompile cycles with equal markers. (0): Gate.x Name: Gate.x Aliases: Gate.x Gate.cliff1 Generators: 'X': 180.0 Matrix: 1.00 1.00 (1): Gate.x Name: Gate.x Aliases: Gate.x Gate.cliff1 Generators: 'X': 180.0 Matrix: 1.00 1.00 1 Marker 1 Compilation tools may only recompile cycles with equal markers. (0): Meas() Name: Meas() (1): Meas() Name: Meas()
[3]:

circuits[1]

[3]:

True-Q formatting will not be loaded without trusting this notebook or rerunning the affected cells. Notebooks can be marked as trusted by clicking "File -> Trust Notebook".
 Circuit Key: compiled_pauli: II protocol: RCAL Marker 0 Compilation tools may only recompile cycles with equal markers. (0): Gate.id Name: Gate.id Aliases: Gate.id Gate.i Gate.cliff0 Likeness: Identity Generators: 'I': 0 Matrix: 1.00 1.00 (1): Gate.id Name: Gate.id Aliases: Gate.id Gate.i Gate.cliff0 Likeness: Identity Generators: 'I': 0 Matrix: 1.00 1.00 1 Marker 1 Compilation tools may only recompile cycles with equal markers. (0): Meas() Name: Meas() (1): Meas() Name: Meas()

Next, we initialize a simulator with a 20% readout error on both qubits:

[4]:

sim = tq.Simulator().add_readout_error(0.2)

# run the circuits on the simulator to populate their results
sim.run(circuits, n_shots=1000)


We can analyze and fit the results by calling the fit() method that produces the following output:

[5]:

circuits.fit()

[5]:

True-Q formatting will not be loaded without trusting this notebook or rerunning the affected cells. Notebooks can be marked as trusted by clicking "File -> Trust Notebook".
 RCAL Readout Calibration Confusion (0,) P(0 | 0) = 0.808 P(1 | 1) = 0.791 Confusion Matrix: 0.808 0.209 0.192 0.791 Key: labels: (0, 1) protocol: RCAL (1,) P(0 | 0) = 0.817 P(1 | 1) = 0.817 Confusion Matrix: 0.817 0.183 0.183 0.817 Key: labels: (0, 1) protocol: RCAL

Here, $P(A|B)$ are conditional probabilities that denote the probability that we are measuring state $A$ when we expect to have state $B$ prepared. When you hover over each row in the table above it displays the confusion matrices of the outcomes, that is, the probabilities $P(0|0), P(0|1), P(1|0)$ and $P(1|1)$ arranged in matrix form. Given that we simulated these circuits with a 20% readout error, we observe a more or less 80/20 split in probabilities on the diagonal and the off-diagonal elements.

The make_rcal() function can be used for qudits in exactly the same way as for qubits. The qudit dimension determines how many circuits are needed to estimate the error on all states, because there are more computational basis states for higher-dimensional systems and therefore more conditional probabilities to measure.

In this example we consider qutrits, that is qudits of dimension three, by setting the set_dim() parameter to 3. Higher-dimensional qudits can be analyzed in the same fashion.

For a qutrit, make_rcal() generates three circuits that prepare the ground state on each qudit by applying identity gates, the first excited state by applying $X$ gates, and the second excited state by applying $X^2$ gates to each qudit.

As before, we consider a system of two qutrits (labelled 0 and 1) and start by generating those circuits:

[6]:

tq.settings.set_dim(3)

circuits = tq.make_rcal([0, 1])

# display the circuits
circuits[0]

[6]:

True-Q formatting will not be loaded without trusting this notebook or rerunning the affected cells. Notebooks can be marked as trusted by clicking "File -> Trust Notebook".
 Circuit Key: compiled_pauli: W20W20 protocol: RCAL Marker 0 Compilation tools may only recompile cycles with equal markers. (0): Gate.x3pow2 Name: Gate.x3pow2 Aliases: Gate.x3pow2 Matrix: 1.00 1.00 1.00 (1): Gate.x3pow2 Name: Gate.x3pow2 Aliases: Gate.x3pow2 Matrix: 1.00 1.00 1.00 1 Marker 1 Compilation tools may only recompile cycles with equal markers. (0): Meas() Name: Meas() (1): Meas() Name: Meas()
[7]:

circuits[1]

[7]:

True-Q formatting will not be loaded without trusting this notebook or rerunning the affected cells. Notebooks can be marked as trusted by clicking "File -> Trust Notebook".
 Circuit Key: compiled_pauli: W10W10 protocol: RCAL Marker 0 Compilation tools may only recompile cycles with equal markers. (0): Gate.x3 Name: Gate.x3 Aliases: Gate.x3 Gate.x3pow1 Matrix: 1.00 1.00 1.00 (1): Gate.x3 Name: Gate.x3 Aliases: Gate.x3 Gate.x3pow1 Matrix: 1.00 1.00 1.00 1 Marker 1 Compilation tools may only recompile cycles with equal markers. (0): Meas() Name: Meas() (1): Meas() Name: Meas()
[8]:

circuits[2]

[8]:

True-Q formatting will not be loaded without trusting this notebook or rerunning the affected cells. Notebooks can be marked as trusted by clicking "File -> Trust Notebook".
 Circuit Key: compiled_pauli: W00W00 protocol: RCAL Marker 0 Compilation tools may only recompile cycles with equal markers. (0): Gate.id3 Name: Gate.id3 Aliases: Gate.id3 Matrix: 1.00 1.00 1.00 (1): Gate.id3 Name: Gate.id3 Aliases: Gate.id3 Matrix: 1.00 1.00 1.00 1 Marker 1 Compilation tools may only recompile cycles with equal markers. (0): Meas() Name: Meas() (1): Meas() Name: Meas()

Next, we simulate the outcome of these circuits with a 20% readout error on both qutrits and fit the results:

[9]:

sim = tq.Simulator().add_readout_error(0.2)
sim.run(circuits, n_shots=1000)

estimate = circuits.fit()
estimate

[9]:

True-Q formatting will not be loaded without trusting this notebook or rerunning the affected cells. Notebooks can be marked as trusted by clicking "File -> Trust Notebook".
 RCAL Readout Calibration Confusion (0,) P(0 | 0) = 0.807 P(1 | 1) = 0.793 P(2 | 2) = 0.809 Confusion Matrix: 0.807 0.106 0.083 0.098 0.793 0.108 0.095 0.101 0.809 Key: labels: (0, 1) protocol: RCAL (1,) P(0 | 0) = 0.787 P(1 | 1) = 0.791 P(2 | 2) = 0.807 Confusion Matrix: 0.787 0.109 0.106 0.115 0.791 0.087 0.098 0.100 0.807 Key: labels: (0, 1) protocol: RCAL

And just as in the qubit case, when hovering over this table you can again inspect the confusion matrices that now have a dimension of $3\times3$ and contain all the conditional probabilities $P(0|0), P(0|1), P(0|2), P(1|0) \dots P(2|2)$.

## Applying the Correction

The estimate returned by the RCAL protocol upon calling the fit() method is an RCalEstimate instance that can be used to apply a correction to a different set of results that were obtained under the same readout error. For this, we can call the apply_correction() method that inverts the confusion matrices and multiplies them onto the corresponding indices of the results that we wish to correct.

For example, consider the following two-qutrit circuit with a Fourier or $F$ gate (the generalized version of the Hadamard gate for higher qudit dimensions) on the first qutrit followed by a $CX$ gate on both:

[10]:

circuit = tq.Circuit([{0: tq.Gate.f3}, {(0, 1): tq.Gate.cx3}])
circuit.measure_all()

circuit.draw()

[10]:


In the absence of readout errors, this circuit should produce an equal superposition between the the states 00, 11 and 20. However, when we simulate this circuit with the noisy simulator from above, the actual results differ. But using the RCalEstimate we obtained from the calibration, we can correct those results using the apply_correction() method:

[11]:

results = sim.sample(circuit, n_shots=1000)

tq.visualization.plot_results(
results,
estimate[0].apply_correction(results),
)


Note that the linear transformation used to correct the readout results can yield negative-valued outputs in the histogram, which can be interpreted as zero outputs.

## Automatic Correction

The correction we applied manually in the example above can also be automated. If you are using a CircuitCollection that also contains RCAL circuits, then that information will be used automatically to apply readout correction to your circuits when you call fit() or plot().

The code below illustrates this for SRB (Streamlined Randomized Benchmarking) circuits executed on a noisy simulator with readout error. It also shows how you can view the results with and without readout correction being applied. For simplicity, we switch our focus back to qubits but the example below works for qudits as well.

[12]:

tq.settings.set_dim(2)

# generate RCAL circuits to measure the readout errors on three qubits [0, 1, 2]
circuits = tq.make_rcal([0, 1, 2])

# generate SRB circuits to simultaneously characterize a single qubit [0] and
# a pair of qubits [1, 2] with 30 circuits for each random cycle in [4, 32, 64]
circuits.append(tq.make_srb([[0], [1, 2]], [4, 32, 64], 30))

# initialize a noisy simulator with a 10% readout error

# RCAL generally needs more shots than the other protocols because it is estimating
# an absolute value rather than a decay over randomizations, thus in this simulation
# we use different numbers of shots for RCAL and SRB to populate their results
sim.run(circuits.subset(protocol="RCAL"), n_shots=1000)

# to simulate the SRB circuits, we create a subset that contains all circuits except
# those from RCAL, which we can achieve by using square brackets in our syntax:
sim.run(circuits.subset(protocol=["RCAL"]), n_shots=50)

# plot the exponential decay with readout correction,
# where each expectation value (dot) has been compensated
circuits.plot.raw()


To avoid performing readout correction during analysis, we can remove all RCAL circuits from the collection before calling plot(). Notice that the y-intercept is now lower than in the plot above:

plot the exponential decay without readout correction

[13]:

circuits.subset(protocol="SRB").plot.raw()