Compiling and running a quantum program

The latest version of this notebook is available on https://github.com/QISKit/qiskit-tutorial.


Contributors

Andrew Cross and Jay Gambetta

The qubits in the QX devices are arranged in a plane and connected to their neighbors. Because each qubit is not connected to all the others, some circuits cannot execute without rewriting them to use the available interactions. A standard way to do this is to insert "swap" gates, which exchange the states of pairs of qubits, to move distant qubits near one another. QISKit includes methods to do this for you.

Circuit rewriting occurs in QISKit whenever you specify a "coupling map", but by default your circuits are not changed. The coupling map is a Python dictionary whose keys are qubits that can be used as controls, and whose values are lists of possible targets for CNOT gates. In other words, the coupling map represents the qubit layout as an adjacency list for a directed graph.

The compile() method of QuantumProgram currently applies a fixed sequence of passes:

  • swap_mapper: uses a greedy randomized algorithm to find a swap circuit for each layer of the input circuit
  • direction_mapper: changes the direction of CNOT gates as needed
  • cx_cancellation: simplifies adjacent pairs of CNOT gates
  • optimize_1q_gates: replaces sequences of single-qubit gates by their compositions

Here is an example of this process showing the tools we have provided; we then give a worked example using the quantum Fourier transform (QFT).


In [1]:
# Import the QuantumProgram and our configuration
import math
from pprint import pprint

from qiskit import QuantumProgram
import Qconfig

Lets start by first making two circuits:

  • a GHZ state on four qubits
  • a superposition on two qubits

In [2]:
qp = QuantumProgram()
# quantum register for the first circuit
q1 = qp.create_quantum_register('q1', 4)
c1 = qp.create_classical_register('c1', 4)
# quantum register for the second circuit
q2 = qp.create_quantum_register('q2', 2)
c2 = qp.create_classical_register('c2', 2)
# making the first circuits
qc1 = qp.create_circuit('GHZ', [q1], [c1])
qc2 = qp.create_circuit('superposition', [q2], [c2])
qc1.h(q1[0])
qc1.cx(q1[0], q1[1])
qc1.cx(q1[1], q1[2])
qc1.cx(q1[2], q1[3])
for i in range(4):
    qc1.measure(q1[i], c1[i])
# making the second circuits
qc2.h(q2)
for i in range(2):
    qc2.measure(q2[i], c2[i])
# printing the circuits
print(qp.get_qasm('GHZ'))
print(qp.get_qasm('superposition'))


OPENQASM 2.0;
include "qelib1.inc";
qreg q1[4];
creg c1[4];
h q1[0];
cx q1[0],q1[1];
cx q1[1],q1[2];
cx q1[2],q1[3];
measure q1[0] -> c1[0];
measure q1[1] -> c1[1];
measure q1[2] -> c1[2];
measure q1[3] -> c1[3];

OPENQASM 2.0;
include "qelib1.inc";
qreg q2[2];
creg c2[2];
h q2[0];
h q2[1];
measure q2[0] -> c2[0];
measure q2[1] -> c2[1];

The above shows the OpenQASM for both circuits. These can be converted to qobj to run on local simulator backend.


In [3]:
qobj = qp.compile(['GHZ','superposition'], backend='local_qasm_simulator')
qp.get_execution_list(qobj)


Out[3]:
['GHZ', 'superposition']

If you want more information about the circuits to be run, you can set verbose=True


In [4]:
qp.get_execution_list(qobj, verbose=True)


id: aLdZa6gCSeVI3nqXHe4sacjuWGdZDV
backend: local_qasm_simulator
qobj config:
 max_credits: 3
 shots: 1024
  circuit name: GHZ
  circuit config:
   coupling_map: None
   layout: None
   basis_gates: u1,u2,u3,cx,id
   seed: 272820342348853763191034715157325328767
  circuit name: superposition
  circuit config:
   coupling_map: None
   layout: None
   basis_gates: u1,u2,u3,cx,id
   seed: 272820342348853763191034715157325328767
Out[4]:
['GHZ', 'superposition']

To get the configuration of a circuit, use

get_compiled_configuration(qobj, 'circuit')


In [5]:
qp.get_compiled_configuration(qobj, 'GHZ', )


Out[5]:
{'basis_gates': 'u1,u2,u3,cx,id',
 'coupling_map': None,
 'layout': None,
 'seed': 272820342348853763191034715157325328767}

To get the compiled qasm, use

get_compiled_qasm(qobj,'circuit')


In [6]:
print(qp.get_compiled_qasm(qobj, 'GHZ'))


OPENQASM 2.0;
include "qelib1.inc";
qreg q1[4];
creg c1[4];
u2(0.0,3.141592653589793) q1[0];
cx q1[0],q1[1];
cx q1[1],q1[2];
cx q1[2],q1[3];
measure q1[2] -> c1[2];
measure q1[3] -> c1[3];
measure q1[1] -> c1[1];
measure q1[0] -> c1[0];

If we need to change the cx gates so that they work on a device with a restricted coupling graph, we can use the coupling map in the compile command. Here we assume that the device only supports two-qubit gates, with qubit 0 being the control.


In [7]:
# Coupling map 
coupling_map = {0: [1, 2, 3]}
# Place the qubits on a triangle in the bow-tie
initial_layout={("q1", 0): ("q", 0), ("q1", 1): ("q", 1), ("q1", 2): ("q", 2), ("q1", 3): ("q", 3)}

In [8]:
qobj = qp.compile(['GHZ'], backend='local_qasm_simulator', coupling_map=coupling_map, initial_layout=initial_layout)
print(qp.get_compiled_qasm(qobj,'GHZ'))


OPENQASM 2.0;
include "qelib1.inc";
qreg q[4];
creg c1[4];
u2(0.0,3.141592653589793) q[1];
u1(6.283185307179586) q[0];
cx q[0],q[1];
u2(0.0,3.141592653589793) q[1];
u2(0.0,3.141592653589793) q[0];
cx q[0],q[1];
cx q[0],q[2];
cx q[0],q[3];
u2(0.0,3.141592653589793) q[0];
u2(0.0,3.141592653589793) q[3];
cx q[0],q[3];
u2(0.0,3.141592653589793) q[3];
u2(0.0,3.141592653589793) q[0];
cx q[0],q[3];
measure q[3] -> c1[1];
u2(0.0,3.141592653589793) q[0];
u2(0.0,3.141592653589793) q[2];
cx q[0],q[2];
u2(0.0,3.141592653589793) q[2];
measure q[2] -> c1[2];
u2(0.0,3.141592653589793) q[0];
measure q[0] -> c1[3];
measure q[1] -> c1[0];

The above circuit, which used three cx gates originally, has a total of five now.

QFT

Here we provide another example, which is the Quantum Fourier transform. These can be loaded directly by using

import qiskit.tools.qi as qi


In [9]:
# Define methods for making QFT circuits
def input_state(circ, q, n):
    """n-qubit input state for QFT that produces output 1."""
    for j in range(n):
        circ.h(q[j])
        circ.u1(math.pi/float(2**(j)), q[j]).inverse()


def qft(circ, q, n):
    """n-qubit QFT on q in circ."""
    for j in range(n):
        for k in range(j):
            circ.cu1(math.pi/float(2**(j-k)), q[j], q[k])
        circ.h(q[j])

Start by creating a quantum circuit on three qubits that prepares an input state, does the QFT, and measures each qubit. The input state is chosen so that the ideal measurement outcome after the QFT is "001". The OpenQASM output is expressed in terms of Hadamard (h), u1(theta):=diag(1,$e^{i\theta}$), and controlled-u1 (cu1) gates.


In [10]:
qp = QuantumProgram()
q = qp.create_quantum_register("q", 3)
c = qp.create_classical_register("c", 3)
qft3 = qp.create_circuit("qft3", [q], [c])
input_state(qft3, q, 3)
qft(qft3, q, 3)
for i in range(3):
    qft3.measure(q[i], c[i])
print(qft3.qasm())


OPENQASM 2.0;
include "qelib1.inc";
qreg q[3];
creg c[3];
h q[0];
u1(-3.141592653589793) q[0];
h q[1];
u1(-1.570796326794897) q[1];
h q[2];
u1(-0.785398163397448) q[2];
h q[0];
cu1(1.570796326794897) q[1],q[0];
h q[1];
cu1(0.785398163397448) q[2],q[0];
cu1(1.570796326794897) q[2],q[1];
h q[2];
measure q[0] -> c[0];
measure q[1] -> c[1];
measure q[2] -> c[2];

If we execute this circuit on the local simulator, we indeed see that the outcome is always "001".


In [11]:
result = qp.execute(["qft3"], backend="local_qasm_simulator", shots=1024)
result.get_counts("qft3")


Out[11]:
{'001': 1024}

After calling execute, we can request the "compiled" OpenQASM that was sent to the local simulator. The default behavior is that the circuit is not changed. Looking at the output below, you can see that each gate is expanded according to its definition into gates u1, u2, u3, and cx. There are no further simplifications. For example, the first three gates on q[2] could be combined into a single gate, but they are not.


In [12]:
print(result.get_ran_qasm("qft3"))


OPENQASM 2.0;
include "qelib1.inc";
qreg q[3];
creg c[3];
u2(0.0,3.141592653589793) q[2];
u1(-0.785398163397448) q[2];
u1(0.392699081698724) q[2];
u2(0.0,3.141592653589793) q[1];
u1(-1.570796326794897) q[1];
u1(0.7853981633974485) q[1];
u2(0.0,3.141592653589793) q[0];
u1(-3.141592653589793) q[0];
u2(0.0,3.141592653589793) q[0];
cx q[1],q[0];
u1(-0.7853981633974485) q[0];
cx q[1],q[0];
u1(0.7853981633974485) q[0];
cx q[2],q[0];
u1(-0.392699081698724) q[0];
cx q[2],q[0];
u1(0.392699081698724) q[0];
measure q[0] -> c[0];
u1(0.7853981633974485) q[2];
u2(0.0,3.141592653589793) q[1];
cx q[2],q[1];
u1(-0.7853981633974485) q[1];
cx q[2],q[1];
u1(0.7853981633974485) q[1];
measure q[1] -> c[1];
u2(0.0,3.141592653589793) q[2];
measure q[2] -> c[2];

Now we will allow QISKit to rewrite the circuit for us. The ibmqx2 backend has subsets of three fully connected qubits. We will get the best results if we use one of these, since there won't be any need to swap.

To get QISKit to rewrite the circuit in this way, we need to provide the "coupling map" and an initial layout. The coupling map below has entries such as "0: [1, 2]". This means that it is valid to apply a CNOT gate from q[0] to q[1], and from q[0] to q[2] (where q[0] is the control qubit). The initial layout has entries like "("q", 0): ("q", 2)", which means that we should place q[0] from our input circuit at qubit q[2] on the device. Our choice places the qubits of the QFT circuit onto one of the triangles in the coupling graph.

QISKit will only attempt to rewrite the circuit if coupling_map is not None. The initial_layout is always optional. If one is not given, QISKit will lay out the qubits somewhat arbitrarily, and attempt to adjust the layout so the first layer of gates does not require swapping. Note that the mapper will currently fail and raise an exception if the graph induced by the layout is not connected.

We will run on the local simulator for convenience, but you can change the backend to "ibmqx2" to select the real device.


In [13]:
# Coupling map for ibmqx2 "bowtie"
coupling_map = {0: [1, 2],
                1: [2],
                2: [],
                3: [2, 4],
                4: [2]}
# Place the qubits on a triangle in the bow-tie
initial_layout={("q", 0): ("q", 2), ("q", 1): ("q", 3), ("q", 2): ("q", 4)}
result2 = qp.execute(["qft3"], backend="local_qasm_simulator", coupling_map=coupling_map, initial_layout=initial_layout)
result2.get_counts("qft3")


pre-mapping properties: {'size': 27, 'depth': 16, 'width': 3, 'bits': 3, 'factors': 1, 'operations': {'u2': 6, 'u1': 12, 'cx': 6, 'measure': 3}}
initial layout: {('q', 0): ('q', 2), ('q', 1): ('q', 3), ('q', 2): ('q', 4)}
final layout: {('q', 0): ('q', 2), ('q', 1): ('q', 3), ('q', 2): ('q', 4)}
post-mapping properties: {'size': 22, 'depth': 14, 'width': 5, 'bits': 3, 'factors': 3, 'operations': {'u2': 4, 'u3': 2, 'cx': 6, 'u1': 7, 'measure': 3}}
running on backend: local_qasm_simulator
Out[13]:
{'001': 1024}

We can see that the chosen layout is the layout we requested. The number of CNOT gates was unchanged, but several single-qubit gates were eliminated. We can confirm this by looking at the "compiled" OpenQASM. Notice that the "cx q[2], q[1];" gate was mapped to "cx q[3], q[4];" instead of "cx q[4], q[3];" because the latter is not in the coupling map. Hadamard gates were inserted to exchange the control and target, and the resulting single-qubit gates were simplified.


In [14]:
print(result2.get_ran_qasm("qft3"))


OPENQASM 2.0;
include "qelib1.inc";
qreg q[5];
creg c[3];
u2(-0.392699081698724,3.141592653589793) q[4];
u2(-0.7853981633974485,3.141592653589793) q[3];
u3(3.141592653589793,1.5707963267948966,4.71238898038469) q[2];
cx q[3],q[2];
u1(-0.7853981633974485) q[2];
cx q[3],q[2];
u1(6.283185307179586) q[3];
u1(0.7853981633974485) q[2];
cx q[4],q[2];
u1(-0.392699081698724) q[2];
cx q[4],q[2];
u2(0.0,3.9269908169872414) q[4];
cx q[3],q[4];
u1(6.283185307179586) q[4];
u3(0.7853981633974485,1.5707963267948966,4.71238898038469) q[3];
cx q[3],q[4];
u1(6.283185307179586) q[4];
measure q[4] -> c[2];
u2(0.7853981633974485,3.141592653589793) q[3];
measure q[3] -> c[1];
u1(0.392699081698724) q[2];
measure q[2] -> c[0];

Finally, let's lay out the qubits onto a segment of the ibmqx3 16-qubit device.


In [15]:
# Place the qubits on a linear segment of the ibmqx3
coupling_map = {0: [1], 1: [2], 2: [3], 3: [14], 4: [3, 5], 6: [7, 11], 7: [10], 8: [7], 9: [8, 10], 11: [10], 12: [5, 11, 13], 13: [4, 14], 15: [0, 14]}
initial_layout={("q", 0): ("q", 0), ("q", 1): ("q", 1), ("q", 2): ("q", 2)}
result3 = qp.execute(["qft3"], backend="local_qasm_simulator", coupling_map=coupling_map, initial_layout=initial_layout)
result3.get_counts("qft3")


Out[15]:
{'001': 1024}

Because the qubits are now on a line, a swap gate is needed to interact the qubits at the endpoints of the line. As you can see, the number of cx gates increases, as does the circuit depth. We can look at the "compiled" OpenQASM to see the additional swap.


In [16]:
print(result3.get_ran_qasm("qft3"))


OPENQASM 2.0;
include "qelib1.inc";
qreg q[3];
creg c[3];
u2(-0.392699081698724,3.141592653589793) q[2];
u3(0.7853981633974485,1.5707963267948966,4.71238898038469) q[1];
u2(3.141592653589793,3.141592653589793) q[0];
cx q[0],q[1];
u1(6.283185307179586) q[1];
u3(0.7853981633974485,1.5707963267948966,4.71238898038469) q[0];
cx q[0],q[1];
u1(6.283185307179586) q[1];
cx q[1],q[2];
u2(0.0,3.141592653589793) q[1];
u2(0.0,3.141592653589793) q[2];
cx q[1],q[2];
u2(0.0,3.141592653589793) q[2];
u2(0.0,3.141592653589793) q[1];
cx q[1],q[2];
u2(0.0,3.141592653589793) q[1];
u3(-0.7853981633974485,1.5707963267948966,4.71238898038469) q[0];
cx q[0],q[1];
u1(6.283185307179586) q[1];
u3(0.392699081698724,1.5707963267948966,4.71238898038469) q[0];
cx q[0],q[1];
u2(0.7853981633974485,3.141592653589793) q[1];
cx q[1],q[2];
u1(-0.7853981633974485) q[2];
cx q[1],q[2];
u2(0.0,3.141592653589793) q[1];
measure q[1] -> c[2];
u1(0.7853981633974485) q[2];
measure q[2] -> c[1];
u2(0.392699081698724,3.141592653589793) q[0];
measure q[0] -> c[0];


In [ ]: