# Quantum NNN: Ver 0.1,  By Sultan Almuhammadi and Sarah Alghamdi
# Date: Dec 1, 2021
from sklearn import model_selection, datasets, svm
from qiskit import QuantumCircuit, Aer, IBMQ, QuantumRegister, ClassicalRegister
import qiskit
import numpy as np
import copy
import matplotlib.pyplot as plt1
import matplotlib.pyplot as plt2


iris = datasets.load_iris()
X = iris.data[0:100]  # We only take the first two features.
Y = iris.target[0:100]

X_train, X_test, Y_train, Y_test, = model_selection.train_test_split(X, Y, test_size=0.33, random_state=42)


print(Y_train)

print(X_train[0])
print (len(Y_train), len(X_train), len(X_test), len(Y_test))

print (X_test)


N = 4
myX_train = [[0 for i in range(N)] for j in range(len(X_train))]
myX_test = [[0 for i in range(N)] for j in range(len(X_test))]



# Correct the size of the features basaed on N

for i in range(len(X_train)): 
    for j in range(N):
        if (j<len(X_train[i])):
            myX_train[i][j]=X_train[i][j] 
        else:
            myX_train[i][j] = 1.0
            
for i in range(len(X_test)): 
    for j in range(N):
        if (j<len(X_test[i])): 
            myX_test[i][j]=X_test[i][j]
        else:
            myX_test[i][j] = 1.0
            

print(myX_test)

def feature_map(X):
    q = QuantumRegister(N)
    c = ClassicalRegister(1)
    qc = QuantumCircuit(q, c)
    for i, x in enumerate(X):
        qc.rx(x, i)
    return qc, c     


def variational_circuit(qc, theta):
    for i in range(N-1):
        qc.cnot(i, i+1)
    qc.cnot(N-1, 0)
    for i in range(N):
        qc.ry(theta[i], i)
    return qc    

# SSultan Test  

X = [5.2, 3.4, 1.4,0.2]
theta = [0.785]*N
myqc, myc = feature_map(X) 
myqc = variational_circuit(myqc, theta)
myqc.measure(0, myc)
myqc.draw()


def quantum_nn(X, theta, simulator=True):
    qc, c = feature_map(X)
    qc = variational_circuit(qc, theta)
    qc.measure(0, c)

    shots= 10000
    backend = Aer.get_backend('qasm_simulator')
    if simulator == False:
        shots = 1000
        provider= IBMQ.load_account()
        backend= provider.get_backend('ibmq_bogota')
    
    job = qiskit.execute(qc, backend, shots=shots)
    result = job.result()
    counts = result.get_counts(qc)
           
    return counts['1']/shots
   

def loss(prediction, target):
    return (prediction - target)**2

def gradient (X,Y,theta):
    delta = 0.01
    grad = []
    for i in range(len(theta)):
        dtheta = copy.copy(theta)
        dtheta[i] += delta
        
        pred1 =quantum_nn(X,dtheta)
        pred2 =quantum_nn(X,theta)
        
        grad.append((loss(pred1, Y) - loss(pred2, Y)) / delta)
                    
    return np.array(grad)             

def accuracy(X, Y, theta):
    counter = 0
    for i in range(len(X)):
        X_i = X[i]
        Y_i = Y[i]
        prediction = quantum_nn(X_i, theta)
        #pint(X_i, Y_i, prediction)
        if prediction < 0.5 and Y_i == 0:
            counter +=1
        elif prediction >= 0.5 and Y_i == 1 :  
            counter +=1
    return  100.0*counter/len(Y)      

# main program 

eta = 0.05
loss_list = []
acc_list = []
theta = np.ones(N)
print('  Epoch\t  Loss\t  Accuracy')


for k in range(10):
    loss_tmp = []
    
    for i in range(len(myX_train)):
        X_i = myX_train[i]
        Y_i = Y_train[i]
        prediction = quantum_nn(X_i, theta)
        loss_tmp.append(loss(prediction,Y_i))
        theta =theta - eta * gradient(X_i, Y_i, theta)
    loss_list.append(np.mean(loss_tmp))  
    acc = accuracy(myX_train, Y_train, theta)
    acc_list.append(acc)
    print(f'\t{k}\t  {loss_list[-1]:.3f}\t  {acc:.1f}')

plt1.plot(loss_list)
plt1.xlabel('Epoch')
plt1.ylabel('Loss')
plt1.show()

plt2.plot(acc_list)
plt2.xlabel('Epoch')
plt2.ylabel('Accuracy (%)')
plt2.show()


accuracy(myX_test, Y_test, theta)

X_sample = myX_test[6]

print('The predection of the test sample on a simulator:')
quantum_nn(X_sample, theta)

print('The predection of the test sample (for N = 4) on IBM-Q quantum computer (ibm_bogota):')
quantum_nn(X_sample, theta, simulator = False)

