#initialization
import matplotlib.pyplot as plt
%matplotlib inline
import numpy as np
import math

# importing Qiskit
from qiskit import IBMQ, BasicAer
from qiskit.providers.ibmq import least_busy
from qiskit import QuantumCircuit, ClassicalRegister, QuantumRegister, execute

# import basic plot tools
from qiskit.tools.visualization import plot_histogram
from qiskit.visualization import plot_state_qsphere

# Loading your IBM Q account(s)
provider = IBMQ.load_account()

##############################EXERCISE 1#############################################

#Define multi-qubit controlled Z-gate for n=2,3 or 4.
#n=2
def cz(circuit,qr):
    circuit.h(qr[1])
    circuit.cx(qr[0],qr[1])
    circuit.h(qr[1])
#n=3
def ccz(circuit,qr):
    circuit.h(qr[2])
    # ccx is the Toffoli gate 
    circuit.ccx(qr[0], qr[1], qr[2])
    circuit.h(qr[2])
#n=4
def cccz(circuit,qr):
    pi = math.pi
    circuit.cu1(pi/4,qr[0],qr[3])
    circuit.cx(qr[0], qr[1])
    circuit.cu1(-pi/4, qr[1],qr[3])
    circuit.cx(qr[0], qr[1])
    circuit.cu1(pi/4, qr[1],qr[3])
    circuit.cx(qr[1], qr[2])
    circuit.cu1(-pi/4, qr[2],qr[3])
    circuit.cx(qr[0], qr[2])
    circuit.cu1(pi/4, qr[2],qr[3])
    circuit.cx(qr[1], qr[2])
    circuit.cu1(-pi/4, qr[2],qr[3])
    circuit.cx(qr[0], qr[2])
    circuit.cu1(pi/4, qr[2],qr[3])
#Definition
def n_controlled_Z(circuit, qr):
    """Implement a Z gate with multiple controls"""
    if (len(qr) > 4):
        raise ValueError('The controlled Z with more than 3 controls is not implemented')
    # This is the case n = 2 (1 control + 1 target qubit)
    elif (len(qr) == 2):
        cz(circuit,qr)
    # This is the case n = 3 (2 control + 1 target qubit)
    elif (len(qr) == 3):
        ccz(circuit,qr)
        # This is the case n = 4 (3 control + 1 target qubit)
    elif (len(qr) == 4):
        cccz(circuit,qr)
#Now we check if this fuction ihas the same function as a multi-qubit controlled Z-gate. (ex.|11>->-|11>).

#####n=2####

##Circuitn2, input=all posible states.
#Create the circuit
qrn2=QuantumRegister(2)
crn2=ClassicalRegister(2)
circuitn2=QuantumCircuit(qrn2,crn2)
#Built all the posible input states using H-gates.
circuitn2.h(0)
circuitn2.h(1)
#Apply our function.
n_controlled_Z(circuitn2, qrn2)
#Draw the circuit.
circuitn2.draw()

#Execute the quantum circuit using qasm simulator.
backend = BasicAer.get_backend('statevector_simulator')
result = execute(circuitn2, backend).result()
psin2 = result.get_statevector(circuitn2)
#Print and plot the results.
plot_state_qsphere(psin2)


#Measure.
circuitn2.measure([0,1],[0,1])
#Execute the quantum circuit using qasm simulator.
backend = BasicAer.get_backend('qasm_simulator')
result = execute(circuitn2, backend, shots=10000).result()
countsn2  = result.get_counts(circuitn2)
#Print and plot the results.
print(countsn2)
plot_histogram(countsn2)

##Circuitn2Z, input=all posible states.
#Create the circuit
qrn2Z=QuantumRegister(2)
crn2Z=ClassicalRegister(2)
circuitn2Z=QuantumCircuit(qrn2Z,crn2Z)
#Built all the posible input states using H-gates.
circuitn2Z.h(0)
circuitn2Z.h(1)
#Apply our function.
circuitn2Z.cz(qrn2Z[0],qrn2Z[1])
#Draw the circuit.
circuitn2Z.draw()

#Execute the quantum circuit using state vector simulator.
backend = BasicAer.get_backend('statevector_simulator')
result = execute(circuitn2Z, backend).result()
psin2Z = result.get_statevector(circuitn2Z)
#Print and plot the results.
plot_state_qsphere(psin2Z)



#####n=3####

##Circuitn3, input=all posible states.
#Create the circuit
qrn3=QuantumRegister(3)
crn3=ClassicalRegister(3)
circuitn3=QuantumCircuit(qrn3,crn3)
#Built all the posible input states using H-gates.
circuitn3.h(0)
circuitn3.h(1)
circuitn3.h(2)
#Apply our function.
n_controlled_Z(circuitn3, qrn3)
#Draw the circuit.
circuitn3.draw()

#Execute the quantum circuit using statevector simulator.
backend = BasicAer.get_backend('statevector_simulator')
result = execute(circuitn3, backend).result()
psin3 = result.get_statevector(circuitn3)
#Print and plot the results.
plot_state_qsphere(psin3)

#Measure.
circuitn3.measure([0,1,2],[0,1,2])
#Execute the quantum circuit using qasm simulator.
backend = BasicAer.get_backend('qasm_simulator')
result = execute(circuitn3, backend, shots=10000).result()
countsn3  = result.get_counts(circuitn3)
#Print and plot the results.
print(countsn3)
plot_histogram(countsn3)



#####n=4####

##Circuitn4, input=all posible states.
#Create the circuit
qrn4=QuantumRegister(4)
crn4=ClassicalRegister(4)
circuitn4=QuantumCircuit(qrn4,crn4)
#Built all the posible input states using H-gates.
circuitn4.h(0)
circuitn4.h(1)
circuitn4.h(2)
circuitn4.h(3)
#Apply our function.
n_controlled_Z(circuitn4, qrn4)
#Draw the circuit.
circuitn4.draw()

#Execute the quantum circuit using statevector simulator.
backend = BasicAer.get_backend('statevector_simulator')
result = execute(circuitn4, backend).result()
psin4 = result.get_statevector(circuitn4)
#Print and plot the results.
plot_state_qsphere(psin4)

#Measure.
circuitn4.measure([0,1,2,3],[0,1,2,3])
#Execute the quantum circuit using qasm simulator.
backend = BasicAer.get_backend('qasm_simulator')
result = execute(circuitn4, backend, shots=10000).result()
countsn4  = result.get_counts(circuitn4)
#Print and plot the results.
print(countsn4)
plot_histogram(countsn4)

##Circuit0n4, input=|0000>.
#Create the circuit
qr0n4=QuantumRegister(4)
cr0n4=ClassicalRegister(4)
circuit0n4=QuantumCircuit(qr0n4,crn4)
#The states are already 0.
#Apply our function.
n_controlled_Z(circuit0n4, qr0n4)
#Draw the circuit.
circuit0n4.draw()

#Execute the quantum circuit using statevector simulator.
backend = BasicAer.get_backend('statevector_simulator')
result = execute(circuit0n4, backend).result()
psi0n4 = result.get_statevector(circuit0n4)
#Print and plot the results.
plot_state_qsphere(psi0n4)





##############################EXERCISE 2#############################################

#Check whether Oracle acts as expected.
#Define Oracle
def phase_oracle(circuit,qr,element):
    # element is an array that defines the searched element, for example, element =  [0,1,0,1]
    n = len(element)
    for j,x in enumerate(element):
        if (x == 0):
            circuit.x(qr[j])
    n_controlled_Z(circuit,qr) 
    for j,x in enumerate(element):
        if (x == 0):
            circuit.x(qr[j])

# Create Circuite2, input=all posible states.
#Create the circuit
qre2=QuantumRegister(4)
cre2=ClassicalRegister(4)
circuite2=QuantumCircuit(qre2,cre2)
#Built all the posible input states using H-gates.
circuite2.h(0)
circuite2.h(1)
circuite2.h(2)
circuite2.h(3)
#Chose element to search.
elemente2=[0,1,1,0]
#Apply Oracle.
phase_oracle(circuite2,qre2,elemente2)
#Draw the circuit.
circuite2.draw()

#Execute the quantum circuit using statevector simulator.
backend = BasicAer.get_backend('statevector_simulator')
result = execute(circuite2, backend).result()
psie2 = result.get_statevector(circuite2)
#Print and plot the results.
plot_state_qsphere(psie2)

#Measure.
circuite2.measure([0,1,2,3],[0,1,2,3])
#Execute the quantum circuit using qasm simulator.
backend = BasicAer.get_backend('qasm_simulator')
result = execute(circuite2, backend, shots=10000).result()
countse2  = result.get_counts(circuite2)
#Print and plot the results.
print(countse2)
plot_histogram(countse2)





##############################EXERCISE 3#############################################

#Define Inversion 
def inversion_about_average(circuit, register):
    """Apply inversion about the average step of Grover's algorithm."""
    circuit.h(register)
    circuit.x(register)
    n_controlled_Z(circuit, qr)
    circuit.x(register)
    circuit.h(register)

#Circuit that implements the Grover's Algorithm (we define a function in which we introduce our parameters).

def grovers_algorithm(element,R,circuit,qr):
    #circuit=defined input of the circuit.
    #element=[a,b,c,d] with a,b=0,1, array representing the searched element.
    #R=number of repetitions.
    #Apply the algorithm R times.
    for i in range(1,R):
        #Apply Oracle.
        phase_oracle(circuit,qr,element)
        #Apply Inversion.
        inversion_about_average(circuit,qr)




#Ex. searching |1010> 
#Create the circuit and set it with input=all posible states (2^4).
qr=QuantumRegister(4)
cr=ClassicalRegister(4)
circuit=QuantumCircuit(qr,cr)
circuit.h(0)
circuit.h(1)
circuit.h(2)
circuit.h(3)

#Apply the algorithm.
grovers_algorithm([0,1,0,1],2,circuit,qr)

#Plot the circuit.
circuit.draw()

#Measure.
circuit.measure([0,1,2,3],[0,1,2,3])
#Execute the quantum circuit using qasm simulator.
backend = BasicAer.get_backend('qasm_simulator')
result = execute(circuit, backend, shots=1000).result()
counts  = result.get_counts(circuit)
#Print and plot the results.
print(counts)
plot_histogram(counts)


#Ex. searching |111> 
#Create the circuit and set it with input=all posible states (2^4).
qr=QuantumRegister(3)
cr=ClassicalRegister(3)
circuit=QuantumCircuit(qr,cr)
circuit.h(0)
circuit.h(1)
circuit.h(2)

#Apply the algorithm.
grovers_algorithm([1,1,1],3,circuit,qr)

#Plot the circuit.
circuit.draw()

#Measure.
circuit.measure([0,1,2],[0,1,2])
#Execute the quantum circuit using qasm simulator.
backend = BasicAer.get_backend('qasm_simulator')
result = execute(circuit, backend, shots=1000).result()
counts  = result.get_counts(circuit)
#Print and plot the results.
print(counts)
plot_histogram(counts)

#Ex. searching |00> 
#Create the circuit and set it with input=all posible states (2^4).
qr=QuantumRegister(2)
cr=ClassicalRegister(2)
circuit=QuantumCircuit(qr,cr)
circuit.h(0)
circuit.h(1)

#Apply the algorithm.
grovers_algorithm([0,1],2,circuit,qr)

#Plot the circuit.
circuit.draw()

#Measure.
circuit.measure([0,1],[0,1])
#Execute the quantum circuit using qasm simulator.
backend = BasicAer.get_backend('qasm_simulator')
result = execute(circuit, backend, shots=1000).result()
counts  = result.get_counts(circuit)
#Print and plot the results.
print(counts)
plot_histogram(counts)





##############################EXERCISE 4#############################################
#number of measurements: it's easy to see by changing the parameter shoots that it doesn't really affect the results.
#number of iterations: it must be chosen carefully because it follows a repetitive structure, being R~sqrt(n) a good election.

