from qiskit import *
from qiskit.compiler import transpile, assemble
from qiskit.tools.jupyter import *
from qiskit.visualization import *
import matplotlib.pyplot as plotter
import numpy as np
from IPython.display import display, Math, Latex
%matplotlib inline
# Loading your IBM Q account(s)
provider = IBMQ.load_account()

qc1 = QuantumCircuit(3,3)
# All initialized to '0' by default.
qc1.x(0) #This is for the purpose of setting the control qubit to '1'
qc1.x(2) #As a result, the second target qubit becomes '1' while the first remains '0'. Now, lets's try swapping them.
#Fredkin gate:
def fredkin(qc):
    qc.toffoli(0,1,2)
    qc.toffoli(0,2,1)
    qc.toffoli(0,1,2)
fredkin(qc1)
qc1.draw('mpl')


#First let's measure all three qubits.
#We're using the classical bits to store the result obtained on measuring each corresponding qubit. 
qc1.measure(0,0)
qc1.measure(1,1)
qc1.measure(2,2)
#Now we use the same function we defined yesterday to run a quantum circuit
def run_circuit(qc2):
    backend = Aer.get_backend('qasm_simulator') # we choose the simulator as our backend
    result = execute(qc2, backend, shots = 2000).result() # we run the simulation
    counts = result.get_counts() # we get the counts
    return counts
counts1=run_circuit(qc1)
print(counts1)
plot_histogram(counts1)


qc2 = QuantumCircuit(3,3)
# All initialized to '0' by default.
qc2.x(2) #The second target qubit is initialised to '1'
fredkin(qc2)
qc2.measure(0,0)
qc2.measure(1,1)
qc2.measure(2,2)
qc2.draw('mpl')


counts2=run_circuit(qc2)
print(counts2)
plot_histogram(counts2)

qc = QuantumCircuit(1)
#This is how we apply the rotation operators in Qiskit, mentioning the angle of rotation and qubit no. as parameters
qc.rx(np.pi/2,0) 
qc.draw('mpl')


def final_vector(qc):
    backend = Aer.get_backend('statevector_simulator')
    job = execute(qc, backend)
    result = job.result()
    outputstate = result.get_statevector(qc, decimals=3)
    return outputstate
print(final_vector(qc))
# This prints the vector obtained on applying the above gate to the qubit state '0'

#Verifying for the X gate
qc3=QuantumCircuit(1)
qc3.x(0)

qc3.ry(np.pi/8,0)
#Enter an angle of your choice

qc3.x(0)

print(final_vector(qc3))


qc3=QuantumCircuit(1)
qc3.ry(-np.pi/8,0)
print(final_vector(qc3))
#Run this code for different values of theta and see if the two vectors printed are equal in each case

#Verifying for the H gate
qc3=QuantumCircuit(1)
qc3.h(0)

qc3.ry(np.pi/8,0)
#Enter an angle of your choice

qc3.h(0)

print(final_vector(qc3))


qc3=QuantumCircuit(1)
qc3.ry(-np.pi/8,0)
print(final_vector(qc3))
#Run this code for different values of theta and see if the two vectors printed are equal in each case

gamma = np.pi/2
alpha = np.pi/2
beta= 0
delta = np.pi

def A(qc,b,g,q):
    qc.ry(g/2,q)
    qc.rz(b,q)
    

def B(qc,g,d,b,q):
    qc.rz((-d-b)/2,q)
    qc.ry(-g/2,q)

def C(qc,d,b,q):
    qc.rz((d-b)/2,q)

#Initialising the unitary matrix for the global phase
#exp(i pi/2) = i

U = np.array([[1j, 0],[0, 1j]])

#Let us now apply the transformation

qc4 = QuantumCircuit(1)

C(qc4,delta,beta,0)

qc4.x(0)
 
B(qc4,gamma,delta,beta,0)

qc4.x(0)

A(qc4,beta,gamma,0)

qc4.unitary(U, [0], 0)

print(final_vector(qc4))

#Reinitialise the Quantum Circuit

qc4 = QuantumCircuit(1)

qc4.h(0)

print(final_vector(qc4))

#We get that exp(i alpha) AXBXC = H, thus the decomposition is correct

#Note: We will be accepting solutions which differ in global phase

qc5=QuantumCircuit(2,2)
qc5.x(0)


C(qc5,delta,beta,1)
qc5.cx(0,1)
B(qc5,gamma,delta,beta,1)
qc5.cx(0,1)
A(qc5,beta,gamma,1)
qc5.u1(alpha,0)


qc5.measure(0,0)
qc5.measure(1,1)
qc5.draw('mpl')

#Note: You don't need to draw the circuit, we're just displaying it for clarity

counts5=run_circuit(qc5)
print(counts5)
plot_histogram(counts5)

#We have succesfully implemented the controlled Hadamard gate

qc_u3=QuantumCircuit(1)
qc_u3.u3(np.pi/6,-np.pi/2,np.pi/2,0)
print(final_vector(qc_u3))

qc_rx=QuantumCircuit(1)
qc_rx.rx(np.pi/6,0)
print(final_vector(qc_rx))
#Getting the same results will verify our observation stated above

#Let's first build controlled V and controlled V dagger gates
def V(qc,q0,qt):
    C(qc,np.pi/2,3*np.pi/2,qt)
    qc.cx(q0,qt)
    B(qc,np.pi/2,np.pi/2,3*np.pi/2,qt)
    qc.cx(q0,qt)
    A(qc,3*np.pi/2,np.pi/2,qt)
    qc.u1(5*np.pi/4,q0)
    
def Vdag(qc,q0,qt):
    C(qc,3*np.pi/2,np.pi/2,qt)
    qc.cx(q0,qt)
    B(qc,np.pi/2,3*np.pi/2,np.pi/2,qt)
    qc.cx(q0,qt)
    A(qc,np.pi/2,np.pi/2,qt)
    qc.u1(3*np.pi/4,q0)
    
    
#Now we can build the Toffoli gate 
def toffoli(qc,q0,q1,q2):
    V(qc,q1,q2)
    qc.cx(q0,q1)
    Vdag(qc,q1,q2)
    qc.cx(q0,q1)
    V(qc,q0,q2)



qch=QuantumCircuit(8,5)
#The 3 qubits from fifth to seventh will be used as work qubits
#To check the final vector, let's just set all qubits to |1>
for i in range(4):
    qch.x(i)


toffoli(qch,0,1,4)
toffoli(qch,2,4,5)
toffoli(qch,3,5,6)
#The seventh qubit, ie, q6 will be used as the control qubit in the controlled Hadamard
#Let's just copy paste the code of the controlled Hadamard gate
gamma = np.pi/2
alpha = np.pi/2
beta= 0
delta = np.pi

C(qch,delta,beta,7)
qch.cx(6,7)
B(qch,gamma,delta,beta,7)
qch.cx(6,7)
A(qch,beta,gamma,7)
qch.u1(alpha,6)
#Let's undo the Toffolis
toffoli(qch,0,1,4)
toffoli(qch,2,4,5)
toffoli(qch,3,5,6)

qch.measure(0,0)
qch.measure(1,1)
qch.measure(2,2)
qch.measure(3,3)
qch.measure(7,4)

qch.draw('mpl')




countsh=run_circuit(qch)
print(countsh)
plot_histogram(countsh)

#Hence we have successfully applied the C^4(H) gate