Note

# 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 code cell below, 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 cell below, 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, i.e. map the circuit qubits to the device qubits, and add 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 cell below, 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 cell below, 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 cell below, 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


### 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