# Import the QuantumProgram and our configuration
import math
from pprint import pprint

from qiskit import QuantumProgram
import Qconfig

"""
Explanation: <img src="../../images/qiskit-heading.gif" alt="Note: In order for images to show up in this jupyter notebook you need to select File => Trusted Notebook" width="500 px" align="left">
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).
End of explanation
"""


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'))

"""
Explanation: Lets start by first making two circuits:
* a GHZ state on four qubits 
* a superposition on two qubits
End of explanation
"""


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

"""
Explanation: The above shows the OpenQASM for both circuits. These can be converted to qobj to run on local simulator backend.
End of explanation
"""


qp.get_execution_list(qobj, verbose=True)

"""
Explanation: If you want more information about the circuits to be run, you can set verbose=True
End of explanation
"""


qp.get_compiled_configuration(qobj, 'GHZ', )

"""
Explanation: To get the configuration of a circuit, use
get_compiled_configuration(qobj, 'circuit')
End of explanation
"""


print(qp.get_compiled_qasm(qobj, 'GHZ'))

"""
Explanation: To get the compiled qasm, use
get_compiled_qasm(qobj,'circuit')
End of explanation
"""


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

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

"""
Explanation: 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.
End of explanation
"""


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

"""
Explanation: 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
End of explanation
"""


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())

"""
Explanation: 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.
End of explanation
"""


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

"""
Explanation: If we execute this circuit on the local simulator, we indeed see that the outcome is always "001".
End of explanation
"""


print(result.get_ran_qasm("qft3"))

"""
Explanation: 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.
End of explanation
"""


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

"""
Explanation: 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.
End of explanation
"""


print(result2.get_ran_qasm("qft3"))

"""
Explanation: 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.
End of explanation
"""


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

"""
Explanation: Finally, let's lay out the qubits onto a segment of the ibmqx3 16-qubit device.
End of explanation
"""


print(result3.get_ran_qasm("qft3"))

"""
Explanation: 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.
End of explanation
"""


