Compare transpiler settings
The code on this page was developed using the following requirements. We recommend using these versions or newer.
qiskit[all]~=2.3.1 qiskit-ibm-runtime~=0.45.1
Different transpiler settings provide different types of optimization to the circuit, often at the expense of longer classical processing time. This guide walks through the full process of creating, transpiling, and submitting circuits to demonstrate testing the performance of various settings.
Note that the same setting could improve the results of one circuit while hindering another. Be sure to inspect the resulting transpiled circuits before running on actual hardware.
Set up and create sample circuit
# Create circuit to test transpiler on
from qiskit import QuantumCircuit
from qiskit.transpiler.preset_passmanagers import generate_preset_pass_manager
from qiskit.circuit.library import grover_operator, DiagonalGate
# Use Statevector object to calculate the ideal output
from qiskit.quantum_info import Statevector
from qiskit.visualization import plot_histogram
from qiskit.transpiler import PassManager
from qiskit.circuit.library import XGate
from qiskit.quantum_info import hellinger_fidelityCreate a small circuit for the transpiler to try to optimize. This example creates a circuit that carries out Grover's algorithm with an oracle that marks the state 111. Next, simulate the ideal distribution (what you'd expect to measure if you ran this on a perfect quantum computer an infinite number of times) for comparison later.
oracle = DiagonalGate([1] * 7 + [-1])
qc = QuantumCircuit(3)
qc.h([0, 1, 2])
qc = qc.compose(grover_operator(oracle))
qc.draw(output="mpl", style="iqp")Output:
ideal_distribution = Statevector.from_instruction(qc).probabilities_dict()
plot_histogram(ideal_distribution)Output:
Transpile
Next, transpile the circuits for the QPU. You will compare the performance of the transpiler with optimization_level set to 0 (lowest) against 3 (highest). The lowest optimization level does the bare minimum needed to get the circuit running on the device; it maps the circuit qubits to the device qubits and adds swap gates to allow all two-qubit operations. The highest optimization level is much smarter and uses lots of tricks to reduce the overall gate count. Since multi-qubit gates have high error rates and qubits decohere over time, the shorter circuits should give better results.
This example uses IBM Quantum® hardware, but you can try it on any Qiskit-compatible QPU. Your results might be different.
The following cell transpiles qc for both values of optimization_level, prints the number of two-qubit gates, and adds the transpiled circuits to a list. Some of the transpiler's algorithms are randomized, so it sets a seed for reproducibility.
# Use Qiskit Runtime to run jobs on hardware
from qiskit_ibm_runtime import (
QiskitRuntimeService,
SamplerV2 as Sampler,
)# Select the backend with the fewest number of jobs in the queue
service = QiskitRuntimeService()
backend = service.least_busy(
operational=True, simulator=False, min_num_qubits=127
)
backend.nameOutput:
'ibm_fez'
# Need to add measurements to the circuit
qc.measure_all()
# Find the correct two-qubit gate
twoQ_gates = set(["ecr", "cz", "cx"])
for gate in backend.basis_gates:
if gate in twoQ_gates:
twoQ_gate = gate
circuits = []
for optimization_level in [0, 3]:
pm = generate_preset_pass_manager(
optimization_level, backend=backend, seed_transpiler=0
)
t_qc = pm.run(qc)
print(
f"Two-qubit gates (optimization_level={optimization_level}): ",
t_qc.count_ops()[twoQ_gate],
)
circuits.append(t_qc)Output:
Two-qubit gates (optimization_level=0): 21
Two-qubit gates (optimization_level=3): 12
Since CNOTs usually have a high error rate, the circuit transpiled with optimization_level=3 should perform much better.
Another way you can improve performance is through dynamical decoupling, by applying a sequence of gates to idling qubits. This cancels out some unwanted interactions with the environment. The following cell adds dynamic decoupling to the circuit transpiled with optimization_level=3 and adds it to the list.
from qiskit_ibm_runtime.transpiler.passes.scheduling import (
ASAPScheduleAnalysis,
PadDynamicalDecoupling,
)
# Get gate durations so the transpiler knows how long each operation takes
durations = backend.target.durations()
# This is the sequence we'll apply to idling qubits
dd_sequence = [XGate(), XGate()]
# Run scheduling and dynamic decoupling passes on circuit
pm = PassManager(
[
ASAPScheduleAnalysis(durations),
PadDynamicalDecoupling(durations, dd_sequence),
]
)
circ_dd = pm.run(circuits[1])
# Add this new circuit to our list
circuits.append(circ_dd)circ_dd.draw(output="mpl", style="iqp", idle_wires=False)Output:
Execute the circuit
At this point, you have a list of circuits transpiled with different settings. Next, run these circuits using the Sampler primitive and store the results to result.
sampler = Sampler(backend)
job = sampler.run(
[(circuit) for circuit in circuits], # sample all three circuits
shots=8000,
)
result = job.result()View results
Finally, plot the results from the device runs against the ideal distribution. You can see the results with optimization_level=3 are closer to the ideal distribution due to the lower gate count, and optimization_level=3 + dd is even closer due to the dynamical decoupling.
binary_prob = [
{
k: v / res.data.meas.num_shots
for k, v in res.data.meas.get_counts().items()
}
for res in result
]
plot_histogram(
binary_prob + [ideal_distribution],
bar_labels=False,
legend=[
"optimization_level=0",
"optimization_level=3",
"optimization_level=3 + dd",
"ideal distribution",
],
)Output:
You can confirm this by computing the Hellinger fidelity between each set of results and the ideal distribution (higher is better, and 1 is perfect fidelity).
for prob in binary_prob:
print(f"{hellinger_fidelity(prob, ideal_distribution):.3f}")Output:
0.858
0.988
0.992
Next steps
-
Explore some advanced transpilation resources, such as:
-
Browse the available tutorials.