# Installing a few dependencies
!pip install --upgrade seaborn==0.10.1
!pip install --upgrade scikit-learn==0.23.1
!pip install --upgrade matplotlib==3.2.0
!pip install --upgrade pandas==1.0.4
!pip install --upgrade qiskit==0.19.6 
!pip install --upgrade plotly==4.9.0
!pip install qiskit-qcgpu-provider

# The output will be cleared after installation
from IPython.display import clear_output
clear_output()

# we have imported a few libraries we think might be useful 
from qiskit import QuantumCircuit, QuantumRegister, ClassicalRegister
from qiskit import Aer

from qiskit import *
import numpy as np
from qiskit.visualization import plot_bloch_multivector, plot_histogram
%matplotlib inline
import matplotlib.pyplot as plt

import time
from qiskit.circuit.library import ZZFeatureMap, ZFeatureMap, PauliFeatureMap, RealAmplitudes, EfficientSU2
from qiskit.aqua.utils import split_dataset_to_data_and_labels, map_label_to_class_name
from qiskit.aqua import QuantumInstance
from qiskit.aqua.algorithms import VQC
from qiskit.aqua.components.optimizers import COBYLA


# The the write_and_run() magic function creates a file with the content inside the cell that it is run. 

from IPython.core.magic import register_cell_magic
@register_cell_magic
def write_and_run(line, cell):
    argz = line.split()
    file = argz[-1]
    mode = 'w'
    with open(file, mode) as f:
        f.write(cell)
    get_ipython().run_cell(cell)

!wget -O "dataset_4_9.csv" https://gitlab.com/GlazeDonuts/vqc-datasets/-/raw/master/datasets/dataset_4_9.csv

data = np.loadtxt("dataset_4_9.csv", delimiter=",")

# extracting the first column which contains the labels
data_labels = data[:, :1].reshape(data.shape[0],)
# extracting all the columns but the first which are our features
data_features = data[:, 1:]

import plotly.express as px
import pandas as pd

# Creating a dataframe using pandas only for the purpose fo plotting
df = pd.DataFrame({'Component 0':data_features[:,0], 'Component 1':data_features[:,1], 
                   'Component 2':data_features[:,2], 'label':data_labels})

fig = px.scatter_3d(df, x='Component 0', y='Component 1', z='Component 2', color='label')
fig.show()

### WRITE YOUR CODE BETWEEN THESE LINES - START
# Percentage of corpus considered for training
corp_perc = 4

# Calculating the total number of samples
corp_split = int(corp_perc * data.shape[0] // 100)

# Choosing random samples from data points corresponind to four and nine
four_indices = np.random.choice(np.arange(data.shape[0]//2), corp_split//2, replace=False)
nine_indices = np.random.choice(np.arange(data.shape[0]//2, data.shape[0]), corp_split//2, replace=False)
my_data_four = np.copy(data[four_indices])
my_data_nine = np.copy(data[nine_indices])

# Forming data set with samples of fours and nines
my_data = np.concatenate((my_data_four, my_data_nine))
test_perc = 5
semi_split = int(test_perc*my_data.shape[0]//200)

# Creating training and testing input dictionaries
training_input = {'A': my_data_four[:-semi_split, 1:], 'B': my_data_nine[:-semi_split, 1:]}
test_input = {'A': my_data_four[-semi_split:, 1:], 'B': my_data_nine[-semi_split:, 1:]}
testing_input = np.concatenate((my_data_four[-semi_split:, 1:], my_data_nine[-semi_split:, 1:]))
testing_labels = np.concatenate((my_data_four[-semi_split:, :1], my_data_nine[-semi_split:, :1]))

### WRITE YOUR CODE BETWEEN THESE LINES - END

%%write_and_run feature_map.py
# The write_and_run function writes the content in this cell into the file "feature_map.py"

### WRITE YOUR CODE BETWEEN THESE LINES - START
    
# Import libraries that are used in the function below.
from qiskit import QuantumCircuit
from qiskit.circuit import ParameterVector
from qiskit.circuit.library import ZZFeatureMap, ZFeatureMap, PauliFeatureMap
    
### WRITE YOUR CODE BETWEEN THESE LINES - END

def feature_map(): 
    # BUILD FEATURE MAP HERE - START
    
    # Import required qiskit libraries if additional libraries are required
    
    # Build the feature map
    reps = 3
    num_qubits = 3    
    feature_map = QuantumCircuit(num_qubits)
    x = ParameterVector('x', length=num_qubits)

    # EXPERIMENT 1 : 64
    # for _ in range(reps):
    #     for i in range(num_qubits):
    #         feature_map.rx(x[i], i)
    #     for i in range(num_qubits):
    #         for j in range(i + 1, num_qubits):
    #             feature_map.cx(i, j)
    #             feature_map.u1(x[i] * x[j], j)
    #             feature_map.cx(i, j)

    # EXPERIMENT 2: 63.5
    # for _ in range(reps):
    #     for i in range(num_qubits):
    #         feature_map.h(i)
    #     for i in range(num_qubits - 1):
    #         feature_map.cz(i, i+1)
    #     for i in range(num_qubits):
    #         feature_map.rx(x[i], i)
    #     for i in range(num_qubits-1, 0, -1):
    #         feature_map.cz(i, i-1)
    #     for i in range(num_qubits):
    #         feature_map.h(i)

    # #EXPERIMENT 3: 72.9
    # for _ in range(reps):
    #     for i in range(num_qubits):
    #         feature_map.rx(x[i], i)
    #         feature_map.rz(x[i], i)
    #     for i in range(num_qubits-1, 0, -1):
    #         feature_map.cx(i, i-1)

    #EXPERIMENT 4: 71.2
    # for _ in range(reps):
    #     for i in range(num_qubits):
    #         feature_map.rx(x[i], i)
    #         feature_map.rz(x[i], i)
    #     for i in range(num_qubits-1, 0, -1):
    #         feature_map.cz(i, i-1)

    # EXPERIMENT 5: 75.6
    # for _ in range(reps):
    #     for i in range(num_qubits):
    #         feature_map.rx(x[i], i)
    #         feature_map.rz(x[i], i)
    #     for i in range(num_qubits-1, 0, -1):
    #         feature_map.rz(x[i-1], i-1)
    #         feature_map.cx(i, i-1)
    #         feature_map.rz(x[i-1], i-1)

    # EXPERIMENT 6: 75.7
    # for _ in range(reps):
    #     for i in range(num_qubits):
    #         feature_map.rx(x[i], i)
    #         feature_map.rz(x[i], i)
    #     for i in range(num_qubits-1, 0, -1):
    #         feature_map.rx(x[i-1], i-1)
    #         feature_map.cx(i, i-1)
    #         feature_map.rx(x[i-1], i-1)

    # Experiment 7: 76.8 with 2 reps and 4 vqc
    # for _ in range(reps):
    #     for i in range(num_qubits):
    #         feature_map.rx(x[i], i)
    #         feature_map.rz(x[i], i)
    #     for control in range(num_qubits-1, -1, -1):
    #         for target in range(num_qubits-1, -1, -1):
    #             if control != target:
    #                 feature_map.rz(x[target], target)
    #                 feature_map.cx(control, target)
    #                 feature_map.rz(x[target], target)

    # Experiment 8: 75.2
    # for _ in range(reps):
    #     for i in range(num_qubits):
    #         feature_map.rx(x[i], i)
    #         feature_map.rz(x[i], i)
    #     for control in range(num_qubits-1, -1, -1):
    #         for target in range(num_qubits-1, -1, -1):
    #             if control != target:
    #                 feature_map.rx(x[target], target)
    #                 feature_map.cx(control, target)
    #                 feature_map.rx(x[target], target)

    # Experiment 9: 77.6
    # for _ in range(reps):
    #     for i in range(num_qubits):
    #         feature_map.rx(x[i], i)
    #         feature_map.rz(x[i], i)
    #     for control in range(num_qubits-1, 0, -1):
    #         target = control - 1
    #         feature_map.rz(x[target], target)
    #         feature_map.cx(control, target)
    #         feature_map.rz(x[target], target) # | Upto this plus Experiment 10 (both 1 rep each), gave 79 with 4 reps of VQC
        # for i in range(num_qubits): # Reduced to 75.7
        #     feature_map.rx(x[i], i)
        #     feature_map.rz(x[i], i)
        # feature_map.rz(x[1], 1) # Reduced to 70.5
        # feature_map.cx(2, 1)
        # feature_map.rz(x[1], 1)

    # Experiment 10: 77.8
    for _ in range(reps):
        for i in range(num_qubits):
            feature_map.rx(x[i], i)
            feature_map.rz(x[i], i)
        feature_map.barrier()
        for control in range(num_qubits-1, 0, -1):
            target = control - 1
            feature_map.rx(x[target], target)
            feature_map.cx(control, target)
            feature_map.rx(x[target], target)
            feature_map.barrier()
        for i in range(num_qubits):
            feature_map.rx(x[i], i)
            feature_map.rz(x[i], i)
        feature_map.barrier()

    # Experiment 11: 65.4 with 2 vqc reps | 71.1 with 3 reps of vqc
    # for _ in range(reps):
    #     for i in range(num_qubits):
    #         feature_map.h(i)
    #     for i in range(num_qubits - 1):
    #         feature_map.cz(i, i+1)
    #     for i in range(num_qubits):
    #         feature_map.rx(x[i], i)

    # Experiment 12: 76.4 
    # for _ in range(reps):
    #     for i in range(num_qubits):
    #         feature_map.ry(x[i], i)
    #     for i in range(num_qubits - 1, 0, -1):
    #         feature_map.cz(i, i-1)
    #     feature_map.cz(num_qubits-1, 0)
    #     for i in range(num_qubits):
    #         feature_map.ry(x[i], i)

    # Experiment 13: 70.1
    # for _ in range(reps):
    #     for i in range(num_qubits):
    #         feature_map.ry(x[i], i)
    #         feature_map.rz(x[i], i)
    #     for i in range(num_qubits - 1, 0, -1):
    #         feature_map.cx(i, i-1)

    # Experiment 14: 75.2
    # for _ in range(reps): 
    #     for i in range(num_qubits):
    #         feature_map.ry(x[i], i)
    #         feature_map.rz(x[i], i)
    #     for i in range(num_qubits - 1, 0, -1):
    #         feature_map.cx(i, i-1)
    #     feature_map.ry(x[1], 1) 
    #     feature_map.rz(x[1], 1)


    # Experiment 15: 77.3
    # for _ in range(reps):
    #     for i in range(num_qubits):
    #         feature_map.ry(x[i], i)
    #         feature_map.rz(x[i], i)
    #     for i in range(num_qubits - 1, 0, -1):
    #         feature_map.cz(i, i-1)
    #     feature_map.ry(x[1], 1)
    #     feature_map.rz(x[1], 1)

    # Experiment 16: 73.1
    # for _ in range(reps):
    #     for i in range(num_qubits):
    #         feature_map.rx(x[i], i)
    #         feature_map.rz(x[i], i)
    #     feature_map.rx(x[0], 0)
    #     feature_map.cx(num_qubits - 1, 0)
    #     feature_map.rx(x[0], 0)
    #     for i in range(num_qubits-2, -1, -1):
    #         feature_map.rx(x[i+1], i+1)
    #         feature_map.cx(i, i+1)
    #         feature_map.rx(x[i+1], i+1)

    # Experiment 17: 77.0
    # for _ in range(reps):
    #     for i in range(num_qubits):
    #         feature_map.rx(x[i], i)
    #         feature_map.rz(x[i], i)
    #     feature_map.rz(x[0], 0)
    #     feature_map.cx(num_qubits - 1, 0)
    #     feature_map.rz(x[0], 0)
    #     for i in range(num_qubits-2, -1, -1):
    #         feature_map.rz(x[i+1], i+1)
    #         feature_map.cx(i, i+1)
    #         feature_map.rz(x[i+1], i+1)

    # Experiment 18: 73.8
    # for _ in range(reps):
    #     for i in range(num_qubits):
    #         feature_map.rx(x[i], i)
    #         feature_map.rz(x[i], i)
    #     for control in range(num_qubits - 1, 0, -1):
    #         target = control - 1
    #         feature_map.rx(x[target], target)
    #         feature_map.cx(control, target)
    #         feature_map.rx(x[target], target)

    # Experiment 19: 78.7 2 reps 2 su2 | Gave 52 with RealAmp 4 reps | 68 with 2 reps and SU2 4 reps | 61.4 with 3 reps and su2 4 reps
    # for _ in range(2):
    #     for i in range(num_qubits):
    #         feature_map.ry(x[i], i)
    #     feature_map.cx(num_qubits-1, 0)
    #     for i in range(num_qubits-1):
    #         feature_map.cx(i, i+1)
    #     for i in range(num_qubits):
    #         feature_map.ry(x[i], i)
    #     feature_map.cx(num_qubits - 1, num_qubits - 2)
    #     feature_map.cx(0, num_qubits - 1)
    #     for i in range(1, num_qubits - 1):
    #         feature_map.cx(i, i-1)

    # Experiment 19: 79.9 on 3 reps and 4 vqc and 10 perc data. This is almost same as experiment 10, but only the last two gates are reversed in order
    # for _ in range(reps):
    #     for i in range(num_qubits):
    #         feature_map.rx(x[i], i)
    #         feature_map.rz(x[i], i)
    #     for control in range(num_qubits-1, 0, -1):
    #         target = control - 1
    #         feature_map.rx(x[target], target)
    #         feature_map.cx(control, target)
    #         feature_map.rx(x[target], target)
    #     for i in range(num_qubits):
    #         feature_map.rz(x[i], i)
    #         feature_map.rx(x[i], i)

    # Experiment 20: 78.5 | Same as exp 10 just starting 2 and ending 2 xz swapped
    # 79.6 with 3 and 4 vqc 2perc data | 79.0 with 10 perc

    # for _ in range(reps):               # Exp 20
    #     for i in range(num_qubits):
    #         feature_map.rz(x[i], i)
    #         feature_map.rx(x[i], i)
    #     for control in range(num_qubits-1, 0, -1):
    #         target = control - 1
    #         feature_map.rx(x[target], target)
    #         feature_map.cx(control, target)
    #         feature_map.rx(x[target], target)
    #     for i in range(num_qubits):
    #         feature_map.rz(x[i], i)
    #         feature_map.rx(x[i], i)

    # ALL SCORES ARE WITH 2 REPS OF SU2 LINEARLY ENTAGLED BY DEFAULT UNLESS MENTIONED OTHERWISE
    # BUILD FEATURE MAP HERE - END

    #return the feature map which is either a FeatureMap or QuantumCircuit object
    return feature_map

fmap = feature_map()
fmap.draw(output='mpl', filename='final_fmap.jpg')

%%write_and_run variational_circuit.py
# the write_and_run function writes the content in this cell into the file "variational_circuit.py"

### WRITE YOUR CODE BETWEEN THESE LINES - START
    
# import libraries that are used in the function below.
from qiskit import QuantumCircuit
from qiskit.circuit import ParameterVector
from qiskit.circuit.library import  RealAmplitudes, EfficientSU2, TwoLocal, NLocal
    
### WRITE YOUR CODE BETWEEN THESE LINES - END

def variational_circuit():
    # BUILD VARIATIONAL CIRCUIT HERE - START
    
    # import required qiskit libraries if additional libraries are required
    
    # build the variational circuit
    # var_circuit = EfficientSU2(3, entanglement='linear', reps=2, insert_barriers=True)
    var_circuit = TwoLocal(3, ['ry', 'rz'], ['cx'], entanglement='linear', reps=4, insert_barriers=True)
    # BUILD VARIATIONAL CIRCUIT HERE - END
    
    # return the variational circuit which is either a VaritionalForm or QuantumCircuit object
    return var_circuit

var_circ = variational_circuit()
var_circ.draw(output='mpl', filename='final_var_circ.jpg')

def classical_optimizer():
    # CHOOSE AND RETURN CLASSICAL OPTIMIZER OBJECT - START
    
    # import the required clasical optimizer from qiskit.aqua.optimizers
    from qiskit.aqua.components.optimizers import ADAM, SPSA, COBYLA
    # create an optimizer object
#     cls_opt = ADAM(maxiter=250, lr=1e-2, beta_1=0.9, beta_2=0.9, tol=1e-6)
    cls_opt = COBYLA(maxiter=100, disp=True, tol=1e-6)
#     cls_opt = SPSA(maxiter=250)
    # CHOOSE AND RETURN CLASSICAL OPTIMIZER OBJECT - END
    return cls_opt

def call_back_vqc(eval_count, var_params, eval_val, index):
    print("eval_count: {}".format(eval_count))
    print("var_params: {}".format(var_params))
    print("eval_val: {}".format(eval_val))
    print("index: {}".format(index))

# A fixed seed so that we get the same answer when the same input is given. 
seed = 10598

# Setting our backend to qasm_simulator with the "statevector" method on. This particular setup is given as it was 
# Found to perform better than most. Feel free to play around with different backend options.
backend = Aer.get_backend('qasm_simulator')
backend_options = {"method": "statevector"}

# Creating a quantum instance using the backend and backend options taken before
quantum_instance = QuantumInstance(backend, shots=1024, seed_simulator=seed, seed_transpiler=seed, 
                                   backend_options=backend_options)

# Creating a VQC instance which you will be used for training. Make sure you input the correct training_dataset and 
# Testing_dataset as defined in your program.
vqc = VQC(optimizer=classical_optimizer(), 
          feature_map=feature_map(), 
          var_form=variational_circuit(), 
          callback=call_back_vqc, 
          training_dataset=training_input,     # Training_input must be initialized with your training dataset
          test_dataset=test_input)             # Testing_input must be initialized with your testing dataset

start = time.process_time()

result = vqc.run(quantum_instance)

print("time taken: ")
print(time.process_time() - start)

print("testing success ratio: {}".format(result['testing_accuracy']))

print(repr(vqc.optimal_params))

%%write_and_run optimal_params.py
# The write_and_run function writes the content in this cell into the file "optimal_params.py"

### WRITE YOUR CODE BETWEEN THESE LINES - START
    
# import libraries that are used in the function below.
import numpy as np
    
### WRITE YOUR CODE BETWEEN THESE LINES - END

def return_optimal_params():
    # STORE THE OPTIMAL PARAMETERS AS AN ARRAY IN THE VARIABLE optimal_parameters 
    
    optimal_parameters = np.array([-0.82871645, -0.50819365, -0.83197798,  3.64408214,  0.99032149,
       -1.72461303, -1.8798925 ,  0.22324836,  0.7469075 , -0.29177951,
        1.18396727, -0.8831999 , -0.20987174,  0.1609452 ,  0.72160706,
       -0.59644311, -1.46078401, -1.12552463,  1.34940875, -0.40489246,
       -1.20122758,  0.10007856, -0.95181456, -1.6608501 ,  0.70347488,
       -0.37977302, -0.26907574, -0.08800676, -0.56356505,  1.85327073])
    
    # STORE THE OPTIMAL PARAMETERS AS AN ARRAY IN THE VARIABLE optimal_parameters 
    return np.array(optimal_parameters)

solution = ['feature_map.py','variational_circuit.py','optimal_params.py']
file = open("summary.py","w")
file.truncate(0)
for i in solution:    
    with open(i) as f:
        with open("summary.py", "a") as f1:
            for line in f:
                f1.write(line)
file.close()

#imports required for the grading function 
from qiskit import *
from qiskit.aqua import QuantumInstance
from qiskit.aqua.algorithms import VQC
from qiskit.aqua.components.feature_maps import FeatureMap
from qiskit.aqua.components.variational_forms import VariationalForm
import numpy as np

def grade(test_data, test_labels, feature_map, variational_form, optimal_params, find_circuit_cost=True, verbose=True):
    seed = 10598
    model_accuracy = None 
    circuit_cost=None 
    ans = None
    unrolled_circuit = None
    result_msg=''
    data_dim = np.array(test_data).shape[1]
    dataset_size = np.array(test_data).shape[0]
    dummy_training_dataset=training_input = {'A':np.ones((2,data_dim)), 'B':np.ones((2, data_dim))}
    
    # converting 4's to 0's and 9's to 1's for checking 
    test_labels_transformed = np.where(test_labels==4, 0., 1.)
    max_qubit_count = 6
    max_circuit_cost = 2000
    
    # Section 1
    if feature_map is None:
        result_msg += 'feature_map variable is None. Please submit a valid entry' if verbose else ''
    elif variational_form is None: 
        result_msg += 'variational_form variable is None. Please submit a valid entry' if verbose else ''
    elif optimal_params is None: 
        result_msg += 'optimal_params variable is None. Please submit a valid entry' if verbose else ''
    elif test_data is None: 
        result_msg += 'test_data variable is None. Please submit a valid entry' if verbose else ''
    elif test_labels is None: 
        result_msg += 'test_labels variable is None. Please submit a valid entry' if verbose else ''
    elif not isinstance(feature_map, (QuantumCircuit, FeatureMap)):
        result_msg += 'feature_map variable should be a QuantumCircuit or a FeatureMap not (%s)' % \
                      type(feature_map) if verbose else ''
    elif not isinstance(variational_form, (QuantumCircuit, VariationalForm)):
        result_msg += 'variational_form variable should be a QuantumCircuit or a VariationalForm not (%s)' % \
                      type(variational_form) if verbose else ''
    elif not isinstance(test_data, np.ndarray):
        result_msg += 'test_data variable should be a numpy.ndarray not (%s)' % \
                      type(test_data) if verbose else ''
    elif not isinstance(test_labels, np.ndarray):
        result_msg += 'test_labels variable should be a numpy.ndarray not (%s)' % \
                      type(test_labels) if verbose else ''
    elif not isinstance(optimal_params, np.ndarray):
        result_msg += 'optimal_params variable should be a numpy.ndarray not (%s)' % \
                      type(optimal_params) if verbose else ''
    elif not dataset_size == test_labels_transformed.shape[0]:
        result_msg += 'Dataset size and label array size must be equal'
    # Section 2
    else:
        
        # setting up COBYLA optimizer as a dummy optimizer
        from qiskit.aqua.components.optimizers import COBYLA
        dummy_optimizer = COBYLA()

        # setting up the backend and creating a quantum instance
        backend = Aer.get_backend('qasm_simulator')
        backend_options = {"method": "statevector"}
        quantum_instance = QuantumInstance(backend, 
                                           shots=2000, 
                                           seed_simulator=seed, 
                                           seed_transpiler=seed, 
                                           backend_options=backend_options)

        # creating a VQC instance and running the VQC.predict method to get the accuracy of the model 
        vqc = VQC(optimizer=dummy_optimizer, 
                  feature_map=feature_map, 
                  var_form=variational_form, 
                  training_dataset=dummy_training_dataset)
        
        from qiskit.transpiler import PassManager
        from qiskit.transpiler.passes import Unroller
        pass_ = Unroller(['u3', 'cx'])
        pm = PassManager(pass_)
        # construct circuit with first datapoint
        circuit = vqc.construct_circuit(data[0], optimal_params)
        unrolled_circuit = pm.run(circuit)
        gates = unrolled_circuit.count_ops()
        if 'u3' in gates: 
            circuit_cost = gates['u3']
        if 'cx' in gates: 
            circuit_cost+= 10*gates['cx']
        
        if circuit.num_qubits > max_qubit_count:
            result_msg += 'Your quantum circuit is using more than 6 qubits. Reduce the number of qubits used and try again.'
        elif circuit_cost > max_circuit_cost:
            result_msg += 'The cost of your circuit is exceeding the maximum accpetable cost of 2000. Reduce the circuit cost and try again.'
        else: 
            
            ans = vqc.predict(test_data, quantum_instance=quantum_instance, params=np.array(optimal_params))
            model_accuracy = np.sum(np.equal(test_labels_transformed, ans[1]))/len(ans[1])

            result_msg += 'Accuracy of the model is {}'.format(model_accuracy) if verbose else ''
            result_msg += ' and circuit cost is {}'.format(circuit_cost) if verbose else ''
            
    return model_accuracy, circuit_cost, ans, result_msg, unrolled_circuit

grading_dataset_size=2000    # this value is not per digit but in total
grading_features = data_features[-grading_dataset_size:]
grading_labels = data_labels[-grading_dataset_size:]

start = time.process_time()

accuracy, circuit_cost, ans, result_msg, full_circuit  =  grade(test_data=grading_features, 
                                                                test_labels=grading_labels, 
                                                                feature_map=feature_map(), 
                                                                variational_form=variational_circuit(), 
                                                                optimal_params=return_optimal_params())

print("time taken: {} seconds".format(time.process_time() - start))
print(result_msg)

print("Accuracy of the model: {}".format(accuracy))
print("Circuit Cost: {}".format(circuit_cost))
print("The complete unrolled circuit: ")
full_circuit.draw()