Improving the accuracy of BQSKit compiled circuits with error mitigation#
In this tutorial we describe how to use error mitigation capabilities from Mitiq together with the compilation capabilities of BQSKit [66], a compiler for quantum circuits. BQSKit stands for Berkeley Quantum Synthesis Toolkit and it allows one “to compile quantum programs to efficient physical circuits for any QPU”.
To get started, ensure you have the requisite python packages by running the following install commands.
pip install mitiq
pip install 'bqskit[ext]'
The main goal of this tutorial is to understand how to use bqskit
together with mitiq
.
To do this, we will
generate a random circuit,
compile it with
bqskit
,use error mitigation on the compiled circuit, and
compare the results obtained with and without error mitigation.
After demonstrating the use of the two packages, we can then try and understand how circuit compilation with BQSKit in general interacts with error mitigation by running the process many times and comparing results.
To begin we import many of the required modules and functions.
import bqskit
from bqskit.ext import cirq_to_bqskit, bqskit_to_cirq
import mitiq
import cirq
import numpy as np
from cirq.contrib.qasm_import import circuit_from_qasm
Random circuit generation#
We use cirq
’s random_circuit
function to generate a random circuit with specified qubit number, depth and density (which refers to the probability of an operation occurring at a given moment).
Here we also use a random seed for reproducibility.
The random circuit is then converted to BQSKit’s custom internal representation with the cirq_to_bqskit
function.
num_qubits = 3
depth = 10
density = 1
RANDOM_SEED = 479
random_circuit = cirq.testing.random_circuit(
num_qubits, depth, density, random_state=RANDOM_SEED
)
bqskit_circuit = cirq_to_bqskit(random_circuit)
print(random_circuit)
┌──┐
0: ───X───Z───Y───X───────────────iSwap─────@────H───Y───
│ │ │
1: ───S───X───────@───Y───iSwap───┼────────S┼────X───X───
│ │ │ │
2: ───────@───S───Y───Y───iSwap───iSwap─────@────────────
└──┘
Compilation#
With the circuit initialized we can compile it with BQSKit. The default optimization of this compiler is to reduce circuit depth, and in doing the compilation assumes an all-to-all connected hardware with \(\mathsf{CNOT}\) and \(\mathsf{U3}\) as native gates.
Note
If you are compiling a circuit to run on a specific QPU, you can use the compile
functions model
argument to pass a connectivity graph, and native gateset.
This allows for one to skip the second pass of compilation that is usually required when first optimizing, and then compiling to hardware.
compiled = bqskit.compile(bqskit_circuit)
compiled_circuit = bqskit_to_cirq(compiled)
print(compiled_circuit)
q_0: ───cirq.circuits.qasm_output.QasmUGate(theta=1.9119968682602453, phi=1.0978830500657786, lmda=1.0340700262611344)───cirq.circuits.qasm_output.QasmUGate(theta=0.3063571310302597, phi=1.475420752150956, lmda=0.5863152051131191)────cirq.circuits.qasm_output.QasmUGate(theta=0.013851655419571346, phi=1.2075880489584256, lmda=0.540254755704615)────cirq.circuits.qasm_output.QasmUGate(theta=0.8084734011322992, phi=0.6585820205161826, lmda=0.7712544800621027)────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────@───cirq.circuits.qasm_output.QasmUGate(theta=0.658123347943945, phi=1.5000000000169926, lmda=0.5709555097272883)────X───cirq.circuits.qasm_output.QasmUGate(theta=1.7690720057823057, phi=0.2449275730310065, lmda=0.20934987623796056)─────
│ │
q_1: ───cirq.circuits.qasm_output.QasmUGate(theta=0.5181946087692286, phi=0.5775165422976121, lmda=0.6960237616154153)───cirq.circuits.qasm_output.QasmUGate(theta=0.7190629881194404, phi=0.7832783057624402, lmda=1.4619955634329496)───cirq.circuits.qasm_output.QasmUGate(theta=0.26116420828874676, phi=0.14846915601729194, lmda=0.8807701502520614)───cirq.circuits.qasm_output.QasmUGate(theta=0.28083113067698995, phi=0.18977646599202672, lmda=1.7441676341524222)───cirq.circuits.qasm_output.QasmUGate(theta=1.3724608598960355, phi=1.8534281067769593, lmda=1.923711395032124)───@───cirq.circuits.qasm_output.QasmUGate(theta=0.2120424793018449, phi=0.49999999999197164, lmda=1.7468923100429332)───X───cirq.circuits.qasm_output.QasmUGate(theta=1.2120424793143196, phi=1.9244985752937713, lmda=0.49999999999075373)──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┼────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┼───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
│ │ │ │
q_2: ───cirq.circuits.qasm_output.QasmUGate(theta=0.9021245026501249, phi=1.5561907144007883, lmda=1.5826928866326504)───cirq.circuits.qasm_output.QasmUGate(theta=1.71323459238013, phi=0.5117508065747632, lmda=0.44570666136373016)────cirq.circuits.qasm_output.QasmUGate(theta=0.6859295391534941, phi=1.6846269174344786, lmda=0.5094468712739245)─────cirq.circuits.qasm_output.QasmUGate(theta=0.7185186459735022, phi=0.5916085882479954, lmda=1.8766010740327412)─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────X───cirq.circuits.qasm_output.QasmUGate(theta=0.294814458414254, phi=0.5133164669087029, lmda=1.5000000000312026)─────@───cirq.circuits.qasm_output.QasmUGate(theta=0.1260905540859037, phi=0.2409261957206884, lmda=1.7261263619887472)────cirq.circuits.qasm_output.QasmUGate(theta=0.8562178518050191, phi=1.8081453308506161, lmda=0.46160905456407153)───cirq.circuits.qasm_output.QasmUGate(theta=1.0498423876784082, phi=1.9475741650071439, lmda=1.7165486393638982)───X───cirq.circuits.qasm_output.QasmUGate(theta=1.9999999999919549, phi=1.1924148562396755, lmda=0.3355042456313759)───@───cirq.circuits.qasm_output.QasmUGate(theta=0.8946921266567446, phi=5.590110596642717e-10, lmda=1.9720808986743175)───
We now have two cirq
circuits: random_circuit
and compiled_circuit
.
Both represent the same (or very close to the same) unitary operation, but with different gatesets, and with different structure.
Now we mitigate them!
Error Mitigation#
Using mitiq
’s simplest, and easiest to use method of Zero-Noise Extrapolation (ZNE) we can obtain more accurate results than we would otherwise.
Note
There are multiple other techniques described in our Users Guide which could be used as well.
In this tutorial we assume a simple error model of depolarizing noise on two-qubit gates.
To use this method, we need to define a function (in mitiq
this is often referred to as an executor) which takes as input a circuit, and returns some sort of expectation value, or probability.
We define a function execute
which adds a tunable noise parameter, which controls the strength of the simulated noise.
This function performs a density matrix simulation along with measuring the probability of observing the system in the ground state.
Warning
This error model is not entirely realistic. Two-qubit gates are generally much noisier than single qubit gates, but real quantum hardware is often afflicted with many other types of errors that this model will not account for.
def execute(circuit, noise_level=0.05):
noisy_circuit = cirq.Circuit()
for op in circuit.all_operations():
noisy_circuit.append(op)
if len(op.qubits) == 2:
noisy_circuit.append(
cirq.depolarize(p=noise_level, n_qubits=2)(*op.qubits)
)
rho = (
cirq.DensityMatrixSimulator()
.simulate(noisy_circuit)
.final_density_matrix
)
return rho[0, 0].real
Since we’d like to see how compilation effects error mitigation, we first simulate the ideal and noisy values using the simulator defined above.
uncompiled_ideal_value = execute(random_circuit, noise_level=0.0)
uncompiled_noisy_value = execute(random_circuit)
compiled_ideal_value = execute(compiled_circuit, noise_level=0.0)
compiled_noisy_value = execute(compiled_circuit)
With these values taken, we are now ready to use ZNE — on both the random, and compiled circuit — to obtain mitigated expectation values.
from mitiq import zne
uncompiled_mitigated_result = zne.execute_with_zne(random_circuit, execute)
compiled_mitigated_result = zne.execute_with_zne(compiled_circuit, execute)
Thus we have four variables which we can compare against ideal values to see how performance varies for this circuit across compilation and mitigation.
compiled |
mitigated |
|
---|---|---|
|
❌ |
❌ |
|
❌ |
✅ |
|
✅ |
❌ |
|
✅ |
✅ |
Comparison#
These data are then summarized in the following table printed below.
header = "{:<11} {:<15} {:<10}"
entry = "{:<11} {:<15.2f} {:<10.2f}"
int_entry = "{:<11} {:<15} {:<10}"
print(header.format("", "uncompiled", "compiled"))
print(entry.format("ideal", uncompiled_ideal_value, compiled_ideal_value))
print(entry.format("noisy", uncompiled_noisy_value, compiled_noisy_value))
print(
entry.format(
"mitigated", uncompiled_mitigated_result, compiled_mitigated_result
)
)
print(
entry.format(
"error",
abs(uncompiled_ideal_value - uncompiled_mitigated_result),
abs(compiled_ideal_value - compiled_mitigated_result),
)
)
print(
int_entry.format(
"depth",
len(random_circuit),
len(compiled_circuit),
)
)
uncompiled compiled
ideal 0.50 0.50
noisy 0.43 0.44
mitigated 0.56 0.53
error 0.06 0.03
depth 10 15
Hence for this particular random circuit we see that using both compilation and error mitigation combine for the most accurate result. Note that despite using BQSKit to compile the circuit, the depth has actually increased. This can occasionally happen when the random circuit contains gates that are harder to compile into BQSKit’s default gateset.
More random circuits#
We can now repeat the above procedure with many random circuits to get a better understanding of how these two technologies interact in a more general setting. To do this we execute the above code many times, each iteration using a new random circuit on 4 qubits with depth 40. Because compiling many large circuits is computationally expensive, we leave the code our from this notebook, but it can be accessed in our research repository.
Once the errors are computed for each circuit we can collect the results in a histogram to get an idea of how compilation and mitigation affects accuracy more generally.
These results show that using error mitigation improves the accuracy of both uncompiled, and compiled circuits. The tutorial in the research repository shows further that error mitigation both reduces the mean, and standard deviation of these distributions.
In this tutorial we’ve seen how one can use error mitigation in conjunction with circuit compilation. For more information check out the BQSKit and Mitiq documentation.
References#
BQSKit documentation: https://bqskit.readthedocs.io/
BQSKit whitepaper: https://doi.org/10.1145/3503222.3507739 [66]
Mitiq documentation: https://mitiq.readthedocs.io/
Mitiq whitepaper: https://quantum-journal.org/papers/q-2022-08-11-774/