The purpose of this notebook is to show how to use Qiskit's `Parameter` object to create and manipulated PQCs.

import sys
sys.path.append('../../Pyfiles')

import qiskit
qiskit.__qiskit_version__

from qiskit import QuantumCircuit, Aer
from qiskit.circuit import Parameter

import numpy as np
from sklearn.utils import shuffle
import matplotlib.pyplot as plt

%matplotlib inline
from optimizer import *

dataID = '2a'
dataPath = r'../../dataset/data{0}.txt'.format(dataID)
dataLabel = r'../../dataset/data{0}label.txt'.format(dataID)

dataCoords = np.loadtxt(dataPath)
dataLabels = np.loadtxt(dataLabel)

# Make a data structure which is easier to work with
# for shuffling. 
# Also, notice we change the data labels from {0, 1} to {-1, +1}
data = list(zip(dataCoords, 2*dataLabels-1))
shuffled_data = np.array(shuffle(data), dtype='object')

fig = plt.figure()
ax = fig.add_axes([0,0,1,1])

ax.plot(np.ravel(dataCoords[np.where(dataLabels == 0)])[::2],
np.ravel(dataCoords[np.where(dataLabels == 0)])[1::2], ls='', marker='o')

ax.plot(np.ravel(dataCoords[np.where(dataLabels == 1)])[::2],
np.ravel(dataCoords[np.where(dataLabels == 1)])[1::2], ls='', marker='o')

shuffled_data

x0 = Parameter('x0')
x1 = Parameter('x1')

t0 = Parameter('t0')
t1 = Parameter('t1')
t2 = Parameter('t2')
t3 = Parameter('t3')


qc = QuantumCircuit(2)

qc.rx(x0, 0)
qc.rx(x1, 1)


qc.ry(np.pi/4, range(qc.width()))
qc.rz(np.pi/4, range(qc.width()))

qc.rx(t0, 0)
qc.rz(t1, 0)

qc.rx(t2, 1)
qc.rz(t3, 1)


qc.draw()

def calc_probs_from_results(counts, converter_dict):
    r'''Given a Qiskit `counts` object (i.e., a dictionary containing
    bitstrings and outcome probabilities), make an assignment to the
    probabilities of a +/- 1 label.'''
    
    probsPlus = 0
    probsMinus = 0

    for k in converter_dict.keys():
        if converter_dict[k] == 1:
            probsPlus += counts[k]
        else:
            probsMinus += counts[k]
            
    return probsPlus, probsMinus

def label_assignment(probsPlus, probsMinus):
    r'''Given the probabilities of +/- 1, declare the
    label which should be assigned'''
    
    if probsPlus > probsMinus:
        label = 1
    elif probsMinus > probsPlus:
        label = -1
    else:
        # An edge case: if the probs are equal, just return a random label
        label= 2*np.random.binomial(1, .5) -1
    
    return label

def prediction_loss(label, prediction):
    
    return np.abs(label - prediction)

def loss(params, paramNames, dataCircuits, converter_dict):
    r'''Function which computes the loss.
    
    Inputs:
       params: The particular parameter values
       paramNames: The name of the parameters
       dataCircuits: List of partially-parameterized circuits
       converter_dict: A dictionary representing how to map from bitstrings to labels
    
    Outputs:
       The loss
    '''
    
    # Bind the variables for the classifier part of the circuit
    fully_bound_qcs = [d.bind_parameters(dict(zip(paramNames, params)))for d in dataCircuits]

    # Run all the circuits
    job = be.run(fully_bound_qcs)

    # Get the counts
    counts = job.result().get_counts()

    # Now, calculate the probabilities of the +/- 1 outcome
    probsList = [calc_probs_from_results(C, converter_dict) for C in list(counts)]

    # Do the label assignment
    predictions_list = [label_assignment(P[0], P[1]) for P in probsList]

    # Calculate the loss on each data point
    prediction_loss_list = [prediction_loss(data[j][1], predictions_list[j]) for j in range(len(predictions_list))]

    # Sum it up and return
    return np.sum(prediction_loss_list)

# Converter dictionary for 2-qubit circuit.
two_q_converter_dict = {'00': 1, '01': -1, '10': -1, '11': 1}

# We'll use the statevector simulator, to get the
# exact outcome probabilities.
be = Aer.get_backend('statevector_simulator')

#load the SPSA optimizer from the optimizer.py file
from optimizer import *


c = 1
a = c/5

currentParams = np.random.uniform(-np.pi, np.pi, size=4)

lossList = []
cList = []
aList = []
paramsList = []


for j in range(10):
    print(j)
    cj = c/(j+1)**(1/3)
    aj = a/(j+1)
    aList.append(aj)
    cList.append(cj)

    np.random.seed(j)
    data_ixs = np.random.choice(len(shuffled_data), size=750)
    data_set = shuffled_data[data_ixs]

    # Generate a list of circuits whose parameters are partially bound,
    # based on the data set.
    dataCircuits = [qc.bind_parameters({x0:X[0][0], x1:X[0][1]}) for X in data_set]

    # Use a lambda function to make a call to the loss function
    # where the only thing which changes is the parameter values
    # for the classification part of the circuit.
    L = lambda x: loss(x, [t0, t1, t2, t3], dataCircuits, two_q_converter_dict)

    lossList.append(L(currentParams))

    # Update the parameters
    currentParams = SPSA_update(L, currentParams, aj, cj)     

    paramsList.append(currentParams)

lossList.append(L(currentParams))

fig = plt.figure(figsize=(15, 10))

ax = fig.add_subplot(2, 2, 1)
ax.plot(lossList)

ax = fig.add_subplot(2, 2, 2)
ax.plot(aList, label='a')
ax.plot(cList, label='c')
ax.legend(loc=0)
ax.set_yscale('log')

ax = fig.add_subplot(2, 2, 4)
for j in range(len(paramsList[0])):
    ax.plot([p[j] for p in paramsList], label='t{0}'.format(j))
ax.legend(loc=0)