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(output='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'

# The Pauli-X gate serves this purpose, as demonstrated below:

qc3 = QuantumCircuit(1)

theta = np.pi/7

qc3.x(0)
qc3.ry(theta, 0)

qc3.x(0)
print(final_vector(qc3))

qc3 = QuantumCircuit(1)
qc3.ry(-theta, 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

qc4 = QuantumCircuit(1)

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

def A(qc, qbits, beta, gamma):
    qc.ry(gamma/2, qbits)
    qc.rz(beta, qbits)

def B(qc, qbits, beta, gamma, delta):
    qc.rz(-(beta+delta)/2, qbits)
    qc.ry(-gamma/2, qbits)
    
def C(qc, qbits, beta, delta):
    qc.rz((delta-beta)/2, qbits)

C(qc4, 0, beta, delta)
qc4.x(0)
B(qc4, 0, beta, gamma, delta)
qc4.x(0)
A(qc4, 0, beta, gamma)
qc4.unitary([[1.j, 0.], [0., 1.j]], [0])
print(final_vector(qc4))



qc4 = QuantumCircuit(1)
qc4.h(0)
print(final_vector(qc4))

qc5 = QuantumCircuit(3, 3)

# Target qubit q1 is initially |1> and target qubit q2 is initially |0>.

qc5.x(0) # Control qubit initialized to |1> to see effects of controlled Hadamard.
qc5.x(1) # One target qubit initialized to |1>.

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

def A(qc, qbits, beta, gamma):
    qc.ry(gamma/2, qbits)
    qc.rz(beta, qbits)

def B(qc, qbits, beta, gamma, delta):
    qc.rz(-(beta+delta)/2, qbits)
    qc.ry(-gamma/2, qbits)
    
def C(qc, qbits, beta, delta):
    qc.rz((delta-beta)/2, qbits)

C(qc5, [1, 2], beta, delta)
qc5.cx(0, [1, 2])
B(qc5, [1, 2], beta, gamma, delta)
qc5.cx(0, [1, 2])
A(qc5, [1, 2], beta, gamma)
qc5.s(0)   # Using S gate because alpha = pi/2

qc5.measure(0, 0)
qc5.measure(1, 1)
qc5.measure(2, 2)

qc5.draw('mpl')

def run_circuit(qc):
    backend = Aer.get_backend('qasm_simulator')
    result = execute(qc, backend, shots = 10000).result()
    counts = result.get_counts()
    return counts

counts = run_circuit(qc5)
print(counts)
plot_histogram(counts)

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

######### Defining some constants and functions ##########
# Constants for the decomposition of Hadamard gate.
H_alpha = np.pi/2
H_beta = 0
H_gamma = np.pi/2
H_delta = np.pi

# V^2 = X
V = [[0.5+0.5j, 0.5-0.5j],
     [0.5-0.5j, 0.5+0.5j]]

# Constants for the decomposition of V (where V^2 = X).
V_alpha = np.pi/4
V_beta = np.pi/2
V_gamma = -np.pi/2
V_delta = -np.pi/2

# Functions to implement A, B, C (generalized).
def A_gate(qc, qbits, beta, gamma):
    qc.ry(gamma/2, qbits)
    qc.rz(beta, qbits)

def B_gate(qc, qbits, beta, gamma, delta):
    qc.rz(-(beta+delta)/2, qbits)
    qc.ry(-gamma/2, qbits)
    
def C_gate(qc, qbits, beta, delta):
    qc.rz((delta-beta)/2, qbits)
    
# Higher abstraction functions.
def controlled_V(qc, control, target):
    C_gate(qc, target, V_beta, V_delta)
    qc.cx(control, target)
    B_gate(qc, target, V_beta, V_gamma, V_delta)
    qc.cx(control, target)
    A_gate(qc, target, V_beta, V_gamma)
    qc.t(control)   # Using T gate because V_alpha = pi/4.
    
def controlled_Vdg(qc, control, target):
    C_gate(qc, target, V_beta, V_delta)
    qc.cx(control, target)
    B_gate(qc, target, V_beta, -V_gamma, V_delta)
    qc.cx(control, target)
    A_gate(qc, target, V_beta, -V_gamma)
    qc.tdg(control)   # Using Tdg gate because V_alpha = pi/4, so Vdg_alpha would be -pi/4.
    
def double_controlled_X(qc, control1, control2, target):
    controlled_V(qc, control2, target)
    qc.cx(control1, control2)
    controlled_Vdg(qc, control2, target)
    qc.cx(control1, control2)
    controlled_V(qc, control1, target)
    
############ Constructing the circuit ##############

qc6 = QuantumCircuit(7, 5) # No need to measure ancillary qubits :P
# q0, q1, q2, q3 are control qubits
# q4, q5 are ancillary qubits
# q6 is the target qubit

# Change the following line to try different combinations of control qubits:
qc6.x([0,1,2,3])

C_gate(qc6, 6, H_beta, H_delta)

double_controlled_X(qc6, 0, 1, 4)
double_controlled_X(qc6, 2, 3, 5)
double_controlled_X(qc6, 4, 5, 6)
double_controlled_X(qc6, 2, 3, 5)
double_controlled_X(qc6, 0, 1, 4)

B_gate(qc6, 6, H_beta, H_gamma, H_delta)

double_controlled_X(qc6, 0, 1, 4)
double_controlled_X(qc6, 2, 3, 5)
double_controlled_X(qc6, 4, 5, 6)
double_controlled_X(qc6, 2, 3, 5)
double_controlled_X(qc6, 0, 1, 4)

A_gate(qc6, 6, H_beta, H_gamma)

qc6.s([0,1,2,3])    # Using S gate because H_alpha = pi/2.

qc6.measure(0, 0)
qc6.measure(1, 1)
qc6.measure(2, 2)
qc6.measure(3, 3)
qc6.measure(6, 4)

qc6.draw('mpl')

def run_circuit(qc):
    backend = Aer.get_backend('qasm_simulator')
    result = execute(qc, backend, shots = 2000).result()
    counts = result.get_counts()
    return counts

counts = run_circuit(qc6)
print(counts)
plot_histogram(counts)