# Do not forget to Import Qiskit 

from qiskit.visualization import * 
from qiskit import *

S_simulator=Aer.backends(name='statevector_simulator')[0] 
M_simulator=Aer.backends(name='qasm_simulator')[0] 

# new import allows us to use CircuitOP() and CircuitStateFn() as well as operators I,X,Y,Z etc.

from qiskit.aqua.operators import *
from qiskit.quantum_info import random_unitary

op=QuantumCircuit(1) # make an operator circuit, and turn it into an operator! DO NOT add a classical bit

op.h(0)

op =CircuitOp(op)

psi = QuantumCircuit(1) # make a wavefunction circuit. 

psi=CircuitStateFn(psi)

# psi is our ket and we can use psi.adjoint() to get its complext conjugate, the bra 

# so to do an expectation value of <psi| H |psi> we just do psi.adjoint() on H |psi> this can be done in the following way

print('expectation value: ', psi.adjoint().compose(op).compose(psi).eval().real)

op =  (-1.0523732 * I^I) + (0.39793742 * I^Z) + (-0.3979374 * Z^I) \
    + (-0.0112801 * Z^Z) + (0.18093119 * X^X)
psi = QuantumCircuit(2)
psi.x(0) 
psi.x(1) 

psi=CircuitStateFn(psi)
print('expectation value: ', psi.adjoint().compose(op).compose(psi).eval().real) 

# Try to follow along a problem from a paper [1]

# construct a circuit with 5 parameters. 

from random import uniform 
from math import pi

thetas = []
n=5

for i in range(n): 
    
    thetas.append(uniform(0,2*pi))

theta1 = thetas[0]

theta2 = thetas[1]

theta3 = thetas[2]

theta4 = thetas[3]

theta5 = thetas[4]

print('starting thetas: ', thetas)

# these random thetas are the starting values for our rotation operators


variational_circuit=QuantumCircuit(5)

#rotations that are dependent on theta
variational_circuit.rx(theta1, 0)
variational_circuit.rx(theta2, 1)
variational_circuit.rx(theta3, 2)
variational_circuit.rx(theta4, 3)
variational_circuit.rx(theta5, 4)

variational_circuit.barrier()


#entangling block (dont worry too much)

variational_circuit.cx(0,1)

variational_circuit.barrier()

variational_circuit.cx(2,1)

variational_circuit.barrier()

variational_circuit.cx(3,1)

variational_circuit.barrier()

variational_circuit.cx(4,3)

variational_circuit.barrier()


variational_circuit.draw(output='mpl')

U = CircuitOp(variational_circuit)

Z = QuantumCircuit(5)

Z.z(1)

Z = CircuitOp(Z)

# we can easily find the complex conjugate of the U 

U_dag = U.adjoint()

psi = QuantumCircuit(5) # make a wavefunction circuit. 

psi=CircuitStateFn(psi)


print('expectation value: ', 
psi.adjoint().compose(U_dag).compose(Z).compose(U).compose(psi).eval().real) 

def make_variational_circuit(thetas): 
    
    theta1 = thetas[0]

    theta2 = thetas[1]

    theta3 = thetas[2]

    theta4 = thetas[3]

    theta5 = thetas[4]
    
    variational_circuit=QuantumCircuit(5)

    #rotations that are dependent on theta
    variational_circuit.rx(theta1, 0)
    variational_circuit.rx(theta2, 1)
    variational_circuit.rx(theta3, 2)
    variational_circuit.rx(theta4, 3)
    variational_circuit.rx(theta5, 4)

    variational_circuit.barrier()


    #entangling block (dont worry too much)

    variational_circuit.cx(0,1)

    variational_circuit.barrier()

    variational_circuit.cx(2,1)

    variational_circuit.barrier()

    variational_circuit.cx(3,1)

    variational_circuit.barrier()

    variational_circuit.cx(4,3)

    variational_circuit.barrier()

    return variational_circuit

def get_U_and_U_dag(variational_circuit): 
    
    U = CircuitOp(variational_circuit)
    
    U_dag = U.adjoint()
    
    return U, U_dag


# We can hardcode Z in here but thats not great convention 
# this is strictly for example and not practical 

def expectation_value(U, U_dag):
    
    psi = QuantumCircuit(5) # make a wavefunction circuit. 

    psi=CircuitStateFn(psi)

    Z = QuantumCircuit(5)

    Z.z(1)

    Z = CircuitOp(Z)
    
    return psi.adjoint().compose(U_dag.compose(Z.compose(U))).compose(psi).eval().real

# to shift our thetas it can be done a number of ways

thetas_plus = []

thetas_minus = []

for i in range(len(thetas)): 
    
    thetas_plus.append(thetas[i]+(0.5*pi))
    
    thetas_minus.append(thetas[i]-(0.5*pi))

# our first gradient can be calculated now

def get_eval(thetas): 
    
    variational_circuit = make_variational_circuit(thetas)
    
    U, U_dag = get_U_and_U_dag(variational_circuit)
    
    return expectation_value(U,U_dag)


def get_gradient(thetas): 
    
    thetas_plus = []

    thetas_minus = []

    for i in range(len(thetas)): 

        thetas_plus.append(thetas[i]+(0.5*pi))

        thetas_minus.append(thetas[i]-(0.5*pi))
        
        
    f_plus = get_eval(thetas_plus)

    f_minus = get_eval(thetas_minus)
    
    
    return 0.5*(f_plus - f_minus)

get_gradient(thetas)



def update_thetas(thetas): 
    
    learning_rate=0.2
    gradient = get_gradient(thetas)
    
    updated_thetas=[]
    
    for i in range(len(thetas)): 

        updated_thetas.append(thetas[i] - (learning_rate*gradient))

        
    return updated_thetas


print('thetas before update: ', thetas)

thetas = update_thetas(thetas)

print('thetas after update: ', thetas)

from random import uniform 
from math import pi

thetas = []
n=5

for i in range(n): 
    
    thetas.append(uniform(2*pi,2*pi))

print('thetas before update: ', thetas)

print('gradient: ', get_gradient(thetas))

thetas = update_thetas(thetas)

print('thetas after update: ', thetas)

thetas_plus = []

thetas_minus = []

for i in range(len(thetas)): 

    thetas_plus.append(thetas[i]+(0.5*pi))

    thetas_minus.append(thetas[i]-(0.5*pi))


f_plus = get_eval(thetas_plus)

f_minus = get_eval(thetas_minus)
print(f_plus, f_minus)

var = QuantumCircuit()





