Note
This page was generated from docs/tutorials/user-transpiled-circuits.ipynb.
Submitting user-transpiled circuits using primitives¶
To get the best performance from your circuits, the Qiskit Runtime service will pass all circuits through Qiskit’s transpiler before running them. While this is usually a good thing, we might sometimes want to disable this by passing the argument skip_transpilation=True
to the primitive we’re using.
For example, we may know better than the transpiler in some cases, or want to target a specific subset of qubits on a specific device. In this tutorial, we’ll disable automatic transpilation to test the performance of different transpiler settings. This example will take you through the full process of creating, transpiling, and submitting circuits.
Transpiling circuits for IBM Quantum devices¶
In the following code cell, we create a small circuit that our transpiler will try to optimize. In this example, we create a circuit that carries out Grover’s algorithm, with an oracle that marks the state 111
. We then simulate the ideal distribution (what we’d expect to measure if we ran this on a perfect quantum computer, an infinite number of times) for comparison later.
[1]:
# Create circuit to test transpiler on
from qiskit import QuantumCircuit
from qiskit.circuit.library import GroverOperator, Diagonal
oracle = Diagonal([1] * 7 + [-1])
qc = QuantumCircuit(3)
qc.h([0, 1, 2])
qc = qc.compose(GroverOperator(oracle))
# Use Statevector object to calculate the ideal output
from qiskit.quantum_info import Statevector
ideal_distribution = Statevector.from_instruction(qc).probabilities_dict()
from qiskit.visualization import plot_histogram
plot_histogram(ideal_distribution)
[1]:
Next, we need to choose a backend to transpile for. In the following cell, we create a service instance, which we’ll use to start a session, and get the backend object, which contains information for the transpiler. Since the transpilation process depends on the device, we’ll ask the runtime service for a specific device by name. In this example, we’ll use ibm_algiers
, which is only available through IBM Cloud.
[2]:
from qiskit_ibm_runtime import QiskitRuntimeService
service = QiskitRuntimeService()
backend = service.backend("ibm_algiers")
Next, we transpile the circuits for our backend. We’re going to compare the performance of the transpiler with optimization_level
set to 0
(lowest) against 3
(highest). The lowest optimization level just does the bare minimum needed to get the circuit running on the device; it maps the circuit qubits to the device qubits, and adds swaps gates to allow all 2-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.
In the following cell, we transpile qc
for both values of optimization_level
, print the number of CNOT gates, and add the transpiled circuits to a list. Some of the transpiler’s algorithms are randomized, so we set a seed for reproducibility.
[3]:
# Need to add measurements to the circuit
qc.measure_all()
from qiskit import transpile
circuits = []
for optimization_level in [0, 3]:
t_qc = transpile(qc, backend, optimization_level=optimization_level, seed_transpiler=0)
print(f"CNOTs (optimization_level={optimization_level}): ", t_qc.count_ops()["cx"])
circuits.append(t_qc)
CNOTs (optimization_level=0): 27
CNOTs (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 we can improve performance is through dynamic decoupling, where we apply a sequence of gates to idling qubits. This cancels out some unwanted interactions with the environment. In the following cell, we add dynamic decoupling to the circuit transpiled with optimization_level=3
, and add it to our list.
[4]:
from qiskit.transpiler import PassManager, InstructionDurations
from qiskit.transpiler.passes import ASAPSchedule, DynamicalDecoupling
from qiskit.circuit.library import XGate
# Get gate durations so the transpiler knows how long each operation takes
durations = InstructionDurations.from_backend(backend)
# This is the sequence we'll apply to idling qubits
dd_sequence = [XGate(), XGate()]
# Run scheduling and dynamic decoupling passes on circuit
pm = PassManager([ASAPSchedule(durations), DynamicalDecoupling(durations, dd_sequence)])
circ_dd = pm.run(circuits[1])
# Add this new circuit to our list
circuits.append(circ_dd)
Run user-transpiled circuits using Qiskit Runtime¶
At this point, we have a list of circuits (named circuits
) transpiled for ibm_algiers
. In the following cell, we create an instance of the sampler primitive, and start a session using the context manager (with ...:
), which automatically opens and closes the session for us. This is where we pass the skip_transpilation=True
argument.
Within the context manager, we sample the circuits and store the results to result
.
[5]:
from qiskit_ibm_runtime import Sampler, Session
with Session(service=service, backend=backend):
sampler = Sampler()
job = sampler.run(
circuits=circuits, # sample all three circuits
skip_transpilation=True,
shots=8000,
)
result = job.result()
Finally, we can 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 dynamic decoupling we applied.
[6]:
from qiskit.visualization import plot_histogram
binary_prob = [quasi_dist.binary_probabilities() for quasi_dist in result.quasi_dists]
plot_histogram(
binary_prob + [ideal_distribution],
bar_labels=False,
legend=[
"optimization_level=0",
"optimization_level=3",
"optimization_level=3 + dd",
"ideal distribution",
],
)
[6]:
We 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).
[7]:
from qiskit.quantum_info import hellinger_fidelity
for counts in result.quasi_dists:
print(f"{hellinger_fidelity(counts.binary_probabilities(), ideal_distribution):.3f}")
0.927
0.938
0.951
[8]:
import qiskit_ibm_runtime
qiskit_ibm_runtime.version.get_version_info()
[8]:
'0.4.0'
[9]:
from qiskit.tools.jupyter import *
%qiskit_version_table
%qiskit_copyright
Version Information
Qiskit Software | Version |
---|---|
qiskit-terra | 0.20.0 |
qiskit-aer | 0.9.1 |
qiskit-ignis | 0.7.0 |
qiskit-ibmq-provider | 0.18.3 |
qiskit-aqua | 0.9.5 |
qiskit | 0.34.0 |
qiskit-nature | 0.3.0 |
qiskit-finance | 0.2.1 |
qiskit-optimization | 0.2.3 |
qiskit-machine-learning | 0.2.1 |
System information | |
Python version | 3.9.10 |
Python compiler | Clang 13.0.0 (clang-1300.0.29.3) |
Python build | main, Jan 15 2022 11:48:00 |
OS | Darwin |
CPUs | 8 |
Memory (Gb) | 32.0 |
Wed Apr 13 19:59:49 2022 BST |
This code is a part of Qiskit
© Copyright IBM 2017, 2022.
This code is licensed under the Apache License, Version 2.0. You may
obtain a copy of this license in the LICENSE.txt file in the root directory
of this source tree or at http://www.apache.org/licenses/LICENSE-2.0.
Any modifications or derivative works of this code must retain this
copyright notice, and modified files need to carry a notice indicating
that they have been altered from the originals.