Add supplementary information to transpiler module (#4134)

* start adding transpiler docs

* add more docs

* add gate optim

* tweak docs

* more tweaks

* requested updates

* fix missing file

* remove extra breaks

* update default initial layout selections

* fix line too long

Co-authored-by: Luciano Bello <luciano.bello@ibm.com>
Co-authored-by: Matthew Treinish <mtreinish@kortar.org>
This commit is contained in:
Paul Nation 2020-05-04 17:12:35 -04:00 committed by GitHub
parent c05182a101
commit 1bd1f7bb30
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 358 additions and 5 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 59 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 241 KiB

View File

@ -19,8 +19,361 @@ Transpiler (:mod:`qiskit.transpiler`)
.. currentmodule:: qiskit.transpiler
Pass Management
===============
Overview
========
Transpilation is the process of rewriting a given input circuit to match
the topoplogy of a specific quantum device, and/or to optimize the circuit
for execution on present day noisy quantum systems.
Most circuits must undergo a series of transformations that make them compatible with
a given target device, and optimize them to reduce the effects of noise on the
resulting outcomes. Rewriting quantum circuits to match hardware constraints and
optimizing for performance can be far from trivial. The flow of logic in the rewriting
tool chain need not be linear, and can often have iterative sub-loops, conditional
branches, and other complex behaviors. That being said, the basic building blocks
follow the structure given below:
.. image:: /source_images/transpiling_core_steps.png
.. raw:: html
<br>
Qiskit has four pre-built transpilation pipelines available here:
:mod:`qiskit.transpiler.preset_passmanagers`. Unless the reader is familiar with
quantum circuit optimization methods and their usage, it is best to use one of
these ready-made routines.
Supplementary Information
=========================
.. container:: toggle
.. container:: header
**Basis Gates**
When writing a quantum circuit you are free to use any quantum gate (unitary operator) that
you like, along with a collection of non-gate operations such as qubit measurements and
reset operations. However, when running a circuit on a real quantum device one no longer
has this flexibility. Due to limitations in, for example, the physical interactions
between qubits, difficulty in implementing multi-qubit gates, control electronics etc,
a quantum computing device can only natively support a handful of quantum gates and non-gate
operations. In the present case of IBM Q devices, the native gate set can be found by querying
the devices themselves, and looking for the corresponding attribute in their configuration:
.. jupyter-execute::
:hide-code:
:hide-output:
from qiskit.test.mock import FakeVigo
backend = FakeVigo()
.. jupyter-execute::
backend.configuration().basis_gates
Every quantum circuit run on an IBM Q device must be expressed using only these basis gates.
For example, suppose one wants to run a simple phase estimation circuit:
.. jupyter-execute::
import numpy as np
from qiskit import QuantumCircuit
qc = QuantumCircuit(2, 1)
qc.h(0)
qc.x(1)
qc.cu1(np.pi/4, 0, 1)
qc.h(0)
qc.measure([0], [0])
qc.draw(output='mpl')
We have :math:`H`, :math:`X`, and controlled-:math:`U_{1}` gates, all of which are
not in our devices basis gate set, and must be expanded. This expansion is taken
care of for us in the :func:`qiskit.execute` function. However, we can
decompose the circuit to show what it would look like in the native gate set of
the IBM Quantum devices:
.. jupyter-execute::
qc_basis = qc.decompose()
qc_basis.draw(output='mpl')
A few things to highlight. First, the circuit has gotten longer with respect to the
initial one. This can be verified by checking the depth of the circuits:
.. jupyter-execute::
print('Original depth:', qc.depth(), 'Decomposed Depth:', qc_basis.depth())
Second, although we had a single controlled gate, the fact that it was not in the basis
set means that, when expanded, it requires more than a single `cx` gate to implement.
All said, unrolling to the basis set of gates leads to an increase in the depth of a
quantum circuit and the number of gates.
It is important to highlight two special cases:
1. A SWAP gate is not a native gate on the IBM Q devices, and must be decomposed into
three CNOT gates:
.. jupyter-execute::
swap_circ = QuantumCircuit(2)
swap_circ.swap(0, 1)
swap_circ.decompose().draw(output='mpl')
As a product of three CNOT gates, SWAP gates are expensive operations to perform on a
noisy quantum devices. However, such operations are usually necessary for embedding a
circuit into the limited entangling gate connectivities of actual devices. Thus,
minimizing the number of SWAP gates in a circuit is a primary goal in the
transpilation process.
2. A Toffoli, or controlled-controlled-not gate (`ccx`), is a three-qubit gate. Given
that our basis gate set includes only single- and two-qubit gates, it is obvious that
this gate must be decomposed. This decomposition is quite costly:
.. jupyter-execute::
ccx_circ = QuantumCircuit(3)
ccx_circ.ccx(0, 1, 2)
ccx_circ.decompose().draw(output='mpl')
For every Toffoli gate in a quantum circuit, the IBM Quantum hardware may execute up to
six CNOT gates, and a handful of single-qubit gates. From this example, it should be
clear that any algorithm that makes use of multiple Toffoli gates will end up as a
circuit with large depth and will therefore be appreciably affected by noise and gate
errors.
.. raw:: html
<br>
.. container:: toggle
.. container:: header
**Initial Layout**
Quantum circuits are abstract entities whose qubits are "virtual" representations of actual
qubits used in computations. We need to be able to map these virtual qubits in a one-to-one
manner to the "physical" qubits in an actual quantum device.
.. image:: /source_images/mapping.png
.. raw:: html
<br><br>
By default, qiskit will do this mapping for you. The choice of mapping depends on the
properties of the circuit, the particular device you are targeting, and the optimization
level that is chosen. The basic mapping strategies are the following:
- **Trivial layout**: Map virtual qubits to the same numbered physical qubit on the device,
i.e. `[0,1,2,3,4]` -> `[0,1,2,3,4]` (default in `optimization_level=0` and
`optimization_level=1`).
- **Dense layout**: Find the sub-graph of the device with same number of qubits as the circuit
with the greatest connectivity (default in `optimization_level=2` and `optimization_level=3`).
The choice of initial layout is extremely important when:
1. Computing the number of SWAP operations needed to map the input circuit onto the device
topology.
2. Taking into account the noise properties of the device.
The choice of `initial_layout` can mean the difference between getting a result,
and getting nothing but noise.
Lets see what layouts are automatically picked at various optimization levels. The modified
circuits returned by :func:`qiskit.compiler.transpile` have this initial layout information
in them, and we can view this layout selection graphically using
:func:`qiskit.visualization.plot_circuit_layout`:
.. jupyter-execute::
from qiskit import QuantumCircuit, transpile
from qiskit.visualization import plot_circuit_layout
from qiskit.test.mock import FakeVigo
backend = FakeVigo()
ghz = QuantumCircuit(3, 3)
ghz.h(0)
ghz.cx(0,range(1,3))
ghz.barrier()
ghz.measure(range(3), range(3))
ghz.draw(output='mpl')
- **Layout Using Optimization Level 0**
.. jupyter-execute::
new_circ_lv0 = transpile(ghz, backend=backend, optimization_level=0)
plot_circuit_layout(new_circ_lv0, backend)
- **Layout Using Optimization Level 3**
.. jupyter-execute::
new_circ_lv3 = transpile(ghz, backend=backend, optimization_level=3)
plot_circuit_layout(new_circ_lv3, backend)
It is completely possible to specify your own initial layout. To do so we can
pass a list of integers to :func:`qiskit.compiler.transpile` via the `initial_layout`
keyword argument, where the index labels the virtual qubit in the circuit and the
corresponding value is the label for the physical qubit to map onto:
.. jupyter-execute::
# Virtual -> physical
# 0 -> 3
# 1 -> 4
# 2 -> 2
my_ghz = transpile(ghz, backend, initial_layout=[3, 4, 2])
plot_circuit_layout(my_ghz, backend)
.. raw:: html
<br>
.. container:: toggle
.. container:: header
**Mapping Circuits to Hardware Topology**
In order to implement a CNOT gate between qubits in a quantum circuit that are not directly
connected on a quantum device one or more SWAP gates must be inserted into the circuit to
move the qubit states around until they are adjacent on the device gate map. Each SWAP
gate is decomposed into three CNOT gates on the IBM Quantum devices, and represents an
expensive and noisy operation to perform. Thus, finding the minimum number of SWAP gates
needed to map a circuit onto a given device, is an important step (if not the most important)
in the whole execution process.
However, as with many important things in life, finding the optimal SWAP mapping is hard.
In fact it is in a class of problems called NP-Hard, and is thus prohibitively expensive
to compute for all but the smallest quantum devices and input circuits. To get around this,
by default Qiskit uses a stochastic heuristic algorithm called
:class:`Qiskit.transpiler.passes.StochasticSwap` to compute a good, but not necessarily minimal
SWAP count. The use of a stochastic method means the circuits generated by
:func:`Qiskit.compiler.transpile` (or :func:`Qiskit.execute` that calls `transpile` internally)
are not guaranteed to be the same over repeated runs. Indeed, running the same circuit
repeatedly will in general result in a distribution of circuit depths and gate counts at the
output.
In order to highlight this, we run a GHZ circuit 100 times, using a "bad" (disconnected)
`initial_layout`:
.. jupyter-execute::
import matplotlib.pyplot as plt
from qiskit import QuantumCircuit, transpile
from qiskit.test.mock import FakeBoeblingen
backend = FakeBoeblingen()
ghz = QuantumCircuit(5)
ghz.h(0)
ghz.cx(0,range(1,5))
ghz.draw(output='mpl')
.. jupyter-execute::
depths = []
for _ in range(100):
depths.append(transpile(ghz,
backend,
initial_layout=[7, 0, 4, 15, 19],
).depth())
plt.figure(figsize=(8, 6))
plt.hist(depths, bins=list(range(14,36)), align='left', color='#AC557C')
plt.xlabel('Depth', fontsize=14)
plt.ylabel('Counts', fontsize=14);
This distribution is quite wide, signaling the difficultly the SWAP mapper is having
in computing the best mapping. Most circuits will have a distribution of depths,
perhaps not as wide as this one, due to the stochastic nature of the default SWAP
mapper. Of course, we want the best circuit we can get, especially in cases where
the depth is critical to success or failure. In cases like this, it is best to
:func:`transpile` a circuit several times, e.g. 10, and take the one with the
lowest depth. The :func:`transpile` function will automatically run in parallel
mode, making this procedure relatively speedy in most cases.
.. raw:: html
<br>
.. container:: toggle
.. container:: header
**Gate Optimization**
Decomposing quantum circuits into the basis gate set of the IBM Quantum devices,
and the addition of SWAP gates needed to match hardware topology, conspire to
increase the depth and gate count of quantum circuits. Fortunately many routines
for optimizing circuits by combining or eliminating gates exist. In some cases
these methods are so effective the output circuits have lower depth than the inputs.
In other cases, not much can be done, and the computation may be difficult to
perform on noisy devices. Different gate optimizations are turned on with
different `optimization_level` values. Below we show the benefits gained from
setting the optimization level higher:
.. important::
The output from :func:`transpile` varies due to the stochastic swap mapper.
So the numbers below will likely change each time you run the code.
.. jupyter-execute::
import matplotlib.pyplot as plt
from qiskit import QuantumCircuit, transpile
from qiskit.test.mock import FakeBoeblingen
backend = FakeBoeblingen()
ghz = QuantumCircuit(5)
ghz.h(0)
ghz.cx(0,range(1,5))
ghz.draw(output='mpl')
.. jupyter-execute::
for kk in range(4):
circ = transpile(ghz, backend, optimization_level=kk)
print('Optimization Level {}'.format(kk))
print('Depth:', circ.depth())
print('Gate counts:', circ.count_ops())
print()
.. raw:: html
<br>
Transpiler API
==============
Pass Manager Construction
-------------------------
.. autosummary::
:toctree: ../stubs/
@ -31,7 +384,7 @@ Pass Management
FlowController
Layout and Topology
===================
-------------------
.. autosummary::
:toctree: ../stubs/
@ -40,7 +393,7 @@ Layout and Topology
CouplingMap
Fenced Objects
==============
--------------
.. autosummary::
:toctree: ../stubs/
@ -49,7 +402,7 @@ Fenced Objects
FencedPropertySet
Exceptions
==========
----------
.. autosummary::
:toctree: ../stubs/