English
Languages
English
Japanese
Spanish

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]:
../_images/tutorials_user-transpiled-circuits_1_0.svg

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]:
../_images/tutorials_user-transpiled-circuits_11_0.svg

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 SoftwareVersion
qiskit-terra0.20.0
qiskit-aer0.9.1
qiskit-ignis0.7.0
qiskit-ibmq-provider0.18.3
qiskit-aqua0.9.5
qiskit0.34.0
qiskit-nature0.3.0
qiskit-finance0.2.1
qiskit-optimization0.2.3
qiskit-machine-learning0.2.1
System information
Python version3.9.10
Python compilerClang 13.0.0 (clang-1300.0.29.3)
Python buildmain, Jan 15 2022 11:48:00
OSDarwin
CPUs8
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.